diff --git a/src/core/architects/README.md b/src/core/architects/README.md index e874605..30949db 100644 --- a/src/core/architects/README.md +++ b/src/core/architects/README.md @@ -4,10 +4,10 @@ This directory contains the definitions of individual architects, which are resp ## Bidding -Architects must implement at least one of the `caveBid`, `hallBid`, or `spawnBid` methods, which return a number or a falsy value representing the "bid" to use this architect for a given plan. These methods must ensure the plan is suitable for the architect. Consider checking any of these which apply: +Architects must implement at least one of the `caveBid`, `hallBid`, or `anchorBid` methods, which return a number or a falsy value representing the "bid" to use this architect for a given plan. These methods must ensure the plan is suitable for the architect. Consider checking any of these which apply: - The fluid type, and the fluid types of any intersecting plans. -- The distance from spawn. +- The distance from the anchor. - The biome. - If the architect relies on the presence of monsters or slugs, that those are enabled. - Whether the architect has been used in the level already. @@ -36,7 +36,7 @@ These should perform the task listed in the name. ### Buildings -This step is nominally for adding buildings but may also be used to set up other miscellaneous fixed position items such as open cave flags or the camera position, since those tend to be linked with buildings. Note the camera position must only be set once, and it should be the spawn cave that does this. When buildings are placed, the architect should also set the building's foundation - it will not happen automatically. +This step is nominally for adding buildings but may also be used to set up other miscellaneous fixed position items such as open cave flags or the camera position, since those tend to be linked with buildings. Note the camera position must only be set once, and it should be the anchor cave that does this. When buildings are placed, the architect should also set the building's foundation - it will not happen automatically. ### Crystals & Ore @@ -70,9 +70,9 @@ This stage adds scripted events using Manic Miners' scripting language. If an architect will perform some "cinematic" (i.e. a pan + message) immediately after a zone is discovered, it must claim that zone in the pre-program stage. This is to avoid a situation where two different plans try to trigger events at the same time, causing undesired behavior. If the needed zone is not assigned to the plan ID, it must not perform the cinematic. This does not prevent non-blocking events such as monster spawns or scheduling some event to occur after a random delay. -### The Spawn Cave is Special +### The Anchor Cave is Special -Generally speaking, to avoid any conflicting logic or strange behaviors, the spawn cave's script should be solely responsible for manipulating any global state. If there is some special logic that occurs when a specific spawn architect is used in combination with another architect, that logic should be owned by the spawn. For example, when a "Find HQ" is present the Nomads Spawn disables buildings until the HQ is found. This logic exists entirely within the Nomads Spawn. +Generally speaking, to avoid any conflicting logic or strange behaviors, the anchor cave's script should be solely responsible for manipulating any global state. If there is some special logic that occurs when a specific anchor architect is used in combination with another architect, that logic should be owned by the anchor. For example, when a "Find HQ" is present the Nomads Spawn disables buildings until the HQ is found. This logic exists entirely within the Nomads Spawn. ### Script Globals diff --git a/src/core/architects/collapse.ts b/src/core/architects/collapse.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/core/architects/established_hq.ts b/src/core/architects/established_hq/base.ts similarity index 56% rename from src/core/architects/established_hq.ts rename to src/core/architects/established_hq/base.ts index f2ac5fb..c87f011 100644 --- a/src/core/architects/established_hq.ts +++ b/src/core/architects/established_hq/base.ts @@ -1,6 +1,6 @@ import Delaunator from "delaunator"; -import { Point, plotLine } from "../common/geometry"; -import { Architect } from "../models/architect"; +import { Point, plotLine } from "../../common/geometry"; +import { Architect } from "../../models/architect"; import { Building, CANTEEN, @@ -12,48 +12,27 @@ import { TELEPORT_PAD, TOOL_STORE, UPGRADE_STATION, -} from "../models/building"; -import { Tile } from "../models/tiles"; -import { DefaultCaveArchitect, PartialArchitect } from "./default"; -import { MakeBuildingFn, getBuildings } from "./utils/buildings"; -import { mkRough, Rough } from "./utils/rough"; -import { position } from "../models/position"; -import { getPlaceRechargeSeams, sprinkleCrystals } from "./utils/resources"; -import { placeLandslides } from "./utils/hazards"; -import { - DzPriorities, - escapeString, - eventChain, - mkVars, - scriptFragment, - transformPoint, -} from "./utils/script"; -import { getDiscoveryPoint } from "./utils/discovery"; -import { sprinkleSlugHoles } from "./utils/creatures"; -import { slugSpawnScript } from "./utils/creature_spawners"; - -const DESTROY_PATH_CHANCE = 0.62; - -const T0_BUILDINGS = [TOOL_STORE] as const; -const T1_BUILDINGS = [TELEPORT_PAD, POWER_STATION, SUPPORT_STATION] as const; -const T2_BUILDINGS = [UPGRADE_STATION, GEOLOGICAL_CENTER, DOCKS] as const; -const T3_BUILDINGS = [ - CANTEEN, - MINING_LASER, - MINING_LASER, - MINING_LASER, -] as const; - -const OMIT_T1 = T0_BUILDINGS.length; -const MAX_HOPS = 3; +} from "../../models/building"; +import { position } from "../../models/position"; +import { Tile } from "../../models/tiles"; +import { MakeBuildingFn, getBuildings } from "../utils/buildings"; +import { getPlaceRechargeSeams, sprinkleCrystals } from "../utils/resources"; +import { PseudorandomStream } from "../../common"; +import { PartialArchitect, DefaultCaveArchitect } from "../default"; +import { slugSpawnScript } from "../utils/creature_spawners"; +import { sprinkleSlugHoles } from "../utils/creatures"; +import { mkRough, Rough } from "../utils/rough"; export type HqMetadata = { readonly tag: "hq"; readonly ruin: boolean; + readonly fixedComplete: boolean; readonly crystalsInBuildings: number; }; -function getPrime( +const DESTROY_PATH_CHANCE = 0.62; + +export function getPrime( maxCrystals: number, ruin: boolean, ): Architect["prime"] { @@ -65,13 +44,44 @@ function getPrime( min: 3, max: maxCrystals, }); - return { crystalsInBuildings, ruin, tag: "hq" }; + return { crystalsInBuildings, ruin, fixedComplete: false, tag: "hq" }; }; } -function getPlaceBuildings({ +const T0_BUILDINGS = [TOOL_STORE] as const; +const T1_BUILDINGS = [TELEPORT_PAD, POWER_STATION, SUPPORT_STATION] as const; +const T2_BUILDINGS = [UPGRADE_STATION, GEOLOGICAL_CENTER, DOCKS] as const; +const T3_BUILDINGS = [ + CANTEEN, + MINING_LASER, + MINING_LASER, + MINING_LASER, +] as const; + +function getDefaultTemplates( + rng: PseudorandomStream, + asSpawn: boolean, + asRuin: boolean, +) { + return [ + ...T0_BUILDINGS, + ...(asSpawn && !asRuin ? T1_BUILDINGS : rng.shuffle(T1_BUILDINGS)), + ...rng.shuffle(T2_BUILDINGS), + ...rng.shuffle(T3_BUILDINGS), + ]; +} + +// Here be spaghetti +export function getPlaceBuildings({ discovered = false, from = 2, + templates, + omit, +}: { + discovered?: boolean; + from?: number; + templates?: (rng: PseudorandomStream) => readonly Building["template"][]; + omit?: (bt: Building["template"], i: number) => boolean; }): Architect["placeBuildings"] { return (args) => { const asRuin = args.plan.metadata.ruin; @@ -79,12 +89,9 @@ function getPlaceBuildings({ // Determine the order templates will be applied. const rng = args.cavern.dice.placeBuildings(args.plan.id); - const tq = [ - ...T0_BUILDINGS, - ...(asSpawn && !asRuin ? T1_BUILDINGS : rng.shuffle(T1_BUILDINGS)), - ...rng.shuffle(T2_BUILDINGS), - ...rng.shuffle(T3_BUILDINGS), - ]; + const tq = templates + ? templates(rng) + : getDefaultTemplates(rng, asSpawn, asRuin); // Choose which buildings will be created based on total crystal budget. let crystalBudget = args.plan.metadata.crystalsInBuildings; @@ -105,7 +112,9 @@ function getPlaceBuildings({ ) { return false; } - if (asRuin && i === OMIT_T1) { + if ( + omit ? omit(bt, i) : !templates && asRuin && i === T0_BUILDINGS.length + ) { return false; } return true; @@ -117,7 +126,7 @@ function getPlaceBuildings({ return true; } } else if (asRuin) { - bq.push((pos) => ({ ...bt.atTile(pos), isRuinAtSpawn: true })); + bq.push((pos) => ({ ...bt.atTile(pos), placeRubbleInstead: true })); } return false; }); @@ -133,7 +142,7 @@ function getPlaceBuildings({ for (let i = 0; i < buildings.length; i++) { const building = buildings[i]; let fTile: Tile; - if ("isRuinAtSpawn" in building) { + if ("placeRubbleInstead" in building) { fTile = Tile.RUBBLE_4; } else { fTile = Tile.FOUNDATION; @@ -224,68 +233,13 @@ function getPlaceBuildings({ } return { - buildings: buildings.filter((b) => !("isRuinAtSpawn" in b)), + buildings: buildings.filter((b) => !("placeRubbleInstead" in b)), cameraPosition, }; }; } -export const gLostHq = mkVars("gLostHq", ["foundHq"]); - -const WITH_FIND_OBJECTIVE: Pick< - Architect, - "objectives" | "claimEventOnDiscover" | "scriptGlobals" | "script" -> = { - objectives: () => ({ - variables: [ - { - condition: `${gLostHq.foundHq}>0`, - description: "Find the lost Rock Raider HQ", - }, - ], - sufficient: false, - }), - claimEventOnDiscover({ cavern, plan }) { - const pos = getDiscoveryPoint(cavern, plan); - if (!pos) { - throw new Error("Cave has Find HQ objective but no undiscovered point."); - } - return [{ pos, priority: DzPriorities.OBJECTIVE }]; - }, - scriptGlobals: () => - scriptFragment("# Globals: Lost HQ", `int ${gLostHq.foundHq}=0`), - script({ cavern, plan }) { - const discoPoint = getDiscoveryPoint(cavern, plan)!; - const shouldPanMessage = - cavern.ownsScriptOnDiscover[ - cavern.discoveryZones.get(...discoPoint)!.id - ] === plan.id; - - const camPoint = plan.path.baseplates.reduce((r, p) => { - return r.pearlRadius > p.pearlRadius ? r : p; - }).center; - - const v = mkVars(`p${plan.id}LostHq`, ["messageDiscover", "onDiscover"]); - const message = shouldPanMessage - ? cavern.lore.foundHq(cavern.dice).text - : "undefined"; - - return scriptFragment( - `# P${plan.id}: Lost HQ`, - `string ${v.messageDiscover}="${escapeString(message)}"`, - `if(change:${transformPoint(cavern, discoPoint)})[${v.onDiscover}]`, - eventChain( - v.onDiscover, - shouldPanMessage && `msg:${v.messageDiscover};`, - shouldPanMessage && `pan:${transformPoint(cavern, camPoint)};`, - `wait:1;`, - `${gLostHq.foundHq}=1;`, - ), - ); - }, -}; - -const BASE: Omit, "prime"> & +export const BASE: Omit, "prime"> & Pick, "rough" | "roughExtent"> = { ...DefaultCaveArchitect, ...mkRough( @@ -311,54 +265,3 @@ const BASE: Omit, "prime"> & }, maxSlope: 15, }; - -export const ESTABLISHED_HQ = [ - { - name: "Established HQ Spawn", - ...BASE, - prime: getPrime(10, false), - placeBuildings: getPlaceBuildings({ discovered: true }), - spawnBid: ({ plan }) => !plan.fluid && plan.pearlRadius > 5 && 0.5, - }, - { - name: "Ruined HQ Spawn", - ...BASE, - prime: getPrime(12, true), - placeBuildings: getPlaceBuildings({ - discovered: true, - from: 3, - }), - placeLandslides: (args) => placeLandslides({ min: 15, max: 60 }, args), - spawnBid: ({ plan }) => !plan.fluid && plan.pearlRadius > 6 && 0.5, - }, - { - name: "Find Established HQ", - ...BASE, - prime: getPrime(15, false), - placeBuildings: getPlaceBuildings({}), - caveBid: ({ plan, hops, plans }) => - !plan.fluid && - plan.pearlRadius > 5 && - hops.length <= MAX_HOPS && - !hops.some((id) => plans[id].fluid) && - !plans.some((p) => p.metadata?.tag === "hq") && - 0.5, - ...WITH_FIND_OBJECTIVE, - }, - { - name: "Find Ruined HQ", - ...BASE, - prime: getPrime(15, true), - placeBuildings: getPlaceBuildings({ from: 3 }), - placeLandslides: (args) => placeLandslides({ min: 15, max: 100 }, args), - caveBid: ({ plan, hops, plans }) => - !plan.fluid && - plan.pearlRadius > 6 && - hops.length <= MAX_HOPS && - !plans.some((p) => p.metadata?.tag === "hq") && - (plans[hops[0]].metadata?.tag === "nomads" ? 5 : 0.5), - ...WITH_FIND_OBJECTIVE, - }, -] as const satisfies readonly Architect[]; - -export default ESTABLISHED_HQ; diff --git a/src/core/architects/established_hq/fixed_complete.ts b/src/core/architects/established_hq/fixed_complete.ts new file mode 100644 index 0000000..0e17ecb --- /dev/null +++ b/src/core/architects/established_hq/fixed_complete.ts @@ -0,0 +1,101 @@ +import { inferContextDefaults } from "../../common"; +import { Architect } from "../../models/architect"; +import { + TOOL_STORE, + TELEPORT_PAD, + POWER_STATION, + SUPPORT_STATION, + DOCKS, + SUPER_TELEPORT, + UPGRADE_STATION, + GEOLOGICAL_CENTER, + ALL_BUILDINGS, +} from "../../models/building"; +import { + escapeString, + eventChain, + mkVars, + scriptFragment, +} from "../utils/script"; +import { BASE, HqMetadata, getPlaceBuildings } from "./base"; + +const T0_BUILDINGS = [ + TOOL_STORE, + TELEPORT_PAD, + POWER_STATION, + SUPPORT_STATION, + DOCKS, + TOOL_STORE, + SUPER_TELEPORT, + UPGRADE_STATION, + GEOLOGICAL_CENTER, + SUPPORT_STATION, +] as const; + +const T0_CRYSTALS = T0_BUILDINGS.reduce((r, bt) => r + bt.crystals, 0); + +const gFixedCompleteHq = mkVars("gFCHQ", [ + "onInit", + "onBaseDestroyed", + "msgBaseDestroyed", + "wasBaseDestroyed", +]); + +export const FC_BASE: Pick< + Architect, + "mod" | "prime" | "placeBuildings" | "scriptGlobals" +> = { + mod: (cavern) => { + const context = inferContextDefaults({ + crystalGoalRatio: 0.3, + ...cavern.initialContext, + }); + return { ...cavern, context }; + }, + prime: () => ({ + crystalsInBuildings: T0_CRYSTALS, + ruin: false, + fixedComplete: true, + tag: "hq", + }), + placeBuildings: getPlaceBuildings({ + discovered: true, + templates: () => T0_BUILDINGS, + }), + scriptGlobals: ({ cavern }) => { + return scriptFragment( + `# Globals: Fixed Complete HQ`, + `if(time:0)[${gFixedCompleteHq.onInit}]`, + eventChain( + gFixedCompleteHq.onInit, + // Can't just disable buildings because that disables fences - and + // nobody wants that. + ...ALL_BUILDINGS.map((bt) => `disable:${bt.id};` as `${string};`), + ), + `string ${gFixedCompleteHq.msgBaseDestroyed}="${escapeString(cavern.lore.generateFailureBaseDestroyed(cavern.dice).text)}"`, + `int ${gFixedCompleteHq.wasBaseDestroyed}=0`, + `if(${TOOL_STORE.id}<=0)[${gFixedCompleteHq.onBaseDestroyed}]`, + `if(${POWER_STATION.id}<=0)[${gFixedCompleteHq.onBaseDestroyed}]`, + `if(${SUPPORT_STATION.id}<=0)[${gFixedCompleteHq.onBaseDestroyed}]`, + eventChain( + gFixedCompleteHq.onBaseDestroyed, + `((${gFixedCompleteHq.wasBaseDestroyed}>0))return;`, + `${gFixedCompleteHq.wasBaseDestroyed}=1;`, + `msg:${gFixedCompleteHq.msgBaseDestroyed};`, + `wait:5;`, + `lose;`, + ), + ); + }, +}; + +const FIXED_COMPLETE = [ + { + name: "Hq.FixedComplete", + ...BASE, + ...FC_BASE, + anchorBid: ({ plan }) => !plan.fluid && plan.pearlRadius > 6 && 0.1, + }, +] as const satisfies readonly Architect[]; + +export default FIXED_COMPLETE; diff --git a/src/core/architects/established_hq/index.ts b/src/core/architects/established_hq/index.ts new file mode 100644 index 0000000..8c5e42d --- /dev/null +++ b/src/core/architects/established_hq/index.ts @@ -0,0 +1,35 @@ +import { Architect } from "../../models/architect"; +import { placeLandslides } from "../utils/hazards"; +import { HqMetadata } from "./base"; +import { getPlaceBuildings } from "./base"; +import { getPrime } from "./base"; +import { BASE } from "./base"; +import FIXED_COMPLETE from "./fixed_complete"; +import ISLAND from "./island"; +import LOST from "./lost"; + +export const ESTABLISHED_HQ = [ + { + name: "Hq.Spawn.Established", + ...BASE, + prime: getPrime(10, false), + placeBuildings: getPlaceBuildings({ discovered: true }), + anchorBid: ({ plan }) => !plan.fluid && plan.pearlRadius > 5 && 0.5, + }, + { + name: "Hq.Spawn.Ruins", + ...BASE, + prime: getPrime(12, true), + placeBuildings: getPlaceBuildings({ + discovered: true, + from: 3, + }), + placeLandslides: (args) => placeLandslides({ min: 15, max: 60 }, args), + anchorBid: ({ plan }) => !plan.fluid && plan.pearlRadius > 6 && 0.5, + }, + ...FIXED_COMPLETE, + ...ISLAND, + ...LOST, +] as const satisfies readonly Architect[]; + +export default ESTABLISHED_HQ; diff --git a/src/core/architects/established_hq/island.ts b/src/core/architects/established_hq/island.ts new file mode 100644 index 0000000..b04e406 --- /dev/null +++ b/src/core/architects/established_hq/island.ts @@ -0,0 +1,131 @@ +import { Point } from "../../common/geometry"; +import { Grid } from "../../common/grid"; +import { Architect } from "../../models/architect"; +import { + TOOL_STORE, + POWER_STATION, + SUPPORT_STATION, + DOCKS, + GEOLOGICAL_CENTER, + MINING_LASER, +} from "../../models/building"; +import { Plan } from "../../models/plan"; +import { randomlyInTile } from "../../models/position"; +import { Tile } from "../../models/tiles"; +import { RAPID_RIDER } from "../../models/vehicle"; +import { sprinkleCrystals } from "../utils/resources"; +import { Rough, mkRough } from "../utils/rough"; +import { scriptFragment, transformPoint } from "../utils/script"; +import { BASE, HqMetadata, getPlaceBuildings } from "./base"; + +const T0_BUILDINGS = [TOOL_STORE, DOCKS, POWER_STATION] as const; + +const T0_CRYSTALS = T0_BUILDINGS.reduce((r, bt) => r + bt.crystals, 0); + +const T1_BUILDINGS = [ + MINING_LASER, + MINING_LASER, + SUPPORT_STATION, + GEOLOGICAL_CENTER, +] as const; + +const T1_CRYSTALS = T1_BUILDINGS.reduce((r, bt) => r + bt.crystals, 0); + +function findWaterTile(plan: Plan, tiles: Grid): Point { + for (let ly = 0; ly < plan.innerPearl.length; ly++) { + const layer = plan.innerPearl[ly]; + for (let i = 0; i < layer.length; i++) { + if (tiles.get(...layer[i]) === Tile.WATER) { + return layer[i]; + } + } + } + throw new Error("Failed to find water tile"); +} + +export const ISLAND_BASE: Pick< + Architect, + | "crystalsFromMetadata" + | "prime" + | "placeBuildings" + | "placeCrystals" + | "placeEntities" + | "script" +> = { + crystalsFromMetadata: (metadata) => + RAPID_RIDER.crystals + metadata.crystalsInBuildings, + prime: ({ cavern, plan }) => { + const rng = cavern.dice.prime(plan.id); + const crystalsInBuildings = rng.betaInt({ + a: 1, + b: 1.75, + min: T0_CRYSTALS, + max: T0_CRYSTALS + T1_CRYSTALS + 1, + }); + return { + crystalsInBuildings, + ruin: false, + fixedComplete: false, + tag: "hq", + }; + }, + placeBuildings: getPlaceBuildings({ + discovered: true, + from: 1, + templates: (rng) => [...T0_BUILDINGS, ...rng.shuffle(T1_BUILDINGS)], + }), + placeCrystals: (args) => sprinkleCrystals(args, { seamBias: 1 }), + placeEntities: ({ cavern, plan, minerFactory, vehicleFactory }) => { + const rng = cavern.dice.placeEntities(plan.id); + const [x, y] = findWaterTile(plan, cavern.tiles); + const pos = randomlyInTile({ rng, x, y }); + const miner = minerFactory.create({ + ...pos, + planId: plan.id, + loadout: ["Drill", "JobSailor"], + }); + const vehicles = [ + vehicleFactory.create({ + ...pos, + planId: plan.id, + driverId: miner.id, + template: RAPID_RIDER, + upgrades: ["UpAddDrill"], + }), + ]; + return { miners: [miner], vehicles }; + }, + script: ({ cavern, plan }) => { + return scriptFragment( + ...plan.innerPearl + .flatMap((ly) => ly) + .filter((pos) => cavern.tiles.get(...pos)?.isWall) + .map((pos) => { + const tp = transformPoint(cavern, pos); + return `if(change:${tp})[place:${tp},${Tile.WATER.id}]`; + }), + ); + }, +}; + +const ISLAND = [ + { + name: "Hq.Island", + ...BASE, + ...ISLAND_BASE, + ...mkRough( + { of: Rough.ALWAYS_FLOOR, width: 3 }, + { of: Rough.FLOOR, width: 0, grow: 1 }, + { of: Rough.WATER, width: 2, grow: 2 }, + { of: Rough.MIX_FRINGE }, + ), + anchorBid: ({ plan }) => + plan.fluid === Tile.WATER && + plan.lakeSize > 3 && + plan.path.baseplates.length === 1 && + plan.pearlRadius > 5 && + 0.2, + }, +] satisfies Architect[]; + +export default ISLAND; diff --git a/src/core/architects/established_hq/lost.ts b/src/core/architects/established_hq/lost.ts new file mode 100644 index 0000000..ba6de85 --- /dev/null +++ b/src/core/architects/established_hq/lost.ts @@ -0,0 +1,102 @@ +import { Architect } from "../../models/architect"; +import { getDiscoveryPoint } from "../utils/discovery"; +import { placeLandslides } from "../utils/hazards"; +import { + DzPriorities, + scriptFragment, + mkVars, + escapeString, + transformPoint, + eventChain, +} from "../utils/script"; +import { BASE, HqMetadata, getPlaceBuildings, getPrime } from "./base"; + +const MAX_HOPS = 3; + +export const gLostHq = mkVars("gLostHq", ["foundHq"]); + +const LOST_BASE: Pick< + Architect, + "objectives" | "claimEventOnDiscover" | "scriptGlobals" | "script" +> = { + objectives: () => ({ + variables: [ + { + condition: `${gLostHq.foundHq}>0`, + description: "Find the lost Rock Raider HQ", + }, + ], + sufficient: false, + }), + claimEventOnDiscover({ cavern, plan }) { + const pos = getDiscoveryPoint(cavern, plan); + if (!pos) { + throw new Error("Cave has Find HQ objective but no undiscovered point."); + } + return [{ pos, priority: DzPriorities.OBJECTIVE }]; + }, + scriptGlobals: () => + scriptFragment("# Globals: Lost HQ", `int ${gLostHq.foundHq}=0`), + script({ cavern, plan }) { + const discoPoint = getDiscoveryPoint(cavern, plan)!; + const shouldPanMessage = + cavern.ownsScriptOnDiscover[ + cavern.discoveryZones.get(...discoPoint)!.id + ] === plan.id; + + const camPoint = plan.path.baseplates.reduce((r, p) => { + return r.pearlRadius > p.pearlRadius ? r : p; + }).center; + + const v = mkVars(`p${plan.id}LostHq`, ["messageDiscover", "onDiscover"]); + const message = shouldPanMessage + ? cavern.lore.foundHq(cavern.dice).text + : "undefined"; + + return scriptFragment( + `# P${plan.id}: Lost HQ`, + `string ${v.messageDiscover}="${escapeString(message)}"`, + `if(change:${transformPoint(cavern, discoPoint)})[${v.onDiscover}]`, + eventChain( + v.onDiscover, + shouldPanMessage && `msg:${v.messageDiscover};`, + shouldPanMessage && `pan:${transformPoint(cavern, camPoint)};`, + `wait:1;`, + `${gLostHq.foundHq}=1;`, + ), + ); + }, +}; + +const LOST = [ + { + name: "Hq.Lost.Established", + ...BASE, + ...LOST_BASE, + prime: getPrime(15, false), + placeBuildings: getPlaceBuildings({}), + caveBid: ({ plan, hops, plans }) => + !plan.fluid && + plan.pearlRadius > 5 && + hops.length <= MAX_HOPS && + !hops.some((id) => plans[id].fluid) && + !plans.some((p) => p.metadata?.tag === "hq") && + 0.5, + }, + { + name: "Hq.Lost.Ruins", + ...BASE, + ...LOST_BASE, + prime: getPrime(15, true), + placeBuildings: getPlaceBuildings({ from: 3 }), + placeLandslides: (args) => placeLandslides({ min: 15, max: 100 }, args), + caveBid: ({ plan, hops, plans }) => + !plan.fluid && + plan.pearlRadius > 6 && + hops.length <= MAX_HOPS && + !plans.some((p) => p.metadata?.tag === "hq") && + (plans[hops[0]].metadata?.tag === "nomads" ? 5 : 0.5), + }, +] as const satisfies readonly Architect[]; + +export default LOST; diff --git a/src/core/architects/fissure.ts b/src/core/architects/fissure.ts index 04fce88..c9ce7f7 100644 --- a/src/core/architects/fissure.ts +++ b/src/core/architects/fissure.ts @@ -103,7 +103,7 @@ const BASE: PartialArchitect = { const FISSURE = [ { - name: "Fissure Hall", + name: "Fissure", ...BASE, ...mkRough({ of: Rough.SOLID_ROCK }, { of: Rough.VOID, grow: 1 }), hallBid: ({ plan, plans }) => diff --git a/src/core/architects/flooded.ts b/src/core/architects/flooded.ts index 8bae99d..f6c2fdd 100644 --- a/src/core/architects/flooded.ts +++ b/src/core/architects/flooded.ts @@ -23,7 +23,7 @@ const BASE: PartialArchitect = { const FLOODED = [ { - name: "Lake", + name: "Flooded.Water.Lake", ...BASE, ...mkRough( { of: Rough.WATER, grow: 2 }, @@ -40,7 +40,7 @@ const FLOODED = [ plan.fluid === Tile.WATER && plan.pearlRadius < 10 && 1, }, { - name: "Lake With Sleeping Monsters", + name: "Flooded.Water.LakeWithMonsters", ...BASE, ...mkRough( { of: Rough.WATER, grow: 2 }, @@ -63,7 +63,7 @@ const FLOODED = [ }, }, { - name: "Island", + name: "Flooded.Water.Island", ...BASE, ...mkRough( { of: Rough.ALWAYS_SOLID_ROCK, width: 0, grow: 0.7 }, @@ -80,7 +80,7 @@ const FLOODED = [ plan.fluid === Tile.WATER && plan.pearlRadius > 5 && 2, }, { - name: "Lava Lake", + name: "Flooded.Lava.Lake", ...BASE, ...mkRough( { of: Rough.LAVA, grow: 2 }, @@ -92,7 +92,7 @@ const FLOODED = [ plan.fluid === Tile.LAVA && plan.pearlRadius < 10 && 1, }, { - name: "Lava Island", + name: "Flooded.Lava.Island", ...BASE, ...mkRough( { of: Rough.ALWAYS_SOLID_ROCK, width: 0, grow: 0.7 }, @@ -108,7 +108,7 @@ const FLOODED = [ plan.fluid === Tile.LAVA && plan.pearlRadius > 5 && 1, }, { - name: "Peninsula", + name: "Flooded.Water.Peninsula", ...BASE, ...mkRough( { of: Rough.ALWAYS_SOLID_ROCK, width: 0, grow: 0.7 }, @@ -128,7 +128,7 @@ const FLOODED = [ 1, }, { - name: "Lava Peninsula", + name: "Flooded.Lava.Peninsula", ...BASE, ...mkRough( { of: Rough.ALWAYS_SOLID_ROCK, width: 0, grow: 0.7 }, @@ -146,7 +146,7 @@ const FLOODED = [ 1, }, { - name: "Lava Stalagmite Cave", + name: "Flooded.Lava.Stalagmites", ...BASE, crystalsToPlace: ({ plan }) => plan.crystalRichness * plan.perimeter * 2, ...mkRough( diff --git a/src/core/architects/index.ts b/src/core/architects/index.ts index de92782..cfe9e48 100644 --- a/src/core/architects/index.ts +++ b/src/core/architects/index.ts @@ -1,5 +1,6 @@ import { Architect } from "../models/architect"; -import ESTABLISHED_HQ, { HqMetadata } from "./established_hq"; +import ESTABLISHED_HQ from "./established_hq"; +import { HqMetadata } from "./established_hq/base"; import FISSURE from "./fissure"; import FLOODED from "./flooded"; import LOOPBACK from "./loopback"; diff --git a/src/core/architects/loopback.ts b/src/core/architects/loopback.ts index 802c478..921081d 100644 --- a/src/core/architects/loopback.ts +++ b/src/core/architects/loopback.ts @@ -72,7 +72,7 @@ function withBarrier({ const LOOPBACK = [ { - name: "Loopback Hall", + name: "Loopback", ...BASE, ...withBarrier( mkRough( diff --git a/src/core/architects/lost_miners.ts b/src/core/architects/lost_miners.ts index 5a86529..4259280 100644 --- a/src/core/architects/lost_miners.ts +++ b/src/core/architects/lost_miners.ts @@ -325,7 +325,7 @@ const MULTIPLIERS = { rock: 1.0, ice: 1.4, lava: 0.7 } as const; const LOST_MINERS = [ { - name: "Lost Miners", + name: "LostMiners", ...BASE, ...mkRough( { of: Rough.ALWAYS_FLOOR, width: 2, grow: 2 }, diff --git a/src/core/architects/nomads.ts b/src/core/architects/nomads.ts index c1cc1ab..4457f0c 100644 --- a/src/core/architects/nomads.ts +++ b/src/core/architects/nomads.ts @@ -12,7 +12,7 @@ import { scriptFragment, } from "./utils/script"; import { SUPPORT_STATION } from "../models/building"; -import { Tile } from "../models/tiles"; +import { Hardness, Tile } from "../models/tiles"; import { VehicleTemplate, HOVER_SCOUT, @@ -26,7 +26,7 @@ import { import { Loadout, Miner } from "../models/miner"; import { filterTruthy, pairEach } from "../common/utils"; import { plotLine } from "../common/geometry"; -import { gLostHq } from "./established_hq"; +import { gLostHq } from "./established_hq/lost"; export type NomadsMetadata = { readonly tag: "nomads"; @@ -76,7 +76,10 @@ const BASE: PartialArchitect = { ?.hops.forEach((hopId) => { pairEach(cavern.plans[hopId].path.baseplates, (a, b) => { for (const pos of plotLine(a.center, b.center)) { - if (tiles.get(...pos) === Tile.HARD_ROCK) { + if ( + tiles.get(...pos)?.hardness ?? + Hardness.SOLID >= Hardness.HARD + ) { tiles.set(...pos, Tile.LOOSE_ROCK); } } @@ -185,7 +188,7 @@ const BASE: PartialArchitect = { const NOMAD_SPAWN = [ { - name: "Nomad Spawn", + name: "Nomads", ...BASE, ...mkRough( { of: Rough.ALWAYS_FLOOR, width: 2, grow: 2 }, @@ -198,14 +201,14 @@ const NOMAD_SPAWN = [ placeOre: (args) => { return sprinkleOre(args, { seamBias: 1 }); }, - spawnBid: ({ cavern, plan }) => + anchorBid: ({ cavern, plan }) => !plan.fluid && plan.pearlRadius > 0 && intersectsOnly(cavern.plans, plan, null) && (isDeadEnd(plan) ? 1 : 0.1), }, { - name: "Nomad Spawn Peninsula", + name: "Nomads.WaterPeninsula", ...BASE, ...mkRough( { of: Rough.ALWAYS_FLOOR, grow: 2 }, @@ -215,14 +218,14 @@ const NOMAD_SPAWN = [ { of: Rough.MIX_FRINGE }, ), prime: () => ({ tag: "nomads", minersCount: 1, vehicles: [RAPID_RIDER] }), - spawnBid: ({ cavern, plan }) => + anchorBid: ({ cavern, plan }) => plan.fluid === Tile.WATER && plan.pearlRadius > 4 && intersectsAny(cavern.plans, plan, null) && 0.5, }, { - name: "Nomad Spawn Lava Peninsula", + name: "Nomads.LavaPeninsula", ...BASE, ...mkRough( { of: Rough.ALWAYS_FLOOR, grow: 2 }, @@ -232,7 +235,7 @@ const NOMAD_SPAWN = [ { of: Rough.AT_MOST_HARD_ROCK }, ), prime: () => ({ tag: "nomads", minersCount: 1, vehicles: [TUNNEL_SCOUT] }), - spawnBid: ({ cavern, plan }) => + anchorBid: ({ cavern, plan }) => plan.fluid === Tile.LAVA && plan.pearlRadius > 4 && intersectsAny(cavern.plans, plan, null) && diff --git a/src/core/architects/simple_cave.ts b/src/core/architects/simple_cave.ts index 1903145..8d5bade 100644 --- a/src/core/architects/simple_cave.ts +++ b/src/core/architects/simple_cave.ts @@ -11,7 +11,7 @@ const BASE: PartialArchitect = { const SIMPLE_CAVE = [ { - name: "Filled Cave", + name: "SimpleCave.Filled", ...BASE, ...mkRough( { of: Rough.DIRT, width: 0, grow: 0.25 }, @@ -28,7 +28,7 @@ const SIMPLE_CAVE = [ caveBid: ({ plan }) => !plan.fluid && plan.pearlRadius < 4 && 0.04, }, { - name: "Open Cave", + name: "SimpleCave.Open", ...BASE, ...mkRough( { of: Rough.FLOOR, grow: 2 }, @@ -50,7 +50,7 @@ const SIMPLE_CAVE = [ 2, }, { - name: "Empty Cave", + name: "SimpleCave.Empty", ...BASE, ...mkRough( { of: Rough.FLOOR, grow: 2 }, @@ -68,7 +68,7 @@ const SIMPLE_CAVE = [ caveBid: ({ plan }) => !plan.fluid && plan.pearlRadius < 10 && 1, }, { - name: "Filled Cave with Paths", + name: "SimpleCave.FilledWithPaths", ...BASE, ...mkRough( { of: Rough.FLOOR, width: 0, grow: 0.5 }, @@ -86,7 +86,7 @@ const SIMPLE_CAVE = [ 1, }, { - name: "Doughnut Cave", + name: "SimpleCave.Doughnut", ...BASE, ...mkRough( { of: Rough.ALWAYS_SOLID_ROCK, grow: 0.2 }, @@ -100,7 +100,7 @@ const SIMPLE_CAVE = [ caveBid: ({ plan }) => !plan.fluid && plan.pearlRadius > 5 && 0.5, }, { - name: "Stalagmite Cave", + name: "SimpleCave.Stalagmites", ...BASE, ...mkRough( { diff --git a/src/core/architects/simple_hall.ts b/src/core/architects/simple_hall.ts index 9f2a5d9..d33143e 100644 --- a/src/core/architects/simple_hall.ts +++ b/src/core/architects/simple_hall.ts @@ -12,7 +12,7 @@ const BASE: PartialArchitect = { const SIMPLE_HALL = [ { - name: "Open Hall", + name: "SimpleHall.Open", ...BASE, ...mkRough( { of: Rough.FLOOR, grow: 2 }, @@ -23,7 +23,7 @@ const SIMPLE_HALL = [ hallBid: ({ plan }) => !plan.fluid && plan.pearlRadius > 0 && 1, }, { - name: "Wide Hall With Monsters", + name: "SimpleHall.WideWithMonsters", ...BASE, ...mkRough( { of: Rough.FLOOR, grow: 1 }, @@ -43,7 +43,7 @@ const SIMPLE_HALL = [ }, }, { - name: "Filled Hall", + name: "SimpleHall.Filled", ...BASE, ...mkRough( { @@ -59,7 +59,7 @@ const SIMPLE_HALL = [ hallBid: ({ plan }) => !plan.fluid && plan.pearlRadius > 0 && 1, }, { - name: "River", + name: "SimpleHall.Water.River", ...BASE, crystalsToPlace: ({ plan }) => 3 * plan.crystalRichness * plan.perimeter, ...mkRough( @@ -80,7 +80,7 @@ const SIMPLE_HALL = [ hallBid: ({ plan }) => plan.fluid === Tile.WATER && 1, }, { - name: "Stream", + name: "SimpleHall.Water.Stream", ...BASE, ...mkRough( { of: Rough.WATER, grow: 0.5 }, @@ -92,7 +92,7 @@ const SIMPLE_HALL = [ plan.fluid === Tile.WATER && intersectsOnly(plans, plan, Tile.WATER) && 1, }, { - name: "Lava River", + name: "SimpleHall.Lava.River", ...BASE, ...mkRough( { of: Rough.LAVA, width: 2, grow: 1 }, @@ -102,7 +102,7 @@ const SIMPLE_HALL = [ hallBid: ({ plan }) => plan.fluid === Tile.LAVA && 1, }, { - name: "Wide Lava River with Monsters", + name: "SimpleHall.Lava.WideWithMonsters", ...BASE, ...mkRough( { of: Rough.LAVA, width: 2, grow: 1 }, diff --git a/src/core/architects/simple_spawn.ts b/src/core/architects/simple_spawn.ts index 04b2b51..236a9a2 100644 --- a/src/core/architects/simple_spawn.ts +++ b/src/core/architects/simple_spawn.ts @@ -4,7 +4,6 @@ import { Tile } from "../models/tiles"; import { DefaultCaveArchitect, PartialArchitect } from "./default"; import { mkRough, Rough } from "./utils/rough"; import { getBuildings } from "./utils/buildings"; -import { intersectsOnly } from "./utils/intersects"; import { getPlaceRechargeSeams } from "./utils/resources"; import { position } from "../models/position"; import { sprinkleSlugHoles } from "./utils/creatures"; @@ -61,38 +60,31 @@ const OPEN = mkRough( const SIMPLE_SPAWN = [ { - name: "Open Spawn", + name: "SimpleSpawn.Open", ...BASE, ...OPEN, - spawnBid: ({ cavern, plan }) => - !plan.fluid && - plan.pearlRadius > 0 && - intersectsOnly(cavern.plans, plan, null) && - 1, + anchorBid: ({ plan }) => + !plan.fluid && plan.lakeSize >= 3 && plan.pearlRadius > 0 && 1, }, { - name: "Spawn", + name: "SimpleSpawn.Empty", ...BASE, ...mkRough( { of: Rough.ALWAYS_FLOOR, width: 2, grow: 2 }, { of: Rough.LOOSE_ROCK, grow: 1 }, { of: Rough.MIX_FRINGE }, ), - spawnBid: ({ cavern, plan }) => - !plan.fluid && - plan.pearlRadius > 0 && - intersectsOnly(cavern.plans, plan, null) && - 1, + anchorBid: ({ plan }) => + !plan.fluid && plan.lakeSize >= 3 && plan.pearlRadius > 0 && 1, }, { - // This is mostly a fallback in case there's no other viable spawn cave - // that isn't entirely surrounded by fluid. 9 crystals should be enough to - // ensure an escape route. - name: "Open Spawn with Bonus Crystals", + // This is mostly a fallback in case there's no other viable cave. + // 9 crystals should be enough to ensure an escape route. + name: "SimpleSpawn.Fallback", ...BASE, ...OPEN, crystalsToPlace: () => 9, - spawnBid: ({ plan }) => !plan.fluid && plan.pearlRadius >= 2 && 0.01, + anchorBid: ({ plan }) => !plan.fluid && plan.pearlRadius >= 2 && 0.0001, }, ] as const satisfies readonly Architect[]; export default SIMPLE_SPAWN; diff --git a/src/core/architects/slugs.ts b/src/core/architects/slugs.ts index 0a11e40..69a287e 100644 --- a/src/core/architects/slugs.ts +++ b/src/core/architects/slugs.ts @@ -125,7 +125,7 @@ const SLUG_HALL: PartialArchitect = { const SLUGS = [ { - name: "Slug Nest", + name: "Slugs.Nest", ...SLUG_NEST, ...mkRough( { of: Rough.FLOOR, width: 3, grow: 1 }, @@ -150,7 +150,7 @@ const SLUGS = [ 0.25, }, { - name: "Slug Hall", + name: "Slugs.Hall", ...SLUG_HALL, ...mkRough( { of: Rough.FLOOR }, diff --git a/src/core/architects/thin_hall.ts b/src/core/architects/thin_hall.ts index 267373b..a0ad142 100644 --- a/src/core/architects/thin_hall.ts +++ b/src/core/architects/thin_hall.ts @@ -21,7 +21,7 @@ const HARD_ROCK_MIN_CRYSTALS = const THIN_HALL = [ { - name: "Thin Open Hall", + name: "ThinHall.Open", ...BASE, ...mkRough( { of: Rough.FLOOR }, @@ -36,7 +36,7 @@ const THIN_HALL = [ hallBid: ({ plan }) => !plan.fluid && 0.2, }, { - name: "Thin Filled Hall", + name: "ThinHall.Filled", ...BASE, ...mkRough( { @@ -50,7 +50,7 @@ const THIN_HALL = [ hallBid: ({ plan }) => !plan.fluid && 0.1, }, { - name: "Thin Hard Rock Hall", + name: "ThinHall.HardRock", ...BASE, ...mkRough({ of: Rough.HARD_ROCK }, { of: Rough.VOID, grow: 1 }), hallBid: ({ plan, totalCrystals }) => diff --git a/src/core/architects/treasure.ts b/src/core/architects/treasure.ts index d372d53..2ab800a 100644 --- a/src/core/architects/treasure.ts +++ b/src/core/architects/treasure.ts @@ -96,7 +96,7 @@ const HOARD: typeof BASE = { const discoPoint = plan.innerPearl[0][0]; if ( cavern.ownsScriptOnDiscover[ - cavern.discoveryZones.get(...discoPoint)!.id + cavern.discoveryZones.get(...discoPoint)?.id ?? -1 ] !== plan.id ) { return undefined; @@ -148,7 +148,7 @@ const RICH: typeof BASE = { const TREASURE = [ { - name: "Open Hoard", + name: "Treasure.Hoard.Open", ...HOARD, ...mkRough( { of: Rough.ALWAYS_FLOOR, width: 2, grow: 3 }, @@ -163,7 +163,7 @@ const TREASURE = [ 0.5, }, { - name: "Sealed Hoard", + name: "Treasure.Hoard.Sealed", ...HOARD, ...mkRough( { of: Rough.ALWAYS_FLOOR, width: 1, grow: 3 }, @@ -177,7 +177,7 @@ const TREASURE = [ 0.5, }, { - name: "Open Rich Cave", + name: "Treasure.Rich.Open", ...RICH, ...mkRough( { of: Rough.ALWAYS_SOLID_ROCK, width: 0, grow: 1 }, @@ -191,7 +191,7 @@ const TREASURE = [ !plan.fluid && plan.path.baseplates.length >= 1 && isDeadEnd(plan) && 1, }, { - name: "Rich Island", + name: "Treasure.Rich.Water.Island", ...RICH, ...mkRough( { of: Rough.ALWAYS_SOLID_ROCK, width: 0, grow: 1 }, @@ -217,7 +217,7 @@ const TREASURE = [ }, }, { - name: "Peninsula Hoard", + name: "Treasure.Hoard.Water.Peninsula", ...HOARD, ...mkRough( { of: Rough.ALWAYS_FLOOR, width: 2, grow: 1 }, @@ -234,7 +234,7 @@ const TREASURE = [ 0.5, }, { - name: "Rich Lava Island", + name: "Treasure.Rich.Lava.Island", ...RICH, ...mkRough( { of: Rough.ALWAYS_SOLID_ROCK, width: 0, grow: 1 }, @@ -251,7 +251,7 @@ const TREASURE = [ 0.5, }, { - name: "Lava Peninsula Hoard", + name: "Treasure.Hoard.Lava.Peninsula", ...HOARD, ...mkRough( { of: Rough.ALWAYS_FLOOR, width: 2, grow: 1 }, diff --git a/src/core/architects/utils/hazards.ts b/src/core/architects/utils/hazards.ts index 0f074c2..4b75ffe 100644 --- a/src/core/architects/utils/hazards.ts +++ b/src/core/architects/utils/hazards.ts @@ -1,17 +1,8 @@ import { MutableGrid } from "../../common/grid"; import { Erosion, Landslide } from "../../models/hazards"; import { Plan } from "../../models/plan"; -import { Tile } from "../../models/tiles"; import { DiscoveredCavern } from "../../transformers/03_plastic/01_discover"; -const LANDSLIDABLE_TILES: readonly true[] = (() => { - const r: true[] = []; - r[Tile.DIRT.id] = true; - r[Tile.LOOSE_ROCK.id] = true; - r[Tile.HARD_ROCK.id] = true; - return r; -})(); - const BETA_SUM = 10; export function placeLandslides( @@ -37,7 +28,7 @@ export function placeLandslides( .filter( (point) => !landslides.get(...point) && - LANDSLIDABLE_TILES[cavern.tiles.get(...point)?.id ?? -1] && + cavern.tiles.get(...point)?.canLandslide && rng.chance(spread), ) .forEach((point) => { diff --git a/src/core/architects/utils/resources.ts b/src/core/architects/utils/resources.ts index 5ed55ea..eacd25f 100644 --- a/src/core/architects/utils/resources.ts +++ b/src/core/architects/utils/resources.ts @@ -8,12 +8,17 @@ import { NSEW, Point, offsetBy } from "../../common/geometry"; import { Vehicle } from "../../models/vehicle"; import { Building } from "../../models/building"; -const SEAMABLE = { - [Tile.SOLID_ROCK.id]: true, - [Tile.HARD_ROCK.id]: true, - [Tile.LOOSE_ROCK.id]: true, - [Tile.DIRT.id]: true, -} as const; +function seamable(tile: Tile) { + switch (tile) { + case Tile.SOLID_ROCK: + case Tile.HARD_ROCK: + case Tile.LOOSE_ROCK: + case Tile.DIRT: + return true; + default: + return false; + } +} /** Sprinkles resources throughout the tiles given by getRandomTile. */ function sprinkle( @@ -27,21 +32,20 @@ function sprinkle( ) { for (let remaining = count; remaining > 0; remaining--) { const [x, y] = getRandomTile(); - const t = tiles.get(x, y) ?? Tile.SOLID_ROCK; + const t = tiles.get(x, y); if ( remaining >= 4 && - (t === Tile.SOLID_ROCK || - (t.id in SEAMABLE && seamBias > 0 && rng.chance(seamBias))) + (!t || (seamable(t) && seamBias > 0 && rng.chance(seamBias))) ) { tiles.set(x, y, seam); remaining -= 3; continue; } - if (t === Tile.SOLID_ROCK) { + if (!t) { tiles.set(x, y, Tile.LOOSE_ROCK); } const r = resource.get(x, y) ?? 0; - if (r >= 3 && t.id in SEAMABLE) { + if (r >= 3 && seamable(t ?? Tile.LOOSE_ROCK)) { tiles.set(x, y, seam); resource.set(x, y, r - 3); } else { diff --git a/src/core/common/context.ts b/src/core/common/context.ts index a6105ba..4838829 100644 --- a/src/core/common/context.ts +++ b/src/core/common/context.ts @@ -3,15 +3,15 @@ import { DiceBox } from "./prng"; export type Biome = "rock" | "ice" | "lava"; /** - * Some values are "curved" so they change based on distance from spawn. - * These values can be negative to decrease a value away from spawn. + * Some values are "curved" so they change based on distance from the anchor. + * These values can be negative to decrease a value away from the anchor. */ export type Curve = { - /** The base value at spawn. */ + /** The base value at the anchor. */ readonly base: number; /** * This value is multiplied by a number from 0 to 1 depending on the ratio - * of the maximum possible distance away from spawn. + * of the maximum possible distance away from the anchor. */ readonly hops: number; /** @@ -25,8 +25,6 @@ export type CavernContext = { /** The root seed for the dice box. */ seed: number; - /** Any values not infered directly from the seed. */ - overrides: readonly (keyof CavernContext)[]; /** * Which biome this map is in. Biome affects the default setting for some * other context values, such as how much water or lava a map has. @@ -155,8 +153,8 @@ export type CavernContext = { */ architects: { [key: string]: "encourage" | "disable" }; /** - * The chance each cave will have a recharge seam. Some caves (such as spawn) - * will always have a recharge seam. + * The chance each cave will have a recharge seam. Some caves (like most + * spawns) will always have a recharge seam. */ caveHasRechargeSeamChance: number; /** The chance each hall will have a recharge seam. */ @@ -259,6 +257,9 @@ export type CavernContext = { airSafetyFactor: number; }; +export type PartialCavernContext = Partial & + Pick; + enum Die { biome = 0, targetSize, @@ -370,7 +371,7 @@ function getDefaultFlooding(dice: DiceBox, biome: Biome) { } export function inferContextDefaults( - args: Partial> & Pick, + args: PartialCavernContext, ): CavernContext { const dice = new DiceBox(args.seed); const r = { @@ -404,8 +405,5 @@ export function inferContextDefaults( hasSlugs, heightTargetRange, ...r, - overrides: Object.keys(args) - .filter((k) => k !== "seed") - .sort() as (keyof CavernContext)[], }; } diff --git a/src/core/common/transform.ts b/src/core/common/transform.ts index ec1f199..d835ec0 100644 --- a/src/core/common/transform.ts +++ b/src/core/common/transform.ts @@ -1,12 +1,10 @@ -type TfResult = { +export type TfResult = { result: Current; name: string; progress: number; next: (() => TfResult) | null; }; -export type TransformResult = TfResult; - export type AnyTfResultOf> = BT extends TfBuilder ? T : unknown; diff --git a/src/core/lore/graphs/completeness.test.ts b/src/core/lore/graphs/completeness.test.ts index 1bd90ce..045a2a8 100644 --- a/src/core/lore/graphs/completeness.test.ts +++ b/src/core/lore/graphs/completeness.test.ts @@ -2,6 +2,7 @@ import phraseGraph, { PhraseGraph } from "../builder"; import { State } from "../lore"; import { FAILURE, SUCCESS } from "./conclusions"; import { + FAILURE_BASE_DESTROYED, FOUND_ALL_LOST_MINERS, FOUND_HOARD, FOUND_HQ, @@ -40,8 +41,6 @@ function expectCompletion( } const EXPECTED = phraseGraph(({ pg, state, start, end, cut, skip }) => { - const hasHq = pg(pg(), state("hqIsRuin")); - start .then(skip, state("floodedWithLava", "floodedWithWater")) .then(skip, state("hasMonsters")) @@ -49,12 +48,16 @@ const EXPECTED = phraseGraph(({ pg, state, start, end, cut, skip }) => { .then(skip, state("spawnHasErosion")) .then(skip, state("treasureCaveOne", "treasureCaveMany")) .then( - skip, - state("spawnIsNomadOne", "spawnIsNomadsTogether"), - state("spawnIsHq").then(hasHq).then(cut), + pg(skip, state("spawnIsNomadOne", "spawnIsNomadsTogether")).then( + skip, + state("findHq").then(skip, state("hqIsRuin")), + ), + state("spawnIsHq").then( + skip, + state("hqIsFixedComplete"), + state("hqIsRuin"), + ), ) - .then(skip, state("findHq").then(hasHq).then(cut)) - .then(skip, cut.then(hasHq)) .then( state("resourceObjective"), state("lostMinersOne", "lostMinersTogether", "lostMinersApart").then( @@ -109,3 +112,7 @@ test(`Found Slug Nest is complete`, () => { test(`Seismic Foreshadow is complete`, () => { expectCompletion(SEISMIC_FORESHADOW, EXPECTED); }); + +test(`Failure: Base Destroyed is complete`, () => { + expectCompletion(FAILURE_BASE_DESTROYED, EXPECTED); +}); diff --git a/src/core/lore/graphs/events.ts b/src/core/lore/graphs/events.ts index e69d3be..10327ee 100644 --- a/src/core/lore/graphs/events.ts +++ b/src/core/lore/graphs/events.ts @@ -213,3 +213,23 @@ export const FOUND_SLUG_NEST = phraseGraph( .then(end); }, ); + +export const FAILURE_BASE_DESTROYED = phraseGraph( + ({ pg, state, start, end, cut, skip }) => { + start + .then( + "With your base destroyed,", + "Oh no! The Rock Raider HQ is in ruins, and", + ) + .then( + "I don't think you can complete our mission.", + "That doesn't bode well for our mission.", + ) + .then( + "I'm pulling you out.", + "We're teleporting everyone out.", + "I'm ordering you to evacuate immedately!", + ) + .then(end); + }, +); diff --git a/src/core/lore/graphs/names.ts b/src/core/lore/graphs/names.ts index 8a1407c..7848b1c 100644 --- a/src/core/lore/graphs/names.ts +++ b/src/core/lore/graphs/names.ts @@ -123,3 +123,51 @@ export const NAME = phraseGraph( }); }, ); + +export const OVERRIDE_SUFFIXES = [ + "Ablated", + "Boosted", + "Chief's Version", + "Chrome Edition", + "Diamond Edition", + "Director's Cut", + "Emerald Edition", + "Enhanced", + "Extended", + "Gold Edition", + "HD", + "HD 1.5 Remix", + "Millenium Edition", + "Original Level Do Not Steal", + "Planet U Remix", + "Platinum Edition", + "Rebirthed", + "Reborn", + "Recoded", + "Rectified", + "Recycled", + "Redux", + "Rehashed", + "Reimagined", + "Reloaded", + "Remixed", + "Ressurection", + "Retooled", + "Revenant", + "Revolutions", + "Ruby Edition", + "Sapphire Edition", + "Silver Edition", + "Special Edition", + "Unhinged", + "Unglued", + "Ungrounded", + "Unleashed", + "Unlocked", + "Unobtaininum Edition", + "Unplugged", + "Unsanctioned", + "Unstable", + "Uranium Edition", + "Y2K Edition", +]; diff --git a/src/core/lore/graphs/orders.ts b/src/core/lore/graphs/orders.ts index 0f9ef5e..e1421a5 100644 --- a/src/core/lore/graphs/orders.ts +++ b/src/core/lore/graphs/orders.ts @@ -36,10 +36,6 @@ const ORDERS = phraseGraph(({ pg, state, start, end, cut, skip }) => { .then("and"), pg( skip, - state("spawnHasErosion").then( - "get your Rock Raiders to safety,", - "make sure your Rock Raiders are safe,", - ), state("spawnIsHq") .then(state("hqIsRuin", "spawnHasErosion")) .then("move to a safer cavern,", "find a more suitable location,"), @@ -47,6 +43,10 @@ const ORDERS = phraseGraph(({ pg, state, start, end, cut, skip }) => { .then( "build the Rock Raider HQ", "build up your base", + state("spawnHasErosion").then( + "get your Rock Raiders to safety", + "make sure your Rock Raiders are safe", + ), state("spawnIsHq").then( "send some Rock Raiders down to this base", pg("resume mining operations and") @@ -56,8 +56,15 @@ const ORDERS = phraseGraph(({ pg, state, start, end, cut, skip }) => { "clean up this mess", "get the Rock Raider HQ back in operation", ), + state("hqIsFixedComplete") + .then(skip, state("spawnHasErosion")) + .then( + "keep this base in good working order", + "maintain what you do have here", + ), ), state("findHq") + .then(skip, state("spawnHasErosion")) .then("reach the Rock Raider HQ", "locate the base") .then( skip, diff --git a/src/core/lore/graphs/premise.ts b/src/core/lore/graphs/premise.ts index d8257f6..0ea777a 100644 --- a/src/core/lore/graphs/premise.ts +++ b/src/core/lore/graphs/premise.ts @@ -50,8 +50,20 @@ const PREMISE = phraseGraph(({ pg, state, start, end, cut, skip }) => { "we have reason to believe there are dozens of ${enemies} just out of sight", ); + const hqIsFixedComplete = state("hqIsFixedComplete") + .then( + "the teleporters are operating in a low-power mode, so", + "our engineers tell me that", + ) + .then( + "you will not be able to construct any more buildings.", + "you must make do with the buildings that are already constructed.", + ); + spawnHasErosion.then(", and").then(hasMonstersTexts); - return pg(spawnHasErosion, hasMonstersTexts).then(".").then(end); + return pg(spawnHasErosion, hasMonstersTexts, hqIsFixedComplete.then(end)) + .then(".") + .then(end, hqIsFixedComplete); })(); // Weird case to explain: Find HQ, but the HQ is intact and there are no lost miners. @@ -265,6 +277,14 @@ const PREMISE = phraseGraph(({ pg, state, start, end, cut, skip }) => { .then(skip, findTheOthers.then(cut)), ) .then( + state("hqIsFixedComplete") + .then( + "While the teleporters have been repaired, they are operating in " + + "a low-power mode and cannot send down any buildings.", + "We cannot risk running the teleporters at full power, so you will " + + "have to make do with the buildings that are already there.", + ) + .then(alsoAdditionalHardship), pg( "Our engineers have assured us the teleporters have been repaired, " + "but", diff --git a/src/core/lore/lore.ts b/src/core/lore/lore.ts index cc5dd80..04b3d64 100644 --- a/src/core/lore/lore.ts +++ b/src/core/lore/lore.ts @@ -1,4 +1,4 @@ -import { HqMetadata } from "../architects/established_hq"; +import { HqMetadata } from "../architects/established_hq/base"; import { countLostMiners } from "../architects/lost_miners"; import { DiceBox, PseudorandomStream } from "../common"; import { filterTruthy } from "../common/utils"; @@ -8,6 +8,7 @@ import { Vehicle } from "../models/vehicle"; import { AdjuredCavern } from "../transformers/04_ephemera/01_adjure"; import { FAILURE, SUCCESS } from "./graphs/conclusions"; import { + FAILURE_BASE_DESTROYED, FOUND_ALL_LOST_MINERS, FOUND_HOARD, FOUND_HQ, @@ -32,6 +33,7 @@ export type State = { readonly hasSlugs: boolean; readonly spawnHasErosion: boolean; readonly spawnIsHq: boolean; + readonly hqIsFixedComplete: boolean; readonly spawnIsNomadOne: boolean; readonly spawnIsNomadsTogether: boolean; readonly findHq: boolean; @@ -67,6 +69,7 @@ enum Die { nomadsSettled, foundSlugNest, name, + failureBaseDestroyed, } function floodedWith(cavern: AdjuredCavern): FluidType { @@ -175,18 +178,19 @@ export class Lore { const { lostMiners, lostMinerCaves } = countLostMiners(cavern); - const spawn = cavern.plans.find((p) => !p.hops.length)!; + const anchor = cavern.plans[cavern.anchor]; const hq = cavern.plans.find( (p) => p.metadata?.tag === "hq", ) as Plan; - const spawnIsHq = spawn === hq; + const spawnIsHq = anchor === hq; + const hqIsFixedComplete = hq?.metadata.fixedComplete; const findHq = !!hq && !spawnIsHq; const hqIsRuin = !!hq?.metadata.ruin; const nomads = - spawn.metadata?.tag === "nomads" - ? (spawn.metadata.minersCount as number) + anchor.metadata?.tag === "nomads" + ? (anchor.metadata.minersCount as number) : 0; const treasures = cavern.plans.reduce( @@ -206,9 +210,10 @@ export class Lore { cavern.objectives.studs > 0, hasMonsters: cavern.context.hasMonsters, hasSlugs: cavern.context.hasSlugs, - spawnHasErosion: spawn.hasErosion, + spawnHasErosion: anchor.hasErosion, spawnIsHq, findHq, + hqIsFixedComplete, hqIsRuin, spawnIsNomadOne: nomads === 1, spawnIsNomadsTogether: nomads > 1, @@ -312,4 +317,12 @@ export class Lore { generateSeismicForeshadow(rng: PseudorandomStream) { return SEISMIC_FORESHADOW.generate(rng, this.state, this.vars); } + + generateFailureBaseDestroyed(dice: DiceBox) { + return FAILURE_BASE_DESTROYED.generate( + dice.lore(Die.failureBaseDestroyed), + this.state, + this.vars, + ); + } } diff --git a/src/core/models/architect.ts b/src/core/models/architect.ts index 9e09374..f66891b 100644 --- a/src/core/models/architect.ts +++ b/src/core/models/architect.ts @@ -1,10 +1,15 @@ -import { PartialPlannedCavern } from "../transformers/01_planning/00_negotiate"; import { FoundationPlasticCavern } from "../transformers/02_masonry/00_foundation"; import { RoughPlasticCavern } from "../transformers/02_masonry/01_rough"; import { Plan } from "./plan"; -import { EstablishedPlan } from "../transformers/01_planning/03_establish"; -import { ArchitectedPlan } from "../transformers/01_planning/03_establish"; -import { FloodedPlan } from "../transformers/01_planning/02_flood"; +import { + EstablishedPlan, + OrderedOrEstablishedPlan, +} from "../transformers/01_planning/05_establish"; +import { ArchitectedPlan } from "../transformers/01_planning/05_establish"; +import { + FloodedCavern, + FloodedPlan, +} from "../transformers/01_planning/02_flood"; import { RoughTile, Tile } from "./tiles"; import { MutableGrid } from "../common/grid"; import { Building } from "./building"; @@ -16,37 +21,38 @@ import { Objectives } from "./objectives"; import { DiscoveredCavern } from "../transformers/03_plastic/01_discover"; import { Vehicle, VehicleFactory } from "./vehicle"; import { StrataformedCavern } from "../transformers/03_plastic/02_strataform"; -import { CollapseUnion } from "../common/utils"; -import { AnyMetadata } from "../architects"; import { PreprogrammedCavern } from "../transformers/04_ephemera/03_preprogram"; import { EnscribedCavern } from "../transformers/04_ephemera/02_enscribe"; import { DiscoveryZone } from "./discovery_zone"; import { Point } from "../common/geometry"; - -type SpawnBidArgs = { - readonly cavern: PartialPlannedCavern; +import { ModdedCavern } from "../transformers/01_planning/04_mod"; +import { + AnchoredCavern, + OrderedPlan, +} from "../transformers/01_planning/03_anchor"; + +type anchorBidArgs = { + readonly cavern: FloodedCavern; readonly plan: FloodedPlan; }; export type BaseMetadata = { readonly tag: string } | undefined; -type BidArgs = SpawnBidArgs & { - readonly plans: readonly CollapseUnion< - FloodedPlan | EstablishedPlan - >[]; +type BidArgs = anchorBidArgs & { + readonly plans: readonly OrderedOrEstablishedPlan[]; readonly hops: readonly number[]; readonly totalCrystals: number; }; type EstablishArgs = { - readonly cavern: PartialPlannedCavern; + readonly cavern: ModdedCavern; readonly plan: ArchitectedPlan; readonly totalCrystals: number; }; type PrimeArgs = { - readonly cavern: PartialPlannedCavern; - readonly plan: FloodedPlan; + readonly cavern: ModdedCavern; + readonly plan: OrderedPlan; }; export type BaseArchitect = { @@ -54,7 +60,9 @@ export type BaseArchitect = { caveBid?(args: BidArgs): number | false; hallBid?(args: BidArgs): number | false; - spawnBid?(args: SpawnBidArgs): number | false; + anchorBid?(args: anchorBidArgs): number | false; + + mod?(args: AnchoredCavern): ModdedCavern; prime(args: PrimeArgs): T; @@ -154,5 +162,5 @@ export type Architect = BaseArchitect & ( | { caveBid: NonNullable["caveBid"]> } | { hallBid: NonNullable["hallBid"]> } - | { spawnBid: NonNullable["spawnBid"]> } + | { anchorBid: NonNullable["anchorBid"]> } ); diff --git a/src/core/models/building.ts b/src/core/models/building.ts index c23a91c..b1256c2 100644 --- a/src/core/models/building.ts +++ b/src/core/models/building.ts @@ -215,6 +215,19 @@ export const SUPER_TELEPORT = new BuildingTemplate( [TOOL_STORE, TELEPORT_PAD, POWER_STATION, SUPPORT_STATION], ); +export const ALL_BUILDINGS = [ + TOOL_STORE, + TELEPORT_PAD, + CANTEEN, + POWER_STATION, + SUPPORT_STATION, + UPGRADE_STATION, + GEOLOGICAL_CENTER, + ORE_REFINERY, + MINING_LASER, + SUPER_TELEPORT, +] as const; + export class BuildingDoesNotFitException extends Error {} export type Building = EntityPosition & { diff --git a/src/core/models/cavern.ts b/src/core/models/cavern.ts index 1ab3e92..05a11c8 100644 --- a/src/core/models/cavern.ts +++ b/src/core/models/cavern.ts @@ -1,17 +1,15 @@ -import { AnyMetadata } from "../architects"; import { DiceBox } from "../common"; -import { CavernContext } from "../common/context"; +import { CavernContext, PartialCavernContext } from "../common/context"; import { AnyTfResultOf } from "../common/transform"; import { CollapseUnion } from "../common/utils"; import { CAVERN_TF } from "../transformers"; -import { Plan } from "./plan"; +import { PearledCavern } from "../transformers/01_planning/06_pearl"; export type BaseCavern = { + initialContext: PartialCavernContext; context: CavernContext; dice: DiceBox; }; -export type PlannedCavern = BaseCavern & { - plans: readonly Plan[]; -}; +export type PlannedCavern = PearledCavern; export type Cavern = CollapseUnion>; diff --git a/src/core/models/plan.ts b/src/core/models/plan.ts index 1caa572..c957909 100644 --- a/src/core/models/plan.ts +++ b/src/core/models/plan.ts @@ -1,3 +1,3 @@ -import { PearledPlan } from "../transformers/01_planning/04_pearl"; +import { PearledPlan } from "../transformers/01_planning/06_pearl"; import { BaseMetadata } from "./architect"; export type Plan = PearledPlan; diff --git a/src/core/models/tiles.ts b/src/core/models/tiles.ts index 148b82b..f0d1ed3 100644 --- a/src/core/models/tiles.ts +++ b/src/core/models/tiles.ts @@ -12,6 +12,7 @@ type BaseTile = { id: number; name: string; hardness: Hardness; + canLandslide: boolean; isWall: boolean; isFluid: boolean; maxSlope: number | undefined; @@ -21,27 +22,27 @@ type BaseTile = { // prettier-ignore const TILES = { - FLOOR: {id: 1, name: "Cavern Floor", crystalYield: 0, hardness: Hardness.NONE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, - LAVA: {id: 6, name: "Lava", crystalYield: 0, hardness: Hardness.NONE, isFluid: true, isWall: false, maxSlope: 0, oreYield: 0}, - WATER: {id: 11, name: "Water", crystalYield: 0, hardness: Hardness.NONE, isFluid: true, isWall: false, maxSlope: 0, oreYield: 0}, - DIRT: {id: 26, name: "Dirt", crystalYield: 0, hardness: Hardness.DIRT, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, - LOOSE_ROCK: {id: 30, name: "Loose Rock", crystalYield: 0, hardness: Hardness.LOOSE, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, - HARD_ROCK: {id: 34, name: "Hard Rock", crystalYield: 0, hardness: Hardness.HARD, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, - SOLID_ROCK: {id: 38, name: "Solid Rock", crystalYield: 0, hardness: Hardness.SOLID, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, - RUBBLE_1: {id: 2, name: "Rubble", crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 1}, - RUBBLE_2: {id: 3, name: "Rubble", crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 2}, - RUBBLE_3: {id: 4, name: "Rubble", crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 3}, - RUBBLE_4: {id: 5, name: "Rubble", crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 4}, - SLUG_HOLE: {id: 12, name: "Slimy Slug Hole", crystalYield: 0, hardness: Hardness.NONE, isFluid: false, isWall: false, maxSlope: 15, oreYield: 0}, - FOUNDATION: {id: 14, name: "Foundation", crystalYield: 0, hardness: Hardness.NONE, isFluid: false, isWall: false, maxSlope: 15, oreYield: 0}, - POWER_PATH: {id: 24, name: "Power Path", crystalYield: 0, hardness: Hardness.NONE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, - LANDSLIDE_RUBBLE_4: {id: 60, name: "Rubble", crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, - LANDSLIDE_RUBBLE_3: {id: 61, name: "Rubble", crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, - LANDSLIDE_RUBBLE_2: {id: 62, name: "Rubble", crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, - LANDSLIDE_RUBBLE_1: {id: 63, name: "Rubble", crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, - CRYSTAL_SEAM: {id: 42, name: "Energy Crystal Seam", crystalYield: 4, hardness: Hardness.SEAM, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, - ORE_SEAM: {id: 46, name: "Ore Seam", crystalYield: 0, hardness: Hardness.SEAM, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 8}, - RECHARGE_SEAM: {id: 50, name: "Recharge Seam", crystalYield: 0, hardness: Hardness.SOLID, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 0}, + FLOOR: {id: 1, name: "Cavern Floor", canLandslide: false, crystalYield: 0, hardness: Hardness.NONE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, + LAVA: {id: 6, name: "Lava", canLandslide: false, crystalYield: 0, hardness: Hardness.NONE, isFluid: true, isWall: false, maxSlope: 0, oreYield: 0}, + WATER: {id: 11, name: "Water", canLandslide: false, crystalYield: 0, hardness: Hardness.NONE, isFluid: true, isWall: false, maxSlope: 0, oreYield: 0}, + DIRT: {id: 26, name: "Dirt", canLandslide: true, crystalYield: 0, hardness: Hardness.DIRT, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, + LOOSE_ROCK: {id: 30, name: "Loose Rock", canLandslide: true, crystalYield: 0, hardness: Hardness.LOOSE, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, + HARD_ROCK: {id: 34, name: "Hard Rock", canLandslide: true, crystalYield: 0, hardness: Hardness.HARD, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, + SOLID_ROCK: {id: 38, name: "Solid Rock", canLandslide: false, crystalYield: 0, hardness: Hardness.SOLID, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, + RUBBLE_1: {id: 2, name: "Rubble", canLandslide: false, crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 1}, + RUBBLE_2: {id: 3, name: "Rubble", canLandslide: false, crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 2}, + RUBBLE_3: {id: 4, name: "Rubble", canLandslide: false, crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 3}, + RUBBLE_4: {id: 5, name: "Rubble", canLandslide: false, crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 4}, + SLUG_HOLE: {id: 12, name: "Slimy Slug Hole", canLandslide: false, crystalYield: 0, hardness: Hardness.NONE, isFluid: false, isWall: false, maxSlope: 15, oreYield: 0}, + FOUNDATION: {id: 14, name: "Foundation", canLandslide: false, crystalYield: 0, hardness: Hardness.NONE, isFluid: false, isWall: false, maxSlope: 15, oreYield: 0}, + POWER_PATH: {id: 24, name: "Power Path", canLandslide: false, crystalYield: 0, hardness: Hardness.NONE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, + LANDSLIDE_RUBBLE_4: {id: 60, name: "Rubble", canLandslide: false, crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, + LANDSLIDE_RUBBLE_3: {id: 61, name: "Rubble", canLandslide: false, crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, + LANDSLIDE_RUBBLE_2: {id: 62, name: "Rubble", canLandslide: false, crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, + LANDSLIDE_RUBBLE_1: {id: 63, name: "Rubble", canLandslide: false, crystalYield: 0, hardness: Hardness.RUBBLE, isFluid: false, isWall: false, maxSlope: undefined, oreYield: 0}, + CRYSTAL_SEAM: {id: 42, name: "Energy Crystal Seam", canLandslide: false, crystalYield: 4, hardness: Hardness.SEAM, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 4}, + ORE_SEAM: {id: 46, name: "Ore Seam", canLandslide: false, crystalYield: 0, hardness: Hardness.SEAM, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 8}, + RECHARGE_SEAM: {id: 50, name: "Recharge Seam", canLandslide: false, crystalYield: 0, hardness: Hardness.SOLID, isFluid: false, isWall: true, maxSlope: undefined, oreYield: 0}, } as const satisfies { [K in any]: BaseTile }; export const Tile = TILES; diff --git a/src/core/models/vehicle.ts b/src/core/models/vehicle.ts index 2711e00..f2ac574 100644 --- a/src/core/models/vehicle.ts +++ b/src/core/models/vehicle.ts @@ -193,7 +193,8 @@ export function serializeVehicle( return [ vehicle.template.id, serializePosition(vehicle, offset, heightMap, 0, "entity"), - vehicle.upgrades.map((u) => `${u}/`).join(""), + vehicle.upgrades.length && + `upgrades=${vehicle.upgrades.map((u) => `${u}/`).join("")}`, vehicle.driverId !== null && `driver=${vehicle.driverId.toFixed()}`, vehicle.essential && "Essential=true", `ID=${vehicle.id.toFixed()}`, diff --git a/src/core/transformers/00_outlines/06_weave.ts b/src/core/transformers/00_outlines/06_weave.ts index fb4c034..df4df04 100644 --- a/src/core/transformers/00_outlines/06_weave.ts +++ b/src/core/transformers/00_outlines/06_weave.ts @@ -33,6 +33,7 @@ function getGraph(paths: readonly Path[]): GraphNode[] { return result; } +// Returns the total distance function getAllDistances(graph: GraphNode[], paths: Path[], src: Baseplate) { const distances: number[] = []; const queue: Baseplate[] = []; @@ -63,7 +64,7 @@ function getAllDistances(graph: GraphNode[], paths: Path[], src: Baseplate) { return result; } -/** Returns the angle between two absolute angles. */ +// Returns the angle between two absolute angles. function getOffset(t1: number, t2: number): number { const r = Math.abs(t1 - t2); // Invert reflex angles @@ -92,7 +93,8 @@ export default function weave(cavern: TriangulatedCavern): TriangulatedCavern { }, Infinity); } - // Delete any paths that don't form a minimum angle + // Delete any paths that don't form a minimum angle. + // Returns true if there's at least one path left in the queue. function pruneByAngle() { let ok = false; paths diff --git a/src/core/transformers/01_planning/00_negotiate.ts b/src/core/transformers/01_planning/00_negotiate.ts index 4792d45..fee5496 100644 --- a/src/core/transformers/01_planning/00_negotiate.ts +++ b/src/core/transformers/01_planning/00_negotiate.ts @@ -3,12 +3,7 @@ import { BaseCavern } from "../../models/cavern"; import { TriangulatedCavern } from "../00_outlines/02_triangulate"; import { Path } from "../../models/path"; import { Plan } from "../../models/plan"; -import { AnyMetadata } from "../../architects"; - -export type PartialPlannedCavern>> = - BaseCavern & { - readonly plans: readonly T[]; - }; +import { WithPlanType } from "./utils"; export type NegotiatedPlan = { /** Unique ID of the Plan, used for RNG and indexing. */ @@ -19,6 +14,8 @@ export type NegotiatedPlan = { readonly path: Path; }; +export type NegotiatedCavern = WithPlanType; + /* * Returns whether these Baseplates can be combined into one big Cave. * Must call this both ways. @@ -44,7 +41,7 @@ function isMergeable(a: Baseplate, b: Baseplate): boolean { */ export default function negotiate( cavern: TriangulatedCavern, -): PartialPlannedCavern { +): NegotiatedCavern { const bpIsInBigCave: true[] = []; const queue: Pick, "kind" | "path">[][] = [[], [], []]; @@ -78,5 +75,10 @@ export default function negotiate( .flatMap((a) => a) .map((plan, id) => ({ ...plan, id })); - return { context: cavern.context, dice: cavern.dice, plans }; + return { + initialContext: cavern.initialContext, + context: cavern.context, + dice: cavern.dice, + plans, + }; } diff --git a/src/core/transformers/01_planning/01_measure.ts b/src/core/transformers/01_planning/01_measure.ts index 513062b..97e7b42 100644 --- a/src/core/transformers/01_planning/01_measure.ts +++ b/src/core/transformers/01_planning/01_measure.ts @@ -1,5 +1,5 @@ -import { PartialPlannedCavern } from "./00_negotiate"; -import { NegotiatedPlan } from "./00_negotiate"; +import { NegotiatedCavern, NegotiatedPlan } from "./00_negotiate"; +import { WithPlanType } from "./utils"; export type MeasuredPlan = NegotiatedPlan & { /** @@ -12,9 +12,9 @@ export type MeasuredPlan = NegotiatedPlan & { readonly perimeter: number; }; -export default function measure( - cavern: PartialPlannedCavern, -): PartialPlannedCavern { +export type MeasuredCavern = WithPlanType; + +export default function measure(cavern: NegotiatedCavern): MeasuredCavern { const planIdsByBp: number[][] = []; cavern.plans.forEach((plan) => { plan.path.baseplates.forEach((bp) => diff --git a/src/core/transformers/01_planning/02_flood.ts b/src/core/transformers/01_planning/02_flood.ts index 98276ba..fff3428 100644 --- a/src/core/transformers/01_planning/02_flood.ts +++ b/src/core/transformers/01_planning/02_flood.ts @@ -1,16 +1,23 @@ -import { PartialPlannedCavern } from "./00_negotiate"; import { FluidType, Tile } from "../../models/tiles"; -import { MeasuredPlan } from "./01_measure"; +import { MeasuredCavern, MeasuredPlan } from "./01_measure"; import { PseudorandomStream } from "../../common"; import { pairMap } from "../../common/utils"; +import { WithPlanType } from "./utils"; export type FloodedPlan = MeasuredPlan & { /** What kind of fluid is present in this plan. */ readonly fluid: FluidType; + /** + * How many contiguous plans have the same fluid? + * For plans without fluid, how many contiguous plans have no fluid? + */ + readonly lakeSize: number; /** Should this plan contain erosion? */ readonly hasErosion: boolean; }; +export type FloodedCavern = WithPlanType; + type Lake = { readonly fluid: FluidType; readonly skipChance: number; @@ -19,7 +26,7 @@ type Lake = { }; function getLakes( - cavern: PartialPlannedCavern, + cavern: MeasuredCavern, rng: PseudorandomStream, ): readonly Lake[] { const plans = rng.shuffle( @@ -53,9 +60,37 @@ function getLakes( ]; } -export default function flood( - cavern: PartialPlannedCavern, -): PartialPlannedCavern { +// Measures the final size of all lakes. +function measureLakes( + cavern: MeasuredCavern, + fluids: (FluidType | undefined)[], +) { + const results: number[] = []; + for (let i = 0; i < cavern.plans.length; i++) { + if (results[i]) { + continue; + } + const queue = [i]; + const out = []; + while (queue.length) { + const j = queue.shift()!; + if (results[j]) { + continue; + } + results[j] = Infinity; + out.push(j); + queue.push( + ...cavern.plans[j].intersects + .map((_, k) => k) + .filter((k) => fluids[k] === fluids[i]), + ); + } + out.forEach((j) => (results[j] = out.length)); + } + return results; +} + +export default function flood(cavern: MeasuredCavern): FloodedCavern { const rng = cavern.dice.flood; const lakes = getLakes(cavern, rng); const fluids: (FluidType | undefined)[] = []; @@ -122,9 +157,12 @@ export default function flood( }); } + const lakeSizes = measureLakes(cavern, fluids); + const plans = cavern.plans.map((plan) => ({ ...plan, fluid: fluids[plan.id] ?? null, + lakeSize: lakeSizes[plan.id], hasErosion: !!erosion[plan.id], })); return { ...cavern, plans }; diff --git a/src/core/transformers/01_planning/03_anchor.ts b/src/core/transformers/01_planning/03_anchor.ts new file mode 100644 index 0000000..bb8d11f --- /dev/null +++ b/src/core/transformers/01_planning/03_anchor.ts @@ -0,0 +1,38 @@ +import { ARCHITECTS } from "../../architects"; +import { Architect } from "../../models/architect"; +import { FloodedCavern, FloodedPlan } from "./02_flood"; +import encourageDisable from "./utils"; +import { WithPlanType } from "./utils"; + +export type OrderedPlan = FloodedPlan & { + architect?: Architect; + hops: readonly number[]; +}; + +export type AnchoredCavern = WithPlanType< + FloodedCavern, + FloodedPlan | OrderedPlan +> & { anchor: number }; + +export default function anchor(cavern: FloodedCavern): AnchoredCavern { + const architects = encourageDisable(ARCHITECTS, cavern); + + // Choose a spawn and an architect for that spawn. + const anchor = cavern.dice.pickSpawn.weightedChoice( + architects + .filter((architect) => architect.anchorBid) + .flatMap((architect) => + cavern.plans + .filter((p) => p.kind === "cave") + .map((plan) => ({ + item: { ...plan, architect, hops: [] }, + bid: architect.anchorBid!({ cavern, plan }) || 0, + })), + ), + ); + + const plans: (FloodedPlan | OrderedPlan)[] = [...cavern.plans]; + plans[anchor.id] = anchor; + + return { ...cavern, anchor: anchor.id, plans }; +} diff --git a/src/core/transformers/01_planning/03_establish.ts b/src/core/transformers/01_planning/03_establish.ts deleted file mode 100644 index a6d261d..0000000 --- a/src/core/transformers/01_planning/03_establish.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { ARCHITECTS, AnyMetadata } from "../../architects"; -import { Curve } from "../../common"; -import { CollapseUnion } from "../../common/utils"; -import { Architect, BaseMetadata } from "../../models/architect"; -import { PartialPlannedCavern } from "./00_negotiate"; -import { FloodedPlan } from "./02_flood"; - -type SortedPlan = { - plan: FloodedPlan & { architect?: Architect }; - hops: readonly number[]; - index: number; -}; -export type ArchitectedPlan = FloodedPlan & { - readonly hops: readonly number[]; - /** The architect to use to build out the plan. */ - readonly architect: Architect; - readonly metadata: T; - readonly crystalRichness: number; - readonly oreRichness: number; - readonly monsterSpawnRate: number; - readonly monsterWaveSize: number; -}; -export type EstablishedPlan = ArchitectedPlan & { - /** How blobby the pearl should be. */ - readonly baroqueness: number; - /** How many crystals the Plan will add. */ - readonly crystals: number; - /** How many ore the Plan will add. */ - readonly ore: number; -}; - -type CurveProps = { hops: number; order: number }; - -function curved(curve: Curve, props: CurveProps): number { - return curve.base + curve.hops * props.hops + curve.order * props.order; -} - -function encourageDisable>( - architects: readonly T[], - cavern: PartialPlannedCavern, -): T[] { - return architects - .filter((a) => cavern.context.architects?.[a.name] !== "disable") - .map((a) => { - if (cavern.context.architects?.[a.name] === "encourage") { - const r = { ...a }; - r.caveBid = (args) => !!a.caveBid?.(args) && 999999; - r.hallBid = (args) => !!a.hallBid?.(args) && 999999; - r.spawnBid = (args) => !!a.spawnBid?.(args) && 999999; - return r; - } - return a; - }); -} - -export default function establish( - cavern: PartialPlannedCavern, -): PartialPlannedCavern> { - const architects = encourageDisable(ARCHITECTS, cavern); - - // Choose a spawn and an architect for that spawn. - const spawn = cavern.dice.pickSpawn.weightedChoice( - architects - .filter((architect) => architect.spawnBid) - .flatMap((architect) => - cavern.plans - .filter((p) => p.kind === "cave") - .map((plan) => ({ - item: { ...plan, architect }, - bid: architect.spawnBid!({ cavern, plan }) || 0, - })), - ), - ); - - // Sort the plans in a breadth-first search order, starting from spawn and - // annotating each with the index and the hops it takes to get here from spawn. - function sortPlans(): SortedPlan[] { - const isQueued: true[] = []; - isQueued[spawn.id] = true; - const queue: { plan: FloodedPlan; hops: readonly number[] }[] = [ - { plan: spawn, hops: [] }, - ]; - const result: SortedPlan[] = []; - - for (let index = 0; queue.length > 0; index++) { - const { plan, hops } = queue.shift()!; - - const neighbors = plan.intersects - .map((b, id) => (b ? id : -1)) - .filter( - (id) => - id >= 0 && !isQueued[id] && cavern.plans[id].kind !== plan.kind, - ); - neighbors.forEach((id) => (isQueued[id] = true)); - queue.push( - ...neighbors.map((id) => ({ - plan: cavern.plans[id], - hops: [...hops, plan.id], - })), - ); - result.push({ plan, hops, index }); - } - return result; - } - const inOrder = sortPlans(); - - const plans: CollapseUnion>[] = - cavern.plans.slice(); - let totalCrystals = 0; - - const maxIndex = inOrder.length - 1; - const maxHops = inOrder[inOrder.length - 1].hops.length; - function doArchitect({ - plan, - hops, - index, - }: SortedPlan): ArchitectedPlan { - const props = { hops: hops.length / maxHops, order: index / maxIndex }; - const architect = - plan.architect || - cavern.dice.pickArchitect(plan.id).weightedChoice( - architects.map((architect) => { - const bid = - plan.kind === "cave" ? architect.caveBid : architect.hallBid; - return { - item: architect, - bid: bid?.({ cavern, plan, plans, hops, totalCrystals }) || 0, - }; - }), - ); - const metadata = architect.prime({ cavern, plan }); - const crystalRichness = curved( - plan.kind === "cave" - ? cavern.context.caveCrystalRichness - : cavern.context.hallCrystalRichness, - props, - ); - const oreRichness = curved( - plan.kind === "cave" - ? cavern.context.caveOreRichness - : cavern.context.hallOreRichness, - props, - ); - const monsterSpawnRate = curved(cavern.context.monsterSpawnRate, props); - const monsterWaveSize = curved(cavern.context.monsterWaveSize, props); - return { - ...plan, - hops, - architect, - metadata, - crystalRichness, - oreRichness, - monsterSpawnRate, - monsterWaveSize, - }; - } - function doEstablish(plan: ArchitectedPlan) { - const args = { cavern, plan, totalCrystals }; - const baroqueness = plan.architect.baroqueness(args); - const crystals = Math.round( - plan.architect.crystalsToPlace(args) + - plan.architect.crystalsFromMetadata(plan.metadata), - ); - totalCrystals += crystals; - const ore = Math.round(plan.architect.ore(args)); - const established: EstablishedPlan = { - ...plan, - baroqueness, - crystals, - ore, - }; - plans[plan.id] = established; - } - inOrder.forEach((plan) => doEstablish(doArchitect(plan))); - - return { ...cavern, plans: plans as EstablishedPlan[] }; -} diff --git a/src/core/transformers/01_planning/04_mod.ts b/src/core/transformers/01_planning/04_mod.ts new file mode 100644 index 0000000..2d4503c --- /dev/null +++ b/src/core/transformers/01_planning/04_mod.ts @@ -0,0 +1,16 @@ +import { AnchoredCavern, OrderedPlan } from "./03_anchor"; + +export type ModdedCavern = AnchoredCavern; + +// In this step, the anchor architect has carte blanche to change any aspect of +// the cavern before any plans are established. Use with caution. + +// When defining mods, avoid: +// - Using mods to accomplish anything that can be done without them. +// - Redefining initial context or any context attribute that has already been +// "used" up to this point like lake counts. + +export default function mod(cavern: AnchoredCavern): ModdedCavern { + const architect = (cavern.plans[cavern.anchor] as OrderedPlan).architect!; + return architect.mod ? architect.mod(cavern) : cavern; +} diff --git a/src/core/transformers/01_planning/05_establish.ts b/src/core/transformers/01_planning/05_establish.ts new file mode 100644 index 0000000..fe6a884 --- /dev/null +++ b/src/core/transformers/01_planning/05_establish.ts @@ -0,0 +1,142 @@ +import { ARCHITECTS, AnyMetadata } from "../../architects"; +import encourageDisable from "./utils"; +import { Curve } from "../../common"; +import { CollapseUnion } from "../../common/utils"; +import { Architect, BaseMetadata } from "../../models/architect"; +import { AnchoredCavern, OrderedPlan } from "./03_anchor"; +import { ModdedCavern } from "./04_mod"; +import { WithPlanType } from "./utils"; + +export type ArchitectedPlan = OrderedPlan & { + /** The architect to use to build out the plan. */ + readonly architect: Architect; + readonly metadata: T; + readonly crystalRichness: number; + readonly oreRichness: number; + readonly monsterSpawnRate: number; + readonly monsterWaveSize: number; +}; +export type EstablishedPlan = ArchitectedPlan & { + /** How blobby the pearl should be. */ + readonly baroqueness: number; + /** How many crystals the Plan will add. */ + readonly crystals: number; + /** How many ore the Plan will add. */ + readonly ore: number; +}; + +export type OrderedOrEstablishedPlan = CollapseUnion< + OrderedPlan | EstablishedPlan +>; + +export type EstablishedCavern = WithPlanType< + ModdedCavern, + EstablishedPlan +>; + +// Sort the plans in a breadth-first search order and log the hops they take. +function orderPlans(cavern: ModdedCavern): OrderedPlan[] { + const queue = cavern.plans.filter((plan) => "hops" in plan) as OrderedPlan[]; + const isQueued: true[] = []; + queue.forEach((plan) => (isQueued[plan.id] = true)); + + for (let i = 0; i < cavern.plans.length; i++) { + const plan = queue[i]; + if (!plan) { + throw new Error("Failed to order all plans. (Is the graph disjoint?)"); + } + + const neighbors = plan.intersects + .map((b, id) => (b ? id : -1)) + .filter( + (id) => id >= 0 && !isQueued[id] && cavern.plans[id].kind !== plan.kind, + ); + neighbors.forEach((id) => (isQueued[id] = true)); + queue.push( + ...neighbors.map((id) => ({ + ...cavern.plans[id], + hops: [...plan.hops, plan.id], + })), + ); + } + return queue; +} + +type CurveProps = { hops: number; order: number }; + +function curved(curve: Curve, props: CurveProps): number { + return curve.base + curve.hops * props.hops + curve.order * props.order; +} + +export default function establish(cavern: AnchoredCavern): EstablishedCavern { + const architects = encourageDisable(ARCHITECTS, cavern); + const inOrder = orderPlans(cavern); + const plans: OrderedOrEstablishedPlan[] = []; + inOrder.forEach((plan) => (plans[plan.id] = plan)); + + let totalCrystals = 0; + const maxIndex = inOrder.length - 1; + const maxHops = inOrder[inOrder.length - 1].hops.length; + + function doArchitect(plan: OrderedPlan, index: number): ArchitectedPlan { + const props = { hops: plan.hops.length / maxHops, order: index / maxIndex }; + const architect = + plan.architect || + cavern.dice.pickArchitect(plan.id).weightedChoice( + architects.map((architect) => { + const bid = + plan.kind === "cave" ? architect.caveBid : architect.hallBid; + return { + item: architect, + bid: + bid?.({ cavern, plan, plans, hops: plan.hops, totalCrystals }) || + 0, + }; + }), + ); + const metadata = architect.prime({ cavern, plan }); + const crystalRichness = curved( + plan.kind === "cave" + ? cavern.context.caveCrystalRichness + : cavern.context.hallCrystalRichness, + props, + ); + const oreRichness = curved( + plan.kind === "cave" + ? cavern.context.caveOreRichness + : cavern.context.hallOreRichness, + props, + ); + const monsterSpawnRate = curved(cavern.context.monsterSpawnRate, props); + const monsterWaveSize = curved(cavern.context.monsterWaveSize, props); + return { + ...plan, + architect, + metadata, + crystalRichness, + oreRichness, + monsterSpawnRate, + monsterWaveSize, + }; + } + function doEstablish(plan: ArchitectedPlan) { + const args = { cavern, plan, totalCrystals }; + const baroqueness = plan.architect.baroqueness(args); + const crystals = Math.round( + plan.architect.crystalsToPlace(args) + + plan.architect.crystalsFromMetadata(plan.metadata), + ); + totalCrystals += crystals; + const ore = Math.round(plan.architect.ore(args)); + const established: EstablishedPlan = { + ...plan, + baroqueness, + crystals, + ore, + }; + plans[plan.id] = established; + } + inOrder.forEach((plan, i) => doEstablish(doArchitect(plan, i))); + + return { ...cavern, plans: plans as EstablishedPlan[] }; +} diff --git a/src/core/transformers/01_planning/04_pearl.test.ts b/src/core/transformers/01_planning/06_pearl.test.ts similarity index 96% rename from src/core/transformers/01_planning/04_pearl.test.ts rename to src/core/transformers/01_planning/06_pearl.test.ts index 864b695..12a0322 100644 --- a/src/core/transformers/01_planning/04_pearl.test.ts +++ b/src/core/transformers/01_planning/06_pearl.test.ts @@ -1,8 +1,8 @@ import { PseudorandomStream } from "../../common"; import { Baseplate } from "../../models/baseplate"; import { Path } from "../../models/path"; -import { EstablishedPlan } from "./03_establish"; -import { LayerGrid, caveNucleus, hallNucleus, trail } from "./04_pearl"; +import { EstablishedPlan } from "./05_establish"; +import { LayerGrid, caveNucleus, hallNucleus, trail } from "./06_pearl"; describe("LayerGrid", () => { it("stores layers", () => { diff --git a/src/core/transformers/01_planning/04_pearl.ts b/src/core/transformers/01_planning/06_pearl.ts similarity index 90% rename from src/core/transformers/01_planning/04_pearl.ts rename to src/core/transformers/01_planning/06_pearl.ts index 7fa0d14..b35676b 100644 --- a/src/core/transformers/01_planning/04_pearl.ts +++ b/src/core/transformers/01_planning/06_pearl.ts @@ -4,8 +4,8 @@ import { NSEW, Point, plotLine } from "../../common/geometry"; import { MutableGrid } from "../../common/grid"; import { pairEach } from "../../common/utils"; import { BaseMetadata } from "../../models/architect"; -import { PartialPlannedCavern } from "./00_negotiate"; -import { EstablishedPlan } from "./03_establish"; +import { EstablishedCavern, EstablishedPlan } from "./05_establish"; +import { WithPlanType } from "./utils"; type Layer = readonly Point[]; export type Pearl = readonly Layer[]; @@ -20,6 +20,11 @@ export type PearledPlan = EstablishedPlan & { readonly outerPearl: Pearl; }; +export type PearledCavern = WithPlanType< + EstablishedCavern, + PearledPlan +>; + export class LayerGrid extends MutableGrid { atLayer(layer: number): Point[] { const result: Point[] = []; @@ -159,9 +164,7 @@ function addLayer( ); } -export default function pearl( - cavern: PartialPlannedCavern>, -): PartialPlannedCavern> { +export default function pearl(cavern: EstablishedCavern): PearledCavern { const plans = cavern.plans.map((plan) => { const rng = cavern.dice.pearl(plan.id); const grid: LayerGrid = new LayerGrid(); @@ -169,14 +172,10 @@ export default function pearl( const innerPearl: Point[][] = [grid.map((_, x, y) => [x, y])]; const outerPearl: Point[][] = []; const pearlRadius = plan.architect.roughExtent(plan); - for (let i = 1; i < pearlRadius; i++) { - innerPearl.push(addLayer(grid, rng, plan.baroqueness, i)); - } - if (pearlRadius > 0) { - innerPearl.push(addLayer(grid, rng, 0, pearlRadius)); - } - for (let i = 1; i < 4; i++) { - outerPearl.push(addLayer(grid, rng, 0, i + pearlRadius)); + for (let i = 1; i < pearlRadius + 4; i++) { + (i <= pearlRadius ? innerPearl : outerPearl).push( + addLayer(grid, rng, i < pearlRadius ? plan.baroqueness : 0, i), + ); } return { ...plan, innerPearl, outerPearl }; }); diff --git a/src/core/transformers/01_planning/index.ts b/src/core/transformers/01_planning/index.ts index 5b3c33a..f8f4e47 100644 --- a/src/core/transformers/01_planning/index.ts +++ b/src/core/transformers/01_planning/index.ts @@ -2,11 +2,15 @@ import { tf } from "../../common/transform"; import negotiate from "./00_negotiate"; import measure from "./01_measure"; import flood from "./02_flood"; -import establish from "./03_establish"; -import pearl from "./04_pearl"; +import anchor from "./03_anchor"; +import mod from "./04_mod"; +import establish from "./05_establish"; +import pearl from "./06_pearl"; export const PLANNING_TF = tf(negotiate) .then(measure) .then(flood) + .then(anchor) + .then(mod) .then(establish) .then(pearl); diff --git a/src/core/transformers/01_planning/utils.ts b/src/core/transformers/01_planning/utils.ts new file mode 100644 index 0000000..fccfc8f --- /dev/null +++ b/src/core/transformers/01_planning/utils.ts @@ -0,0 +1,29 @@ +import { AnyMetadata } from "../../architects"; +import { Architect } from "../../models/architect"; +import { BaseCavern } from "../../models/cavern"; +import { Plan } from "../../models/plan"; + +export type WithPlanType< + CavernT extends BaseCavern, + PlanT extends Partial>, +> = Omit & { + readonly plans: readonly PlanT[]; +}; + +export default function encourageDisable>( + architects: readonly T[], + cavern: BaseCavern, +): T[] { + return architects + .filter((a) => cavern.context.architects?.[a.name] !== "disable") + .map((a) => { + if (cavern.context.architects?.[a.name] === "encourage") { + const r = { ...a }; + r.caveBid = (args) => !!a.caveBid?.(args) && 999999; + r.hallBid = (args) => !!a.hallBid?.(args) && 999999; + r.anchorBid = (args) => !!a.anchorBid?.(args) && 999999; + return r; + } + return a; + }); +} diff --git a/src/core/transformers/02_masonry/03_grout.ts b/src/core/transformers/02_masonry/03_grout.ts index 11658f3..3215a79 100644 --- a/src/core/transformers/02_masonry/03_grout.ts +++ b/src/core/transformers/02_masonry/03_grout.ts @@ -10,7 +10,7 @@ import { WEST, } from "../../common/geometry"; import { Grid } from "../../common/grid"; -import { Tile } from "../../models/tiles"; +import { Hardness, Tile } from "../../models/tiles"; import { RoughPlasticCavern } from "./01_rough"; const HOLE_MAP = [ @@ -43,10 +43,10 @@ export default function grout(cavern: RoughPlasticCavern): RoughPlasticCavern { cavern.tiles.forEach((t, x, y) => { if ( // If the point is surrounded by hard or solid rock, make it hard rock - t !== Tile.SOLID_ROCK && + t.hardness < Hardness.SOLID && !NSEW.some(([ox, oy]) => { const ot = tiles.get(x + ox, y + oy); - return ot && ot !== Tile.HARD_ROCK && ot !== Tile.SOLID_ROCK; + return ot && ot.hardness < Hardness.HARD; }) ) { tiles.set(x, y, Tile.HARD_ROCK); diff --git a/src/core/transformers/02_masonry/04_sand.ts b/src/core/transformers/02_masonry/04_sand.ts index 472537a..5507ecb 100644 --- a/src/core/transformers/02_masonry/04_sand.ts +++ b/src/core/transformers/02_masonry/04_sand.ts @@ -1,5 +1,5 @@ import { NSEW } from "../../common/geometry"; -import { Tile } from "../../models/tiles"; +import { Hardness, Tile } from "../../models/tiles"; import { RoughPlasticCavern } from "./01_rough"; export default function sand(cavern: RoughPlasticCavern): RoughPlasticCavern { @@ -10,7 +10,7 @@ export default function sand(cavern: RoughPlasticCavern): RoughPlasticCavern { t === Tile.HARD_ROCK && !NSEW.some(([ox, oy]) => { const ot = tiles.get(x + ox, y + oy); - return !ot || ot === Tile.HARD_ROCK || ot === Tile.SOLID_ROCK; + return !ot || ot.hardness >= Hardness.HARD; }) ) { tiles.set(x, y, Tile.LOOSE_ROCK); diff --git a/src/core/transformers/04_ephemera/00_aerate.ts b/src/core/transformers/04_ephemera/00_aerate.ts index 1e7d46e..84960be 100644 --- a/src/core/transformers/04_ephemera/00_aerate.ts +++ b/src/core/transformers/04_ephemera/00_aerate.ts @@ -7,7 +7,7 @@ import { TOOL_STORE, } from "../../models/building"; import { EntityPosition } from "../../models/position"; -import { Tile } from "../../models/tiles"; +import { Hardness, Tile } from "../../models/tiles"; import { PopulatedCavern } from "../03_plastic/04_populate"; export type AeratedCavern = PopulatedCavern & { @@ -118,16 +118,16 @@ export default function aerate(cavern: PopulatedCavern): AeratedCavern { const origin = getOrigin(cavern); function drillTiming(t: Tile) { - if (t === Tile.DIRT) { - return TIMING.DRILL_DIRT; + switch (t.hardness) { + case Hardness.DIRT: + return TIMING.DRILL_DIRT; + case Hardness.LOOSE: + return TIMING.DRILL_LOOSE_ROCK; + case Hardness.SEAM: + return TIMING.DRILL_SEAM; + default: + return undefined; } - if (t === Tile.LOOSE_ROCK) { - return TIMING.DRILL_LOOSE_ROCK; - } - if (t === Tile.CRYSTAL_SEAM || t === Tile.ORE_SEAM) { - return TIMING.DRILL_SEAM; - } - return undefined; } { @@ -147,7 +147,7 @@ export default function aerate(cavern: PopulatedCavern): AeratedCavern { walkQueue.push([x, y]); } else if (drillTiming(t)) { drillQueue.push([x, y]); - } else if (t === Tile.HARD_ROCK) { + } else if (t.hardness === Hardness.HARD) { dynamiteQueue.push([x, y]); } } diff --git a/src/core/transformers/04_ephemera/02_enscribe.ts b/src/core/transformers/04_ephemera/02_enscribe.ts index 6a35bae..47fae25 100644 --- a/src/core/transformers/04_ephemera/02_enscribe.ts +++ b/src/core/transformers/04_ephemera/02_enscribe.ts @@ -1,4 +1,6 @@ import { CavernContext } from "../../common"; +import { PartialCavernContext } from "../../common/context"; +import { OVERRIDE_SUFFIXES } from "../../lore/graphs/names"; import { Lore } from "../../lore/lore"; import { AdjuredCavern } from "./01_adjure"; @@ -13,51 +15,12 @@ export type EnscribedCavern = AdjuredCavern & { }; }; -const OVERRIDE_SUFFIXES = [ - "Ablated", - "Boosted", - "Chief's Version", - "Chrome Edition", - "Diamond Edition", - "Director's Cut", - "Emerald Edition", - "Enhanced", - "Extended", - "Gold Edition", - "HD", - "HD 1.5 Remix", - "Millenium Edition", - "Planet U Remix", - "Platinum Edition", - "Rebirthed", - "Reborn", - "Recoded", - "Rectified", - "Recycled", - "Redux", - "Reimagined", - "Reloaded", - "Remixed", - "Ressurection", - "Retooled", - "Revenant", - "Revolutions", - "Ruby Edition", - "Sapphire Edition", - "Silver Edition", - "Special Edition", - "Ungrounded", - "Unleashed", - "Unlocked", - "Unobtaininum Edition", - "Unplugged", - "Uranium Edition", -]; - -function overrideSuffix(context: CavernContext) { - const s = [...context.overrides] +function overrideSuffix(initialContext: PartialCavernContext) { + const overrides: Partial = { ...initialContext }; + delete overrides.seed; + const s = Object.keys(overrides) .sort() - .map((k) => `${k}:${JSON.stringify(context[k])}`) + .map((k) => `${k}:${JSON.stringify(overrides[k as keyof CavernContext])}`) .join(","); // https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript let v = 0; @@ -70,7 +33,7 @@ function overrideSuffix(context: CavernContext) { } export default function enscribe(cavern: AdjuredCavern): EnscribedCavern { - const hasOverrides = cavern.context.overrides.length; + const hasOverrides = Object.keys(cavern.initialContext).length > 1; const fileName = (() => { const seed = cavern.context.seed.toString(16).padStart(8, "0"); @@ -91,7 +54,7 @@ export default function enscribe(cavern: AdjuredCavern): EnscribedCavern { cavern.dice, ); const levelName = hasOverrides - ? `${name.text} (${overrideSuffix(cavern.context)})` + ? `${name.text} (${overrideSuffix(cavern.initialContext)})` : name.text; const briefing = { intro: `${premise.text}\n\n${orders.text}`, diff --git a/src/core/transformers/README.md b/src/core/transformers/README.md index 11f77f4..5818e68 100644 --- a/src/core/transformers/README.md +++ b/src/core/transformers/README.md @@ -19,7 +19,9 @@ Create "plans" for the baseplates and paths that will determine how the space wi 1. _Negotiate_: Assign baseplates and paths to new plans. Some paths connecting two adjacent baseplates will become caves. All remaining paths become halls and all remaining baseplates become caves. 1. _Measure_: Determine size information for the plans. 1. _Flood_: Choose which plans will have water, lava, and erosion. -1. _Establish_: Choose which plan will be the spawn, walk through adjacent plans, assign architects, and determine other information based on distance from spawn. +1. _Anchor_: Choose which plan will be the anchor and assign an architect to it. +1. _Mod_: The anchor architect has a chance to modify the cavern in any way. +1. _Establish_: Perform a breadth-first search of all plans, starting with the anchor. Assign architects and and determine other information based on distance from spawn. 1. _Pearl_: Create "pearls" that determine exactly where plans will go. This is the step that ensures the caves and halls will be more "natural" shapes. # III. Masonry diff --git a/src/core/transformers/index.ts b/src/core/transformers/index.ts index c7c9064..dcd0a3a 100644 --- a/src/core/transformers/index.ts +++ b/src/core/transformers/index.ts @@ -1,10 +1,14 @@ +import { tf } from "../common/transform"; import { OUTLINE_TF } from "./00_outlines"; import { PLANNING_TF } from "./01_planning"; import { MASONRY_TF } from "./02_masonry"; import { PLASTIC_TF } from "./03_plastic"; import { EPHEMERA_TF } from "./04_ephemera"; +import init from "./init"; -export const CAVERN_TF = OUTLINE_TF.chain(PLANNING_TF) +export const CAVERN_TF = tf(init) + .chain(OUTLINE_TF) + .chain(PLANNING_TF) .chain(MASONRY_TF) .chain(PLASTIC_TF) .chain(EPHEMERA_TF); diff --git a/src/core/transformers/init.ts b/src/core/transformers/init.ts new file mode 100644 index 0000000..884b9a1 --- /dev/null +++ b/src/core/transformers/init.ts @@ -0,0 +1,10 @@ +import { DiceBox, inferContextDefaults } from "../common"; +import { BaseCavern } from "../models/cavern"; + +export default function init( + cavern: Pick, +): BaseCavern { + const context = inferContextDefaults(cavern.initialContext); + const dice = new DiceBox(context.seed); + return { ...cavern, context, dice }; +} diff --git a/src/webui/App.tsx b/src/webui/App.tsx index 9004ba2..a1c96cc 100644 --- a/src/webui/App.tsx +++ b/src/webui/App.tsx @@ -1,22 +1,19 @@ -import React, { - CSSProperties, - useCallback, - useEffect, - useReducer, - useState, -} from "react"; - -import { CavernContext, DiceBox } from "../core/common"; -import { CavernContextInput } from "./components/context_editor"; +import React, { CSSProperties, useCallback, useEffect, useState } from "react"; + +import { + CavernContextInput, + getInitialSeed, +} from "./components/context_editor"; import { Cavern } from "../core/models/cavern"; import CavernPreview, { MapOverlay } from "./components/map_preview"; import { CAVERN_TF } from "../core/transformers"; -import { TransformResult } from "../core/common/transform"; import LorePreview from "./components/popovers/lore"; import About from "./components/popovers/about"; import styles from "./App.module.scss"; import ErrorPreview from "./components/popovers/error"; import { filterTruthy } from "../core/common/utils"; +import { PartialCavernContext } from "../core/common/context"; +import { TfResult } from "../core/common/transform"; const MAP_OVERLAY_BUTTONS: readonly { of: MapOverlay; @@ -42,34 +39,30 @@ function getDownloadLink(serializedData: string) { return `data:text/plain;charset=utf-8,${encodeURIComponent(serializedData)}`; } -type State = { - cavern?: Cavern; - name?: string; - progress?: number; - next?: () => TransformResult; +function getStateForInitialContext(initialContext: PartialCavernContext) { + return CAVERN_TF.first({ initialContext }); +} + +type State = TfResult & { error?: Error; }; function App() { - const [state, dispatchState] = useReducer( - (was: State, action: State | { context: CavernContext }) => { - if ("context" in action) { - const cavern = { - context: action.context, - dice: new DiceBox(action.context.seed), - }; - const r = CAVERN_TF.first(cavern); - return { - cavern: r.result, - name: r.name, - next: r.next || undefined, - } as State; - } else if ("error" in action) { - return { cavern: was.cavern, ...action }; - } - return action; + const [state, setState] = useState(() => + getStateForInitialContext({ + seed: getInitialSeed(), + }), + ); + + const setInitialContext = useCallback( + (arg: React.SetStateAction) => { + setState((was) => + getStateForInitialContext( + typeof arg === "function" ? arg(was.result.initialContext) : arg, + ), + ); }, - {}, + [], ); const [autoGenerate, setAutoGenerate] = useState(true); @@ -77,7 +70,7 @@ function App() { const [showOutlines, setShowOutlines] = useState(false); const [showPearls, setShowPearls] = useState(false); - const biome = state?.cavern?.context.biome; + const biome = state.result.context?.biome; function playPause() { if (autoGenerate) { @@ -89,27 +82,18 @@ function App() { const step = useCallback(() => { try { - const r = state.next!(); - dispatchState({ - cavern: r.result, - name: r.name, - progress: r.progress, - next: r.next || undefined, - }); - } catch (error: unknown) { - console.error(error); - if (error instanceof Error) { - dispatchState({ error }); - } + setState(state.next!()); + } catch (e: unknown) { + console.error(e); + const error = e instanceof Error ? e : new Error("unknown error"); + setState({ ...state, next: null, progress: 0, error }); } }, [state]); - const reset = useCallback(() => { + const reset = () => { setAutoGenerate(false); - if (state.cavern) { - dispatchState({ context: state.cavern.context }); - } - }, [state]); + setState((was) => getStateForInitialContext(was.result.initialContext)); + }; useEffect(() => { if (state.next && autoGenerate) { @@ -118,16 +102,20 @@ function App() { }, [autoGenerate, state, step]); useEffect(() => { - (window as any).cavern = state.cavern; + (window as any).cavern = state.result; }, [state]); const isLoading = - (autoGenerate && !state.cavern?.serialized) || mapOverlay === "about"; + (autoGenerate && !state.result.serialized) || mapOverlay === "about"; return (
- +
- {state.cavern && ( + {state.result && ( )} {mapOverlay === "about" && } - {mapOverlay === "lore" && } + {mapOverlay === "lore" && } {state.error && ( - + )} {!autoGenerate && state.name && (
{state.name}
@@ -174,11 +166,11 @@ function App() { ) : ( )} - {state.cavern?.serialized ? ( + {state.result.serialized ? ( download @@ -209,7 +201,7 @@ function App() { className={ mapOverlay === of ? styles.active - : enabled(state.cavern) + : enabled(state.result) ? styles.inactive : styles.disabled } diff --git a/src/webui/components/context_editor/architects.tsx b/src/webui/components/context_editor/architects.tsx index 2254ee3..026bfb3 100644 --- a/src/webui/components/context_editor/architects.tsx +++ b/src/webui/components/context_editor/architects.tsx @@ -3,12 +3,12 @@ import { ARCHITECTS } from "../../../core/architects"; import styles from "./style.module.scss"; import React from "react"; -export const ArchitectsInput = ({ update, context }: UpdateData) => { +export const ArchitectsInput = ({ update, initialContext }: UpdateData) => { function updateArchitects( key: string, value: "encourage" | "disable" | undefined, ) { - const r = { ...context.architects }; + const r = { ...initialContext.architects }; if (value === undefined) { if (key in r) { delete r[key]; @@ -25,38 +25,36 @@ export const ArchitectsInput = ({ update, context }: UpdateData) => { update({ architects: r }); } - return [...ARCHITECTS] - .sort((a, b) => a.name.localeCompare(b.name)) - .map((a) => { - const state = context.architects?.[a.name]; - return ( - -

{a.name}

-
- - -
-
- ); - }); + return [...ARCHITECTS].map((a) => { + const state = initialContext.architects?.[a.name]; + return ( + +

{a.name}

+
+ + +
+
+ ); + }); }; diff --git a/src/webui/components/context_editor/controls.tsx b/src/webui/components/context_editor/controls.tsx index dd10ab9..6f58299 100644 --- a/src/webui/components/context_editor/controls.tsx +++ b/src/webui/components/context_editor/controls.tsx @@ -2,11 +2,12 @@ import React, { CSSProperties } from "react"; import styles from "./style.module.scss"; import { CavernContext, Curve } from "../../../core/common"; import { radsToDegrees } from "../../../core/common/geometry"; +import { PartialCavernContext } from "../../../core/common/context"; export type UpdateData = { update: React.Dispatch>; - context: Partial; - contextWithDefaults: CavernContext | undefined; + initialContext: PartialCavernContext; + context: CavernContext; }; type KeysMatching = { [K in keyof T]-?: T[K] extends V ? K : never; @@ -16,8 +17,8 @@ export const Choice = ({ of, choices, update, + initialContext, context, - contextWithDefaults, }: { of: K; choices: CavernContext[K][]; @@ -27,11 +28,11 @@ export const Choice = ({
{choices.map((choice) => { const classes = [styles.choice]; - const selected = context[of] === choice; + const selected = initialContext[of] === choice; if (selected) { classes.push(styles.override); } - const active = contextWithDefaults?.[of] === choice; + const active = context?.[of] === choice; classes.push(active ? styles.active : styles.inactive); return ( @@ -56,8 +57,8 @@ export const CurveSliders = ({ max, step, update, + initialContext, context, - contextWithDefaults, }: { of: KeysMatching; min: number; @@ -66,7 +67,7 @@ export const CurveSliders = ({ } & UpdateData) => { function updateCurve(key: "base" | "hops" | "order", value: number) { update({ - [of]: { ...contextWithDefaults?.[of], ...context?.[of], [key]: value }, + [of]: { ...context[of], [key]: value }, }); } @@ -74,14 +75,13 @@ export const CurveSliders = ({ <>

{of}:

- {contextWithDefaults?.[of]?.base?.toFixed(2)},{" "} - {contextWithDefaults?.[of]?.hops?.toFixed(2)},{" "} - {contextWithDefaults?.[of]?.order?.toFixed(2)} + {context[of].base.toFixed(2)}, {context[of].hops.toFixed(2)},{" "} + {context[of].order.toFixed(2)}

{(["base", "hops", "order"] as const).map((key) => { - const value = contextWithDefaults?.[of]?.[key] ?? min; + const value = context[of][key]; return ( - {of in context ? ( + {of in initialContext ? ( + ) : ( +
)}
diff --git a/src/webui/components/context_editor/index.tsx b/src/webui/components/context_editor/index.tsx index 69fa74b..e6d5c21 100644 --- a/src/webui/components/context_editor/index.tsx +++ b/src/webui/components/context_editor/index.tsx @@ -1,12 +1,17 @@ -import React, { useEffect, useReducer, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { CavernContext, inferContextDefaults } from "../../../core/common"; import { MAX_PLUS_ONE } from "../../../core/common/prng"; import styles from "./style.module.scss"; import { Choice, CurveSliders, Slider } from "./controls"; import { ArchitectsInput } from "./architects"; +import { PartialCavernContext } from "../../../core/common/context"; const INITIAL_SEED = Date.now() % MAX_PLUS_ONE; +export function getInitialSeed() { + return parseSeed(window.location.hash) ?? INITIAL_SEED; +} + function parseSeed(v: string) { const s = v.replace(/[^0-9a-fA-F]+/g, ""); const seed = s === "" ? -1 : parseInt(s, 16); @@ -29,35 +34,52 @@ const expectedTotalPlans = (contextWithDefaults: CavernContext) => { return caves + spanHalls + auxHalls; }; -type PartialContext = Partial & Pick; - export function CavernContextInput({ - dispatchState, + initialContext, + context, + setInitialContext, +}: { + initialContext: PartialCavernContext; + context: CavernContext | undefined; + setInitialContext: React.Dispatch>; +}) { + return ( + + ); +} + +function CavernContextInputInner({ + initialContext, + context, + setInitialContext, }: { - dispatchState: (args: { context: CavernContext }) => void; + initialContext: PartialCavernContext; + context: CavernContext; + setInitialContext: React.Dispatch>; }) { const [showAdvanced, setShowAdvanced] = useState(false); - const [context, update] = useReducer( - ( - was: PartialContext, - args: - | { [K in keyof CavernContext]?: CavernContext[K] | undefined } - | "reset", - ): PartialContext => { - if (args === "reset") { - return { seed: was.seed }; - } - const r = { ...was, ...args }; - for (const key of Object.keys(r) as (keyof typeof r)[]) { - if (r[key] === undefined) { - if (key in r) { - delete r[key]; + const resetContext = useCallback( + () => setInitialContext((was) => ({ seed: was.seed })), + [setInitialContext], + ); + const update = useCallback( + (args: Partial) => + setInitialContext((was: PartialCavernContext) => { + const r = { ...was, ...args }; + for (const key of Object.keys(r) as (keyof typeof r)[]) { + if (r[key] === undefined) { + if (key in r) { + delete r[key]; + } } } - } - return r; - }, - { seed: parseSeed(window.location.hash) ?? INITIAL_SEED }, + return r; + }), + [setInitialContext], ); useEffect(() => { @@ -71,18 +93,13 @@ export function CavernContextInput({ return () => { window.removeEventListener("hashchange", fn); }; - }, []); + }, [update]); useEffect(() => { - window.location.hash = unparseSeed(context.seed, false); - }, [context.seed]); + window.location.hash = unparseSeed(initialContext.seed, false); + }, [initialContext.seed]); - useEffect( - () => dispatchState({ context: inferContextDefaults(context) }), - [context, dispatchState], - ); - const contextWithDefaults = inferContextDefaults(context); - const rest = { update, context, contextWithDefaults }; + const rest = { update, initialContext, context }; return (
@@ -90,7 +107,7 @@ export function CavernContextInput({ { const seed = parseSeed(ev.target.value); if (seed !== undefined) { @@ -110,7 +127,7 @@ export function CavernContextInput({
) : ( @@ -164,13 +178,13 @@ export function CavernContextInput({
@@ -275,8 +289,8 @@ export function CavernContextInput({ max={1} percent update={update} + initialContext={initialContext} context={context} - contextWithDefaults={contextWithDefaults} /> ))}
@@ -320,8 +334,8 @@ export function CavernContextInput({ max={1} percent update={update} + initialContext={initialContext} context={context} - contextWithDefaults={contextWithDefaults} /> ))}
diff --git a/src/webui/components/map_preview/index.tsx b/src/webui/components/map_preview/index.tsx index 1ddc9bb..9996e75 100644 --- a/src/webui/components/map_preview/index.tsx +++ b/src/webui/components/map_preview/index.tsx @@ -9,7 +9,7 @@ import { Cavern } from "../../../core/models/cavern"; import BaseplatePreview from "./baseplate"; import PathPreview from "./path"; import PearlPreview from "./pearl"; -import { PearledPlan } from "../../../core/transformers/01_planning/04_pearl"; +import { PearledPlan } from "../../../core/transformers/01_planning/06_pearl"; import TilesPreview from "./tiles"; import EntityPreview from "./entity"; import OpenCaveFlagPreview from "./open_cave_flag"; @@ -121,8 +121,12 @@ export default function CavernPreview({ default: } - const height = cavern.context.targetSize * 2 * 6; - const width = Math.max(height, cavern.context.targetSize * 6 + 600); + const targetSize = cavern.context?.targetSize; + if (!targetSize) { + return null; + } + const height = targetSize * 2 * 6; + const width = Math.max(height, targetSize * 6 + 600); return (
- {"architect" in plan && plan.architect.name} {plan.id} + {"architect" in plan && plan.architect?.name} {plan.id} ); @@ -191,7 +191,7 @@ export default function PlansPreview({ - {"architect" in plan && plan.architect.name} {plan.id} + {"architect" in plan && plan.architect?.name} {plan.id} @@ -199,12 +199,12 @@ export default function PlansPreview({ }); } let py = -Infinity; + const targetSize = cavern.context?.targetSize ?? 0; return plans.map((plan, i) => { const px = SCALE * planCoords[plan.id][0]; py = Math.max(SCALE * planCoords[plan.id][1], py + 4); - const lx = ((SCALE * cavern.context.targetSize) / 2 + 50) * sign; - const ly = - SCALE * cavern.context.targetSize * ((i + 1) / plans.length - 0.5); + const lx = ((SCALE * targetSize) / 2 + 50) * sign; + const ly = SCALE * targetSize * ((i + 1) / plans.length - 0.5); const bx = lx - Math.abs(py - ly) * 0.56 * sign; const d = filterTruthy([ `M ${lx + 25 * sign} ${ly}`, @@ -216,7 +216,7 @@ export default function PlansPreview({ - {"architect" in plan ? ( + {"architect" in plan && plan.architect?.name ? ( <> {plan.architect.name} {!plan.hops.length && "*"} {plan.id} diff --git a/src/webui/components/popovers/error.tsx b/src/webui/components/popovers/error.tsx index 296b2b6..518aee0 100644 --- a/src/webui/components/popovers/error.tsx +++ b/src/webui/components/popovers/error.tsx @@ -1,14 +1,17 @@ import React, { useState } from "react"; import styles from "./styles.module.scss"; import { CavernContext } from "../../../core/common"; +import { PartialCavernContext } from "../../../core/common/context"; const GITHUB_ISSUE = "https://github.com/charredUtensil/groundhog/issues/new"; export default function ErrorPreview({ error, + initialContext, context, }: { error: Error; + initialContext: PartialCavernContext; context: CavernContext | undefined; }) { const [show, setShow] = useState(true); @@ -19,10 +22,9 @@ export default function ErrorPreview({ const debugInfo = [ `error: ${error.message}`, `groundHog version: ${process.env.REACT_APP_VERSION}`, - `seed: ${context?.seed.toString(16).padStart(8, "0").toUpperCase()}`, - `overrides: ${context?.overrides.join(", ") || "[none]"}`, - `stack: ${error.stack}`, + `initial context: ${JSON.stringify(initialContext)}`, `context: ${JSON.stringify(context)}`, + `stack: ${error.stack}`, ].join("\n"); const bugLink = `${GITHUB_ISSUE}?body=${encodeURIComponent(`Add any relevant info here:\n\n\n${debugInfo}`)}`; return ( @@ -42,7 +44,6 @@ export default function ErrorPreview({
  • seed: {context?.seed.toString(16).padStart(8, "0").toUpperCase()}
  • -
  • overrides: {context?.overrides.join(", ") || "[none]"}
  • Report issue on GitHub (Requires GitHub account) @@ -70,6 +71,8 @@ export default function ErrorPreview({

    {error.stack}

    )} +

    Initial Context

    +

    {JSON.stringify(initialContext)}

    {context && ( <>

    Context