From 2b2fd957cf4b22604690c9269574c541bf033e30 Mon Sep 17 00:00:00 2001 From: Christopher Dollard Date: Mon, 7 Oct 2024 19:49:07 -0400 Subject: [PATCH] 0.10.6 - Script tweaks (#52) --- package.json | 2 +- src/core/architects/build_and_power.ts | 45 +++++++------ .../established_hq/fixed_complete.ts | 38 +++++------ src/core/architects/established_hq/lost.ts | 10 ++- src/core/architects/fissure.ts | 19 ++---- src/core/architects/lost_miners.ts | 16 ++--- src/core/architects/nomads.ts | 29 +++------ src/core/architects/slugs.ts | 10 ++- src/core/architects/treasure.ts | 29 ++++----- .../architects/utils/creature_spawners.ts | 64 ++++++++++++++++--- src/core/architects/utils/objectives.ts | 27 ++++++++ src/core/architects/utils/script.ts | 26 ++++---- src/core/common/context.ts | 10 ++- src/core/models/architect.ts | 9 ++- .../transformers/04_ephemera/02_enscribe.ts | 4 +- .../transformers/04_ephemera/04_program.ts | 57 +++++++++-------- src/webui/components/context_editor/index.tsx | 4 ++ 17 files changed, 232 insertions(+), 167 deletions(-) create mode 100644 src/core/architects/utils/objectives.ts diff --git a/package.json b/package.json index 778002b..efcc75e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "groundhog", - "version": "0.10.5", + "version": "0.10.6", "homepage": "https://charredutensil.github.io/groundhog", "private": true, "dependencies": { diff --git a/src/core/architects/build_and_power.ts b/src/core/architects/build_and_power.ts index 1389a49..2e80593 100644 --- a/src/core/architects/build_and_power.ts +++ b/src/core/architects/build_and_power.ts @@ -10,11 +10,12 @@ import { Plan } from "../models/plan"; import { OrderedOrEstablishedPlan } from "../transformers/01_planning/05_establish"; import { DefaultCaveArchitect, PartialArchitect } from "./default"; import { intersectsOnly } from "./utils/intersects"; +import { gObjectives } from "./utils/objectives"; import { Rough, mkRough } from "./utils/rough"; import { declareStringFromLore, eventChain, - eventChainSynchronized, + EventChainLine, mkVars, scriptFragment, transformPoint, @@ -39,22 +40,19 @@ function buildAndPower( "prime" | "objectives" | "script" | "scriptGlobals" > { const g = mkVars(`gBp${template.inspectAbbrev}`, [ - "onBuild", "built", - "onPower", + "checkPower", "doneCount", "done", "msgA", "msgB", "msgC", - "onComplete", ]); const metadata: BuildAndPowerMetadata = { tag: TAG, template }; const mv = (plan: Plan) => mkVars(`p${plan.id}Bp${template.inspectAbbrev}`, [ "arrow", "building", - "onInit", "onBuild", ]); return { @@ -81,7 +79,7 @@ function buildAndPower( sufficient: true, }; }, - scriptGlobals({ cavern }) { + scriptGlobals({ cavern, sh }) { const pvs = cavern.plans .filter( (plan) => @@ -96,30 +94,31 @@ function buildAndPower( // have collisions. In theory, it shouldn't be possible to level up // multiple buildings at the same time. `building ${g.built}`, - `when(${template.id}.${minLevel > 1 ? "levelup" : "new"})[${g.onBuild}]`, - eventChain( - g.onBuild, + sh.trigger( + `when(${template.id}.${minLevel > 1 ? "levelup" : "new"})`, `savebuilding:${g.built};`, minLevel > 1 && `((${g.built}.level<${minLevel}))return;`, - ...pvs.map((v) => `${v.onBuild};` satisfies `${string};`), + ...pvs.map((v) => `${v.onBuild};` satisfies EventChainLine), ), // Second trigger: power state changes. + `int ${g.checkPower}=0`, `int ${g.doneCount}=0`, ...pvs.map((v) => `arrow ${v.arrow}`), ...pvs.map((v) => `building ${v.building}`), - `when(${template.id}.poweron)[${g.onPower}]`, - `when(${template.id}.poweroff)[${g.onPower}]`, - eventChainSynchronized( - g.onPower, + `when(${template.id}.poweron)[${g.checkPower}+=1]`, + `when(${template.id}.poweroff)[${g.checkPower}+=1]`, + sh.trigger( + `when(${g.checkPower}==1)`, `${g.doneCount}=0;`, ...pvs.flatMap( (v) => [ `((${v.building}.powered>0))[hidearrow:${v.arrow}][showarrow:${v.building}.row,${v.building}.column,${v.arrow}];`, `((${v.building}.powered>0))${g.doneCount}+=1;`, - ] satisfies `${string};`[], + ] satisfies EventChainLine[], ), + `((${g.checkPower}>1))[${g.checkPower}=1][${g.checkPower}=0];`, ), // Messages & done trigger @@ -163,8 +162,13 @@ function buildAndPower( buildingName: template.name, }, ), - `if(${g.doneCount}>=${pvs.length})[${g.onComplete}]`, - eventChain(g.onComplete, `msg:${g.msgC};`, "wait:2;", `${g.done}=1;`), + sh.trigger( + `if(${g.doneCount}>=${pvs.length})`, + `${gObjectives.met}+=1;`, + `msg:${g.msgC};`, + "wait:2;", + `${g.done}=1;`, + ), ); }, script({ cavern, plan }) { @@ -184,8 +188,7 @@ function buildAndPower( return scriptFragment( `# P${plan.id}: Build and Power ${template.name}`, - `if(${openOnSpawn ? `time:0` : `change:${atp}`})[${v.onInit}]`, - eventChain(v.onInit, `showarrow:${atp},${v.arrow};`), + `if(${openOnSpawn ? `time:0` : `change:${atp}`})[showarrow:${atp},${v.arrow}]`, eventChain( v.onBuild, // Filter out buildings outside the baseplate rectangle @@ -193,8 +196,10 @@ function buildAndPower( `((${g.built}.column>=${bp.right - cavern.left}))return;`, `((${g.built}.row<${bp.top - cavern.top}))return;`, `((${g.built}.row>=${bp.bottom - cavern.top}))return;`, + // Setting a building variable to the value of another building + // variable doesn't work in MMScript for some reason `savebuilding:${v.building};`, - `${g.onPower};`, + `${g.checkPower}+=1;`, ), ); }, diff --git a/src/core/architects/established_hq/fixed_complete.ts b/src/core/architects/established_hq/fixed_complete.ts index 89b1565..b36ed1b 100644 --- a/src/core/architects/established_hq/fixed_complete.ts +++ b/src/core/architects/established_hq/fixed_complete.ts @@ -13,9 +13,10 @@ import { GEOLOGICAL_CENTER, ALL_BUILDINGS, } from "../../models/building"; +import { gObjectives } from "../utils/objectives"; import { declareStringFromLore, - eventChain, + EventChainLine, mkVars, scriptFragment, } from "../utils/script"; @@ -36,12 +37,7 @@ const T0_BUILDINGS = [ const T0_CRYSTALS = T0_BUILDINGS.reduce((r, bt) => r + bt.crystals, 0); -const gFCHQ = mkVars("gFCHQ", [ - "onInit", - "onBaseDestroyed", - "msgBaseDestroyed", - "wasBaseDestroyed", -]); +const gFCHQ = mkVars("gFCHQ", ["msgLose", "wasBaseDestroyed"]); export const FC_BASE: Pick< Architect, @@ -64,33 +60,33 @@ export const FC_BASE: Pick< discovered: true, templates: () => T0_BUILDINGS, }), - scriptGlobals: ({ cavern }) => { + scriptGlobals: ({ cavern, sh }) => { return scriptFragment( `# Globals: Fixed Complete HQ`, - `if(time:0)[${gFCHQ.onInit}]`, - eventChain( - gFCHQ.onInit, + sh.trigger( + "if(time:0)", // Can't just disable buildings because that disables fences - and // nobody wants that. - ...ALL_BUILDINGS.map((bt) => `disable:${bt.id};` as `${string};`), + ...ALL_BUILDINGS.map( + (bt) => `disable:${bt.id};` satisfies EventChainLine, + ), ), declareStringFromLore( cavern, LoreDie.failureBaseDestroyed, - gFCHQ.msgBaseDestroyed, + gFCHQ.msgLose, FAILURE_BASE_DESTROYED, {}, {}, ), `int ${gFCHQ.wasBaseDestroyed}=0`, - `if(${TOOL_STORE.id}<=0)[${gFCHQ.onBaseDestroyed}]`, - `if(${POWER_STATION.id}<=0)[${gFCHQ.onBaseDestroyed}]`, - `if(${SUPPORT_STATION.id}<=0)[${gFCHQ.onBaseDestroyed}]`, - eventChain( - gFCHQ.onBaseDestroyed, - `((${gFCHQ.wasBaseDestroyed}>0))return;`, - `${gFCHQ.wasBaseDestroyed}=1;`, - `msg:${gFCHQ.msgBaseDestroyed};`, + `if(${TOOL_STORE.id}<=0)[${gFCHQ.wasBaseDestroyed}=1]`, + `if(${POWER_STATION.id}<=0)[${gFCHQ.wasBaseDestroyed}=1]`, + `if(${SUPPORT_STATION.id}<=0)[${gFCHQ.wasBaseDestroyed}=1]`, + sh.trigger( + `if(${gFCHQ.wasBaseDestroyed}>=1)`, + `((${gObjectives.won}>0))return;`, + `msg:${gFCHQ.msgLose};`, `wait:5;`, `lose;`, ), diff --git a/src/core/architects/established_hq/lost.ts b/src/core/architects/established_hq/lost.ts index 292eeab..55459dd 100644 --- a/src/core/architects/established_hq/lost.ts +++ b/src/core/architects/established_hq/lost.ts @@ -8,7 +8,6 @@ import { scriptFragment, mkVars, transformPoint, - eventChain, declareStringFromLore, } from "../utils/script"; import { BASE, HqMetadata, getPlaceBuildings, getPrime } from "./base"; @@ -39,7 +38,7 @@ const LOST_BASE: Pick< }, scriptGlobals: () => scriptFragment("# Globals: Lost HQ", `int ${gLostHq.foundHq}=0`), - script({ cavern, plan }) { + script({ cavern, plan, sh }) { const discoPoint = getDiscoveryPoint(cavern, plan)!; const shouldPanMessage = cavern.ownsScriptOnDiscover[ @@ -50,7 +49,7 @@ const LOST_BASE: Pick< return r.pearlRadius > p.pearlRadius ? r : p; }).center; - const v = mkVars(`p${plan.id}LostHq`, ["messageDiscover", "onDiscover"]); + const v = mkVars(`p${plan.id}LostHq`, ["messageDiscover"]); return scriptFragment( `# P${plan.id}: Lost HQ`, @@ -63,9 +62,8 @@ const LOST_BASE: Pick< {}, {}, ), - `if(change:${transformPoint(cavern, discoPoint)})[${v.onDiscover}]`, - eventChain( - v.onDiscover, + sh.trigger( + `if(change:${transformPoint(cavern, discoPoint)})`, shouldPanMessage && `msg:${v.messageDiscover};`, shouldPanMessage && `pan:${transformPoint(cavern, camPoint)};`, `wait:1;`, diff --git a/src/core/architects/fissure.ts b/src/core/architects/fissure.ts index 7be082b..1aef111 100644 --- a/src/core/architects/fissure.ts +++ b/src/core/architects/fissure.ts @@ -3,7 +3,7 @@ import { DefaultHallArchitect, PartialArchitect } from "./default"; import { mkRough, Rough } from "./utils/rough"; import { declareStringFromLore, - eventChain, + EventChainLine, mkVars, scriptFragment, transformPoint, @@ -35,7 +35,6 @@ const sVars = (plan: Plan) => mkVars(`p${plan.id}Fissure`, [ "onDiscover", `onTrip`, - `onFissure`, `msgForeshadow`, `spawn`, "tripCount", @@ -44,7 +43,7 @@ const sVars = (plan: Plan) => const BASE: PartialArchitect = { ...DefaultHallArchitect, prime: () => METADATA, - script: ({ cavern, plan }) => { + script: ({ cavern, plan, sh }) => { const v = sVars(plan); const discoveryPoints = getDiscoveryPoints(cavern, plan); const panTo = plan.innerPearl[0][Math.floor(plan.innerPearl[0].length / 2)]; @@ -68,17 +67,13 @@ const BASE: PartialArchitect = { {}, ), ...discoveryPoints.map( - (pos) => `if(change:${transformPoint(cavern, pos)})[${v.onTrip}]`, + (pos) => `if(change:${transformPoint(cavern, pos)})[${v.tripCount}+=1]`, ), ...drillPoints.map( - (pos) => `if(drill:${transformPoint(cavern, pos)})[${v.onTrip}]`, + (pos) => `if(drill:${transformPoint(cavern, pos)})[${v.tripCount}+=1]`, ), - eventChain( - v.onTrip, - `((${v.tripCount}==${trips}))[${v.onFissure}][${v.tripCount}+=1];`, - ), - eventChain( - v.onFissure, + sh.trigger( + `if(${v.tripCount}>=${trips})`, `wait:random(5)(30);`, `shake:1;`, `msg:${v.msgForeshadow};`, @@ -91,7 +86,7 @@ const BASE: PartialArchitect = { .filter((pos) => cavern.tiles.get(...pos)?.isWall) .map( (pos) => - `drill:${transformPoint(cavern, pos)};` satisfies `${string};`, + `drill:${transformPoint(cavern, pos)};` satisfies EventChainLine, ), cavern.context.hasMonsters && `${v.spawn};`, ), diff --git a/src/core/architects/lost_miners.ts b/src/core/architects/lost_miners.ts index 63451a4..f408ef8 100644 --- a/src/core/architects/lost_miners.ts +++ b/src/core/architects/lost_miners.ts @@ -38,6 +38,7 @@ import { FOUND_LM_BREADCRUMB, FOUND_LOST_MINERS, } from "../lore/graphs/events"; +import { gObjectives } from "./utils/objectives"; export type LostMinersMetadata = { readonly tag: "lostMiners"; @@ -260,20 +261,19 @@ const BASE: PartialArchitect = { ), eventChain( gLostMiners.onFoundAll, + `${gObjectives.met}+=1;`, `msg:${gLostMiners.messageFoundAll};`, `wait:3;`, `${gLostMiners.done}=1;`, ), ); }, - script({ cavern, plan }) { + script({ cavern, plan, sh }) { const rng = cavern.dice.script(plan.id); const { lostMinerCaves } = countLostMiners(cavern); const v = mkVars(`p${plan.id}LostMiners`, [ "msgFoundBreadcrumb", "msgFoundMiners", - "onFoundBreadcrumb", - "onFoundMiners", "onIncomplete", "wasFound", ]); @@ -310,9 +310,8 @@ const BASE: PartialArchitect = { }, ), `int ${v.wasFound}=0`, - `if(change:${transformPoint(cavern, minersPoint)})[${v.onFoundMiners}]`, - eventChain( - v.onFoundMiners, + sh.trigger( + `if(change:${transformPoint(cavern, minersPoint)})`, shouldPanOnMiners && `pan:${transformPoint(cavern, minersPoint)};`, `${v.wasFound}=1;`, `${gLostMiners.remainingCaves}-=1;`, @@ -334,9 +333,8 @@ const BASE: PartialArchitect = { vehicleName: breadcrumb!.template.name, }, ), - `if(change:${transformPoint(cavern, breadcrumbPoint)})[${v.onFoundBreadcrumb}]`, - eventChain( - v.onFoundBreadcrumb, + sh.trigger( + `if(change:${transformPoint(cavern, breadcrumbPoint)})`, `((${v.wasFound}>0))return;`, `pan:${transformPoint(cavern, breadcrumbPoint)};`, `msg:${v.msgFoundBreadcrumb};`, diff --git a/src/core/architects/nomads.ts b/src/core/architects/nomads.ts index e8d29ae..2373396 100644 --- a/src/core/architects/nomads.ts +++ b/src/core/architects/nomads.ts @@ -5,12 +5,7 @@ import { intersectsAny, intersectsOnly, isDeadEnd } from "./utils/intersects"; import { getPlaceRechargeSeams, sprinkleOre } from "./utils/resources"; import { position, randomlyInTile } from "../models/position"; import { pickPoint } from "./utils/placement"; -import { - declareStringFromLore, - eventChain, - mkVars, - scriptFragment, -} from "./utils/script"; +import { declareStringFromLore, mkVars, scriptFragment } from "./utils/script"; import { SUPPORT_STATION } from "../models/building"; import { Hardness, Tile } from "../models/tiles"; import { @@ -45,12 +40,7 @@ const VEHICLE_BIDS = [ { item: null, bid: 1 }, ] as const; -export const gNomads = mkVars("gNomads", [ - "messageBuiltBase", - "onBuiltBase", - "onInit", - "onFoundHq", -]); +export const gNomads = mkVars("gNomads", ["messageBuiltBase"]); const BASE: PartialArchitect = { ...DefaultCaveArchitect, @@ -154,21 +144,19 @@ const BASE: PartialArchitect = { }), }; }, - scriptGlobals({ cavern }) { + scriptGlobals({ cavern, sh }) { if (cavern.plans.some((plan) => plan.metadata?.tag === "hq")) { // Has HQ: Disable everything until it's found. return scriptFragment( "# Globals: Nomads with Lost HQ", - `if(time:0)[${gNomads.onInit}]`, - eventChain( - gNomads.onInit, + sh.trigger( + `if(time:0)`, "disable:miners;", "disable:buildings;", "disable:vehicles;", ), - `if(${gLostHq.foundHq}>0)[${gNomads.onFoundHq}]`, - eventChain( - gNomads.onFoundHq, + sh.trigger( + `if(${gLostHq.foundHq}>0)`, "enable:miners;", "enable:buildings;", "enable:vehicles;", @@ -187,8 +175,7 @@ const BASE: PartialArchitect = { {}, {}, ), - `if(${SUPPORT_STATION.id}.new)[${gNomads.onBuiltBase}]`, - eventChain(gNomads.onBuiltBase, `msg:${gNomads.messageBuiltBase};`), + `if(${SUPPORT_STATION.id}.new)[msg:${gNomads.messageBuiltBase}]`, ); }, }; diff --git a/src/core/architects/slugs.ts b/src/core/architects/slugs.ts index 624cee9..12e8a8a 100644 --- a/src/core/architects/slugs.ts +++ b/src/core/architects/slugs.ts @@ -14,7 +14,6 @@ import { getDiscoveryPoint } from "./utils/discovery"; import { declareStringFromLore, DzPriorities, - eventChain, mkVars, scriptFragment, transformPoint, @@ -61,7 +60,7 @@ const SLUG_NEST: PartialArchitect = { const pos = getDiscoveryPoint(cavern, plan); return [{ pos, priority: DzPriorities.TRIVIAL }]; }, - script: ({ cavern, plan }) => { + script: ({ cavern, plan, sh }) => { const discoPoint = getDiscoveryPoint(cavern, plan); if ( !discoPoint || @@ -72,7 +71,7 @@ const SLUG_NEST: PartialArchitect = { return scriptFragment(`# P${plan.id}: Slug Nest`, `# [Skip]`); } - const v = mkVars(`p${plan.id}SgNest`, ["messageDiscover", "onDiscover"]); + const v = mkVars(`p${plan.id}SgNest`, ["messageDiscover"]); return scriptFragment( `# P${plan.id}: Slug Nest`, @@ -84,9 +83,8 @@ const SLUG_NEST: PartialArchitect = { {}, {}, ), - `if(change:${transformPoint(cavern, discoPoint)})[${v.onDiscover}]`, - eventChain( - v.onDiscover, + sh.trigger( + `if(change:${transformPoint(cavern, discoPoint)})`, `msg:${v.messageDiscover};`, `pan:${transformPoint(cavern, discoPoint)};`, ), diff --git a/src/core/architects/treasure.ts b/src/core/architects/treasure.ts index 141e5f2..a4722ee 100644 --- a/src/core/architects/treasure.ts +++ b/src/core/architects/treasure.ts @@ -6,7 +6,6 @@ import { intersectsOnly, isDeadEnd } from "./utils/intersects"; import { declareStringFromLore, DzPriorities, - eventChain, mkVars, scriptFragment, transformPoint, @@ -17,6 +16,7 @@ import { placeSleepingMonsters } from "./utils/creatures"; import { gLostMiners } from "./lost_miners"; import { LoreDie } from "../lore/lore"; import { FOUND_HOARD } from "../lore/graphs/events"; +import { gObjectives } from "./utils/objectives"; const METADATA = { tag: "treasure", @@ -36,7 +36,7 @@ const BASE: PartialArchitect = { }, }; -const g = mkVars("gHoard", ["wasTriggered", "message", "crystalsAvailable"]); +const g = mkVars("gHoard", ["lock", "message", "crystalsAvailable"]); const HOARD: typeof BASE = { ...BASE, @@ -82,7 +82,7 @@ const HOARD: typeof BASE = { } return scriptFragment( "# Globals: Hoard", - `bool ${g.wasTriggered}=false`, + `int ${g.lock}=0`, declareStringFromLore( cavern, LoreDie.foundHoard, @@ -98,7 +98,7 @@ const HOARD: typeof BASE = { const pos = plan.innerPearl[0][0]; return [{ pos, priority: DzPriorities.HINT }]; }, - script({ cavern, plan }) { + script({ cavern, plan, sh }) { if (!cavern.objectives.crystals) { return undefined; } @@ -118,27 +118,22 @@ const HOARD: typeof BASE = { // Generate a script that pans to this cave on discovery if collecting all // of the crystals would win the level. - - const v = mkVars(`p${plan.id}Hoard`, ["onDiscovered", "go"]); - return scriptFragment( `# P${plan.id}: Hoard`, - `if(change:${transformPoint(cavern, discoPoint)})[${v.onDiscovered}]`, - eventChain( - v.onDiscovered, - `((${g.wasTriggered}))[return][${g.wasTriggered}=true];`, + sh.trigger( + `if(change:${transformPoint(cavern, discoPoint)})`, + `((${g.lock}>0))[return][${g.lock}=1];`, `wait:1;`, - `${g.wasTriggered}=false;`, + `${g.lock}=0;`, + // If the game was already won, don't say anything. + `((${gObjectives.won}>0))return;`, // If there's a lost miners objective that isn't fulfilled, don't // act like the level is done. hasLostMiners && `((${gLostMiners.done}<1))return;`, // Count all the crystals in storage and on the floor. `${g.crystalsAvailable}=crystals+Crystal_C;`, - // If this is enough to win the level, alert the player. - `((${g.crystalsAvailable}>=${cavern.objectives.crystals}))${v.go};`, - ), - eventChain( - v.go, + // If this is not enough to win the level, don't alert the player. + `((${g.crystalsAvailable}<${cavern.objectives.crystals}))return;`, `msg:${g.message};`, `pan:${transformPoint(cavern, discoPoint)};`, ), diff --git a/src/core/architects/utils/creature_spawners.ts b/src/core/architects/utils/creature_spawners.ts index 99ab96d..f20da3e 100644 --- a/src/core/architects/utils/creature_spawners.ts +++ b/src/core/architects/utils/creature_spawners.ts @@ -1,5 +1,6 @@ import { PseudorandomStream } from "../../common"; import { Point } from "../../common/geometry"; +import { filterTruthy } from "../../common/utils"; import { CreatureTemplate, SLIMY_SLUG, @@ -8,7 +9,14 @@ import { import { Plan } from "../../models/plan"; import { PreprogrammedCavern } from "../../transformers/04_ephemera/03_preprogram"; import { getDiscoveryPoint } from "./discovery"; -import { eventChain, mkVars, scriptFragment, transformPoint } from "./script"; +import { + eventChain, + EventChainLine, + mkVars, + scriptFragment, + ScriptHelper, + transformPoint, +} from "./script"; type CreatureSpawnerArgs = { readonly armEvent?: string; @@ -31,12 +39,17 @@ const STATE = { EXHAUSTED: 0, /** This spawner has not been activated yet. */ INITIAL: 1, - /** This spawner is waiting to be reactivated by some trigger. */ + /** + * When the cooldown time ends, this spawner will be exhausted. + * The spawner may be promoted back to COOLDOWN before this time is up. + */ AWAITING_REARM: 2, - /** This spawner just activated and is waiting for time to pass. */ + /** When the cooldown time ends, this spawner will be re-armed. */ COOLDOWN: 3, /** This spawner is ready to activate. */ ARMED: 4, + /** This spawner is ready to activate, ignoring global cooldown. */ + ARMED_PRIORITY: 5, } as const; const RETRIGGER_MODES = { @@ -52,6 +65,8 @@ type Emerge = { readonly radius: number; }; +const g = mkVars("gCreatures", ["armThreshold"]); + function getEmerges(plan: Plan): Emerge[] { return plan.path.baseplates.map((bp) => { const [x, y] = bp.center; @@ -95,6 +110,30 @@ function getTriggerPoints( return plan.outerPearl[0].filter((point) => cavern.tiles.get(...point)); } +export function creatureSpawnGlobals({ + cavern: { context }, + sh, +}: { + cavern: PreprogrammedCavern; + sh: ScriptHelper; +}) { + if ( + !(context.hasMonsters || context.hasSlugs) || + context.globalCreatureDelay <= 0 + ) { + return undefined; + } + return scriptFragment( + "# Globals: Creatures", + `int ${g.armThreshold}=${STATE.ARMED}`, + sh.trigger( + `when(${g.armThreshold}>${STATE.ARMED})`, + `wait:${context.globalCreatureDelay};`, + `${g.armThreshold}=${STATE.ARMED};`, + ), + ); +} + export function monsterSpawnScript( args: { cavern: PreprogrammedCavern; plan: Plan }, opts?: Partial, @@ -168,7 +207,14 @@ function creatureSpawnScript( const needTriggerPoints = !(once && opts.triggerOnFirstArmed); return scriptFragment( - `# P${plan.id}: Spawn ${opts.creature.name} x${waveSize}`, + `# P${plan.id}: Spawn ${opts.creature.name}`, + filterTruthy([ + `# x${waveSize}`, + once ? "once" : `/${meanCooldown.toFixed()}s`, + !once && + opts.needCrystals?.increment && + `/${opts.needCrystals.increment}EC`, + ]).join(" "), // Declare variables `int ${v.state}=${STATE.INITIAL}`, @@ -183,8 +229,8 @@ function creatureSpawnScript( opts.initialCooldown && `wait:random(${opts.initialCooldown.min.toFixed(2)})(${opts.initialCooldown.max.toFixed(2)});`, armTriggers.length === 1 - ? `${v.state}=${STATE.ARMED};` - : `((${v.state}>${STATE.INITIAL}))[return][${v.state}=${STATE.ARMED}];`, + ? `${v.state}=${STATE.ARMED_PRIORITY};` + : `((${v.state}>${STATE.INITIAL}))[return][${v.state}=${STATE.ARMED_PRIORITY}];`, opts.triggerOnFirstArmed && `${v.doSpawn};`, ), @@ -201,7 +247,9 @@ function creatureSpawnScript( // Check conditions to reject. opts.needCrystals && `((crystals<${opts.needCrystals.increment ? v.needCrystals : opts.needCrystals.base}))return;`, - `((${v.state}<${STATE.ARMED}))[return][${v.state}=${RETRIGGER_MODES[opts.retriggerMode].afterTriggerState}];`, + `((${v.state}<${cavern.context.globalCreatureDelay > 0 ? g.armThreshold : STATE.ARMED}))[return][${v.state}=${RETRIGGER_MODES[opts.retriggerMode].afterTriggerState}];`, + cavern.context.globalCreatureDelay > 0 && + `${g.armThreshold}=${STATE.ARMED_PRIORITY};`, needCountTriggerEvents && `${v.triggerCount}+=1;`, opts.needCrystals?.increment !== undefined && @@ -213,7 +261,7 @@ function creatureSpawnScript( [ `wait:random(${delay.min.toFixed(2)})(${delay.max.toFixed(2)});`, `emerge:${transformPoint(cavern, [emerge.x, emerge.y])},A,${opts.creature.id},${emerge.radius};`, - ] satisfies `${string};`[], + ] satisfies EventChainLine[], ), // Update the counter. diff --git a/src/core/architects/utils/objectives.ts b/src/core/architects/utils/objectives.ts new file mode 100644 index 0000000..d4e6e6a --- /dev/null +++ b/src/core/architects/utils/objectives.ts @@ -0,0 +1,27 @@ +import { mkVars, scriptFragment, ScriptHelper } from "./script"; +import { PreprogrammedCavern } from "../../transformers/04_ephemera/03_preprogram"; + +export const gObjectives = mkVars("objectives", ["met", "won"]); + +export function objectiveGlobals({ + cavern: { objectives }, + sh, +}: { + cavern: PreprogrammedCavern; + sh: ScriptHelper; +}) { + const resources = (["crystals", "ore", "studs"] as const).filter( + (r) => objectives[r] > 0, + ); + const goalCount = resources.length + objectives.variables.length; + return scriptFragment( + "# Globals: Objectives", + `int ${gObjectives.met}=0`, + `int ${gObjectives.won}=0`, + ...resources.map( + (resource) => + `if(${resource}>=${objectives[resource]})[${gObjectives.met}+=1]`, + ), + `if(${gObjectives.met}>=${goalCount})[${gObjectives.won}=1]`, + ); +} diff --git a/src/core/architects/utils/script.ts b/src/core/architects/utils/script.ts index 4d1eae7..b1e25b7 100644 --- a/src/core/architects/utils/script.ts +++ b/src/core/architects/utils/script.ts @@ -26,33 +26,29 @@ export function transformPoint( } type Falsy = false | null | undefined; +export type EventChainLine = `${string};` | Falsy; export function scriptFragment(...rest: (string | Falsy)[]) { return rest.filter((s) => s).join("\n"); } -export function eventChain(name: string, ...rest: (`${string};` | Falsy)[]) { +export function eventChain(name: string, ...rest: EventChainLine[]) { return `${name}::;\n${scriptFragment(...rest)}\n`; } -export function eventChainSynchronized( - name: string, - ...rest: (`${string};` | Falsy)[] -) { - const semaphore = `${name}_lock`; - return scriptFragment( - `int ${semaphore}=0`, - eventChain(name, `((${semaphore}==0))[${semaphore}=1][${name}_wait];`), - `when(${semaphore}==1)[${name}_syn]`, - eventChain(`${name}_syn`, ...rest, `${semaphore}=0;`), - eventChain(`${name}_wait`, "wait:1;", `${name};`), - ); -} - export function escapeString(s: string) { return s.replace(/\\/g, "").replace(/"/g, '\\"'); } +export class ScriptHelper { + private _uid: number = 0; + + trigger(condition: `${"if" | "when"}(${string})`, ...rest: EventChainLine[]) { + const name = `ec${this._uid++}`; + return `${condition}[${name}]\n${eventChain(name, ...rest)}`; + } +} + export function declareStringFromLore( cavern: EnscribedCavern, die: PseudorandomStream | LoreDie, diff --git a/src/core/common/context.ts b/src/core/common/context.ts index 4838829..7b76dee 100644 --- a/src/core/common/context.ts +++ b/src/core/common/context.ts @@ -255,6 +255,13 @@ export type CavernContext = { * the safety factor to get the final air number. */ airSafetyFactor: number; + /** + * When a monster or slug spawn is triggered, wait at least this many seconds + * before another spawn can be triggered anywhere in the cavern. This is to + * mitigate instances where a single vehicle speeds through a bunch of + * caves and sending a wave of like 17 monsters. + */ + globalCreatureDelay: number; }; export type PartialCavernContext = Partial & @@ -297,8 +304,9 @@ const STANDARD_DEFAULTS = { hallHasLandslidesChance: 0.8, caveLandslideCooldownRange: { min: 15, max: 120 }, hallLandslideCooldownRange: { min: 30, max: 150 }, - airSafetyFactor: 2, crystalGoalRatio: 0.2, + airSafetyFactor: 2, + globalCreatureDelay: 0, } as const satisfies Partial; const DEFAULTS_FOR_BIOME = { diff --git a/src/core/models/architect.ts b/src/core/models/architect.ts index e53157b..f03a164 100644 --- a/src/core/models/architect.ts +++ b/src/core/models/architect.ts @@ -30,6 +30,7 @@ import { AnchoredCavern, OrderedPlan, } from "../transformers/01_planning/03_anchor"; +import { ScriptHelper } from "../architects/utils/script"; export type BaseMetadata = { readonly tag: string } | undefined; @@ -143,18 +144,24 @@ export type BaseArchitect = { cavern: EnscribedCavern; plan: Plan; }): { priority: number; dz?: DiscoveryZone; pos?: Point }[]; - scriptGlobals?(args: { cavern: PreprogrammedCavern }): string | undefined; + scriptGlobals?(args: { + cavern: PreprogrammedCavern; + sh: ScriptHelper; + }): string | undefined; script?(args: { cavern: PreprogrammedCavern; plan: Plan; + sh: ScriptHelper; }): string | undefined; monsterSpawnScript?(args: { cavern: PreprogrammedCavern; plan: Plan; + sh: ScriptHelper; }): string | undefined; slugSpawnScript?(args: { cavern: PreprogrammedCavern; plan: Plan; + sh: ScriptHelper; }): string | undefined; }; diff --git a/src/core/transformers/04_ephemera/02_enscribe.ts b/src/core/transformers/04_ephemera/02_enscribe.ts index e14cc56..c23eaf2 100644 --- a/src/core/transformers/04_ephemera/02_enscribe.ts +++ b/src/core/transformers/04_ephemera/02_enscribe.ts @@ -52,8 +52,8 @@ export default function enscribe(cavern: AdjuredCavern): EnscribedCavern { { rock: "k", ice: "e", lava: "a" }[cavern.context.biome], hasOverrides ? "x" : "", ].join(""), - name.text.toLowerCase().replace(/[^a-z0-9]+/g, ""), - suffix?.toLowerCase().replace(/[^a-z0-9]+/g, ""), + name.text.replace(/[^A-Z0-9]+/g, "").toLowerCase(), + suffix?.replace(/[^A-Z0-9]+/g, "").toLowerCase(), ]).join("-"); })(); const levelName = hasOverrides ? `${name.text} (${suffix})` : name.text; diff --git a/src/core/transformers/04_ephemera/04_program.ts b/src/core/transformers/04_ephemera/04_program.ts index dd8a0c5..ecbbd2d 100644 --- a/src/core/transformers/04_ephemera/04_program.ts +++ b/src/core/transformers/04_ephemera/04_program.ts @@ -1,6 +1,8 @@ -import { filterTruthy } from "../../common/utils"; +import { creatureSpawnGlobals } from "../../architects/utils/creature_spawners"; +import { scriptFragment, ScriptHelper } from "../../architects/utils/script"; import { Architect } from "../../models/architect"; import { PreprogrammedCavern } from "./03_preprogram"; +import { objectiveGlobals } from "../../architects/utils/objectives"; export type ProgrammedCavern = PreprogrammedCavern & { readonly script: string; @@ -20,35 +22,36 @@ export default function program(cavern: PreprogrammedCavern): ProgrammedCavern { [], ), ); - const archGlobals = filterTruthy(globalsFns.map((fn) => fn({ cavern }))); - const archScripts = filterTruthy( - cavern.plans.map((plan) => plan.architect.script?.({ cavern, plan })), - ); - const monsters = cavern.context.hasMonsters - ? filterTruthy( - cavern.plans.map((plan) => - plan.architect.monsterSpawnScript?.({ cavern, plan }), - ), - ) - : []; - const slugs = cavern.context.hasSlugs - ? filterTruthy( - cavern.plans.map((plan) => - plan.architect.slugSpawnScript?.({ cavern, plan }), - ), - ) - : []; - const na = ["# n/a", ""]; - const script = [ - "#> Architect Globals", - ...(archGlobals.length ? archGlobals : na), + const na = "# n/a\n"; + const sh = new ScriptHelper(); + const script = scriptFragment( + "#> Globals", + objectiveGlobals({ cavern, sh }), + creatureSpawnGlobals({ cavern, sh }), + scriptFragment(...globalsFns.map((fn) => fn({ cavern, sh }))), "#> Architect Scripts", - ...(archScripts.length ? archScripts : na), + scriptFragment( + ...cavern.plans.map((plan) => + plan.architect.script?.({ cavern, plan, sh }), + ), + ) || na, "#> Spawn Monsters", - ...(monsters.length ? monsters : na), + cavern.context.hasMonsters + ? scriptFragment( + ...cavern.plans.map((plan) => + plan.architect.monsterSpawnScript?.({ cavern, plan, sh }), + ), + ) + : na, "#> Spawn Slugs", - ...(slugs.length ? slugs : na), - ].join("\n"); + cavern.context.hasSlugs + ? scriptFragment( + ...cavern.plans.map((plan) => + plan.architect.slugSpawnScript?.({ cavern, plan, sh }), + ), + ) + : na, + ); return { ...cavern, script }; } diff --git a/src/webui/components/context_editor/index.tsx b/src/webui/components/context_editor/index.tsx index e6d5c21..bcf1d88 100644 --- a/src/webui/components/context_editor/index.tsx +++ b/src/webui/components/context_editor/index.tsx @@ -356,6 +356,10 @@ function CavernContextInputInner({

Adjure

+
+

Enscribe

+ +
)}