Skip to content

Commit

Permalink
blackout and mob farm updates
Browse files Browse the repository at this point in the history
  • Loading branch information
charredUtensil committed Nov 10, 2024
1 parent 701aee3 commit e5c59cd
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 118 deletions.
20 changes: 16 additions & 4 deletions src/core/architects/blackout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { inferContextDefaults } from "../common";
import { BLACKOUT_START, BLACKOUT_END } from "../lore/graphs/events";
import { Architect } from "../models/architect";
import { Architect, BaseMetadata } from "../models/architect";
import { DefaultSpawnArchitect, PartialArchitect } from "./default";
import { mkRough, Rough } from "./utils/rough";
import { eventChain, mkVars, scriptFragment } from "./utils/script";
Expand All @@ -11,8 +12,19 @@ const MIN_AIR = 500;
const RESET_SECONDS = 120;
const RESET_END = 999;

const BASE: PartialArchitect<undefined> = {
const METADATA = {tag:'blackout'} as const satisfies BaseMetadata;

const BASE: PartialArchitect<typeof METADATA> = {
...DefaultSpawnArchitect,
prime: () => METADATA,
mod(cavern) {
const context = inferContextDefaults({
caveHasRechargeSeamChance: 0.15,
hallHasRechargeSeamChance: 0.15,
...cavern.initialContext
})
return {...cavern, context}
},
script({cavern, plan, sh}) {
const v = mkVars(`p${plan.id}Bo`, [
'crystalBank',
Expand Down Expand Up @@ -98,7 +110,7 @@ const BLACKOUT = [
plan.lakeSize >= 3 &&
plan.pearlRadius > 0 &&
!cavern.context.hasSlugs &&
0.4,
0.03,
},
] as const satisfies readonly Architect<undefined>[];
] as const satisfies readonly Architect<typeof METADATA>[];
export default BLACKOUT;
2 changes: 2 additions & 0 deletions src/core/architects/build_and_power.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@ export const BUILD_AND_POWER = [
plan.pearlRadius > 2 &&
plan.pearlRadius < 10 &&
plan.path.baseplates.length === 1 &&
// Incompatible with fchq or mob farm
!(amd?.tag === "hq" && amd.fixedComplete) &&
!(amd?.tag === "mobFarm") &&
intersectsOnly(plans, plan, null) &&
hops.length > 5 &&
!hops.some((h) => {
Expand Down
6 changes: 4 additions & 2 deletions src/core/architects/established_hq/lost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ const LOST = [
...LOST_BASE,
prime: getPrime(15, false),
placeBuildings: getPlaceBuildings({}),
caveBid: ({ plan, hops, plans }) =>
caveBid: ({ cavern, plan, hops, plans }) =>
!plan.fluid &&
plan.pearlRadius > 5 &&
hops.length <= MAX_HOPS &&
plans[cavern.anchor]?.metadata?.tag !== 'mobFarm' &&
!hops.some((id) => plans[id].fluid) &&
!plans.some((p) => p.metadata?.tag === "hq") &&
0.5,
Expand All @@ -90,10 +91,11 @@ const LOST = [
prime: getPrime(15, true),
placeBuildings: getPlaceBuildings({ from: 3 }),
placeLandslides: (args) => placeLandslides({ min: 15, max: 100 }, args),
caveBid: ({ plan, hops, plans }) =>
caveBid: ({ cavern, plan, hops, plans }) =>
!plan.fluid &&
plan.pearlRadius > 6 &&
hops.length <= MAX_HOPS &&
plans[cavern.anchor]?.metadata?.tag !== 'mobFarm' &&
!plans.some((p) => p.metadata?.tag === "hq") &&
(plans[hops[0]].metadata?.tag === "nomads" ? 5 : 0.5),
},
Expand Down
5 changes: 3 additions & 2 deletions src/core/architects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ import SLUGS from "./slugs";
import THIN_HALL from "./thin_hall";
import TREASURE from "./treasure";
import BLACKOUT from "./blackout";
import MOB_FARM from "./mob_farm";
import MOB_FARM, { MobFarmMetadata } from "./mob_farm";

export type AnyMetadata =
| undefined
| BuildAndPowerMetadata
| HqMetadata
| LostMinersMetadata
| NomadsMetadata
| { tag: "mobFarm" | "seismic" | "slugNest" | "treasure" };
| MobFarmMetadata
| { tag: 'blackout' | "seismic" | "slugNest" | "treasure" };

export const ARCHITECTS = [
...BLACKOUT,
Expand Down
18 changes: 11 additions & 7 deletions src/core/architects/lost_miners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SMALL_DIGGER,
SMALL_TRANSPORT_TRUCK,
TUNNEL_SCOUT,
SMLC,
} from "../models/vehicle";
import { DiscoveredCavern } from "../transformers/03_plastic/01_discover";
import { StrataformedCavern } from "../transformers/03_plastic/02_strataform";
Expand All @@ -38,6 +39,7 @@ import {
FOUND_LOST_MINERS,
} from "../lore/graphs/events";
import { gObjectives } from "./utils/objectives";
import { filterTruthy } from "../common/utils";

export type LostMinersMetadata = {
readonly tag: "lostMiners";
Expand Down Expand Up @@ -103,14 +105,16 @@ function placeBreadcrumbVehicles(
): Vehicle[] {
const tile = cavern.tiles.get(x, y);
const fluid = tile === Tile.LAVA || tile === Tile.WATER ? tile : null;
const template = rng.weightedChoice<VehicleTemplate | null>([
{ item: HOVER_SCOUT, bid: fluid ? 0 : 2 },
{ item: SMALL_DIGGER, bid: fluid ? 0 : 0.5 },
{ item: SMALL_TRANSPORT_TRUCK, bid: fluid ? 0 : 0.75 },
{ item: RAPID_RIDER, bid: fluid === Tile.WATER ? 1 : 0 },
{ item: TUNNEL_SCOUT, bid: 0.25 },
const isMobFarm = cavern.plans[cavern.anchor].metadata?.tag === 'mobFarm';
const template = rng.weightedChoice<VehicleTemplate | null>(filterTruthy([
!fluid && !isMobFarm && { item: HOVER_SCOUT, bid: 2 },
!fluid && { item: SMALL_DIGGER, bid: 0.5 },
!fluid && { item: SMALL_TRANSPORT_TRUCK, bid: 0.75 },
!fluid && { item: SMLC, bid: 0.05},
!isMobFarm && fluid === Tile.WATER && { item: RAPID_RIDER, bid: 1 },
!isMobFarm && { item: TUNNEL_SCOUT, bid: 0.25 },
{ item: null, bid: 0.0025 },
]);
]));
if (template) {
return [
vehicleFactory.create({
Expand Down
101 changes: 77 additions & 24 deletions src/core/architects/mob_farm.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Architect, BaseMetadata } from "../models/architect";
import { Architect } from "../models/architect";
import { Tile } from "../models/tiles";
import { DefaultSpawnArchitect, PartialArchitect } from "./default";
import { mkRough, Rough, weightedSprinkle } from "./utils/rough";
import { monsterSpawnScript } from "./utils/creature_spawners";
import { getBuildings } from "./utils/buildings";
import { DOCKS, SUPER_TELEPORT, TOOL_STORE } from "../models/building";
import { DOCKS, MINING_LASER, SUPER_TELEPORT, TOOL_STORE } from "../models/building";
import { position, randomlyInTile } from "../models/position";
import { asXY, closestTo, NSEW, offsetBy, Point } from "../common/geometry";
import { CARGO_CARRIER, CHROME_CRUSHER, GRANITE_GRINDER, HOVER_SCOUT, LMLC, LOADER_DOZER, RAPID_RIDER, TUNNEL_SCOUT, TUNNEL_TRANSPORT } from "../models/vehicle";
import { CARGO_CARRIER, CHROME_CRUSHER, GRANITE_GRINDER, HOVER_SCOUT, LMLC, LOADER_DOZER, RAPID_RIDER, SMLC, TUNNEL_SCOUT, TUNNEL_TRANSPORT } from "../models/vehicle";
import { getPlaceRechargeSeams, sprinkleCrystals } from "./utils/resources";
import { inferContextDefaults } from "../common";
import { scriptFragment } from "./utils/script";
import { mkVars, scriptFragment } from "./utils/script";
import { HINT_SELECT_LASER_GROUP, MOB_FARM_NO_LONGER_BLOCKING } from "../lore/graphs/events";
import { gObjectives } from "./utils/objectives";
import { PreprogrammedCavern } from "../transformers/04_ephemera/03_preprogram";

const BANLIST = [
DOCKS,
Expand All @@ -21,27 +24,37 @@ const BANLIST = [
LOADER_DOZER,
GRANITE_GRINDER,
CARGO_CARRIER,
LMLC,
CHROME_CRUSHER,
TUNNEL_TRANSPORT,
] as const;

const METADATA = {
export type MobFarmMetadata = {
tag: "mobFarm",
} as const satisfies BaseMetadata;
hoardSize: number,
};

function totalAccessibleWalls({aerationLog, tiles}: PreprogrammedCavern) {
let result = 0;
aerationLog?.forEach((_, x, y) => tiles.get(x, y)?.isWall && result++);
return result;
}

const BASE: PartialArchitect<typeof METADATA> = {
const BASE: PartialArchitect<MobFarmMetadata> = {
...DefaultSpawnArchitect,
prime: () => METADATA,
prime: ({cavern, plan}) => ({tag: "mobFarm", hoardSize: cavern.dice.prime(plan.id).betaInt({a: 4, b: 4, min: 170, max: 230})}),
mod(cavern) {
const context = inferContextDefaults({
caveCrystalRichness: { base: -0.16, hops: 0.32, order: 0.32 },
hallCrystalRichness: { base: 0, hops: 0, order: 0 },
caveCrystalSeamBias: 0.7,
globalHostilesCap: 10,
...cavern.initialContext
})
return {...cavern, context}
},
crystalsToPlace: () => 200,
crystalsFromMetadata: () => 8,
crystalsToPlace: ({ plan }) => Math.max(plan.crystalRichness * plan.perimeter, 9),
crystalsFromMetadata: (metadata) => 4 + LMLC.crystals + metadata.hoardSize,
placeRechargeSeam: getPlaceRechargeSeams(3),
placeBuildings(args) {
const [toolStore] = getBuildings({
Expand All @@ -64,11 +77,12 @@ const BASE: PartialArchitect<typeof METADATA> = {
cameraPosition: position({
...asXY(args.plan.innerPearl[0][0]),
aimedAt: [toolStore.x, toolStore.y],
pitch: Math.PI / 8,
pitch: Math.PI / 3,
})
}
},
placeCrystals(args) {
sprinkleCrystals(args);
const tiles = args.plan.innerPearl.flatMap((ly, i) => i <= 2 ? ly : []).filter(pos => {
const t = args.tiles.get(...pos);
return t && !t.isWall && !t.isFluid;
Expand All @@ -77,40 +91,81 @@ const BASE: PartialArchitect<typeof METADATA> = {
sprinkleCrystals(args, {
getRandomTile: () => rng.betaChoice(tiles, {a: 1, b: 2.5}),
seamBias: 0,
count: args.plan.metadata.hoardSize,
});
},
placeSlugHoles() {},
placeLandslides() {},
placeEntities({cavern, plan, vehicleFactory}) {
placeEntities({cavern, plan, minerFactory, vehicleFactory}) {
const rng = cavern.dice.placeEntities(plan.id);
const ts = cavern.buildings.find(b => cavern.pearlInnerDex.get(...b.foundation[0])?.[plan.id])!;
const tiles = NSEW.map(oPos => offsetBy(ts.foundation[0], oPos)).filter(pos => {
const t = cavern.tiles.get(...pos);
return t && !t.isWall && !t.isFluid;
})
const pos = randomlyInTile({...asXY(rng.uniformChoice(tiles)), rng});
const driver = minerFactory.create({
loadout: ['Drill', 'JobDriver', 'JobEngineer'],
planId: plan.id,
...pos,
})
const lmlc = vehicleFactory.create({
template: LMLC,
upgrades: ['UpLaser'],
planId: plan.id,
...randomlyInTile({...asXY(rng.uniformChoice(tiles)), rng}),
driverId: driver.id,
...pos,
});
return {
vehicles: [lmlc]
miners: [driver],
vehicles: [lmlc],
}
},
objectives({cavern}) {
const crystals = cavern.plans[cavern.anchor].crystals * 0.75
const crystals = cavern.plans[cavern.anchor].crystals * 0.6;
return {
crystals: Math.floor(crystals / 5) * 5, sufficient: true
};
},
scriptGlobals({cavern, sh}) {
script({cavern, plan, sh}) {
const v = mkVars(`p${plan.id}MF`, ['hintGroup', 'msgHintGroup', 'msgNotBlocking'])
const rng = cavern.dice.script(plan.id)
return scriptFragment(
'# Globals: Mob Farm',
sh.trigger(
'if(time:0)',
...BANLIST.map(t => `disable:${t.id};` satisfies `${string};`)
),
cavern.objectives.variables.length > 0 && scriptFragment(
sh.declareString(v.msgNotBlocking, {rng, pg: MOB_FARM_NO_LONGER_BLOCKING}),
sh.trigger(
// There's a good chance any further objectives are softlocked by the
// inability to cross lakes and rivers - so unlock them.
`if(crystals>=${cavern.objectives.crystals})`,
`wait:5;`,
...BANLIST.map(t => `enable:${t.id};` satisfies `${string};`),
`((${gObjectives.won}==0))msg:${v.msgNotBlocking};`,
),
),
// Hint to tell players about control groups. This isn't super annoying
// under normal circumstances, but here it's almost a necessity that the
// player have their lasers bound to a single key.
sh.declareInt(v.hintGroup, 0),
sh.trigger(
`when(${MINING_LASER.id}.click)`,
`((${MINING_LASER.id}<2))return;`,
`${v.hintGroup}=1;`,
),
sh.trigger(
`when(${SMLC.id}.click)`,
`((${SMLC.id}<2))return;`,
`${v.hintGroup}=1;`,
),
sh.declareString(v.msgHintGroup, HINT_SELECT_LASER_GROUP),
sh.trigger(
`if(${v.hintGroup}>0)`,
`msg:${v.msgHintGroup};`,
),
);
},
monsterSpawnScript: (args) => monsterSpawnScript(args, {
Expand All @@ -120,11 +175,6 @@ const BASE: PartialArchitect<typeof METADATA> = {
})
};

// TODO:
// Give crystals - either as a seam or just give them initially
// Lore to indicate the point of the level
// Disable flying vehicles (all vehicles but STT?)

const MOB_FARM = [
{
name: "MobFarm.Water",
Expand All @@ -151,9 +201,12 @@ const MOB_FARM = [
plan.fluid === Tile.WATER &&
plan.pearlRadius > 6 &&
plan.path.baseplates.length === 1 &&
plan.intersects.some((_, i) => {
const p = cavern.plans[i];
return !p.fluid && p.lakeSize >= 6;
}) &&
plan.intersects.reduce((r, _, i) => cavern.plans[i].fluid ? r + 1 : r, 0) <= 1 &&
cavern.plans.reduce((r, p) => p.fluid ? r + 1 : r, 0) <= 5 &&
1,
2,
},
] as const satisfies readonly Architect<typeof METADATA>[];
] as const satisfies readonly Architect<MobFarmMetadata>[];
export default MOB_FARM;
29 changes: 14 additions & 15 deletions src/core/architects/utils/creature_spawners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { Plan } from "../../models/plan";
import { PreprogrammedCavern } from "../../transformers/04_ephemera/03_preprogram";
import { getDiscoveryPoint } from "./discovery";
import {
check,
eventChain,
EventChainLine,
mkVars,
scriptFragment,
ScriptHelper,
Expand Down Expand Up @@ -173,7 +173,6 @@ function creatureSpawnScript(
const v = mkVars(`p${plan.id}${opts.creature.inspectAbbrev}Sp`, [
"arm",
"doCooldown",
"doArm",
"doTrip",
"doSpawn",
"hoardTrip",
Expand Down Expand Up @@ -207,14 +206,18 @@ function creatureSpawnScript(
scriptFragment(
// Arm
sh.declareInt(v.arm, ArmState.DISARMED),
!opts.armEvent && `${getArmTrigger(cavern, plan)}[${v.doArm}]`,
eventChain(
opts.armEvent ?? v.doArm,
opts.initialCooldown &&
`wait:random(${opts.initialCooldown.min.toFixed(2)})(${opts.initialCooldown.max.toFixed(2)});`,
`${v.arm}=${ArmState.ARMED};`,
opts.tripOnArmed && `${v.doTrip};`,
),
(() => {
const body: EventChainLine[] = [
opts.initialCooldown &&
`wait:random(${opts.initialCooldown.min.toFixed(2)})(${opts.initialCooldown.max.toFixed(2)});`,
`${v.arm}=${ArmState.ARMED};`,
opts.tripOnArmed && `${v.doTrip};`,
];
if (opts.armEvent) {
return eventChain(opts.armEvent, ...body);
}
return sh.trigger(getArmTrigger(cavern, plan), ...body);
})(),

// Trip
...(opts.tripPoints ?? getTriggerPoints(cavern, plan)).map(
Expand All @@ -235,11 +238,7 @@ function creatureSpawnScript(
`((crystals<${opts.needCrystals.increment ? v.needCrystals : opts.needCrystals.base}))return;`,
cavern.context.globalHostilesCooldown > 0 &&
`((${gCreatures.globalCooldown}>0))return;`,
check(
`${v.arm}==${ArmState.ARMED}`,
`${v.arm}=${ArmState.FIRE}`,
opts.reArmMode !== "none" && opts.tripOnArmed === 'always' && `${v.arm}=${v.doCooldown}`,
),
`((${v.arm}==${ArmState.ARMED}))${v.arm}=${ArmState.FIRE};`,
),
`when(${v.arm}==${ArmState.FIRE})[${v.doSpawn}]`,
),
Expand Down
Loading

0 comments on commit e5c59cd

Please sign in to comment.