diff --git a/src/main.ts b/src/main.ts index 8925234..286a5c7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,7 @@ import { areaAt, createAreas, createHeat, effectiveHeat, speedAttention, stepHea import { ROAD_ATTENTION, routeAt, segmentAt } from './sim/roads'; import { baseAt, placeBases } from './sim/bases'; import { buildGraph } from './sim/routing'; -import { createIntel, observe, reveal, survey } from './sim/intel'; +import { createIntel, observe, reveal, revealHomeGround, survey } from './sim/intel'; import { applyMissionImpact, controlAt, @@ -293,6 +293,11 @@ async function boot() { heatProps.finished(segment.id); } } + } else { + // A fresh campaign starts knowing its own ground. You are a local with a + // car, not an amnesiac: the pocket your side holds is the part of the map + // you would obviously know, and the war is the part you would not. + revealHomeGround(intel, heat, model.roads, elapsed, controlOf); } // Chassis transform at the end of the last two fixed steps, for interpolation. diff --git a/src/sim/intel.ts b/src/sim/intel.ts index 640710c..42ddfb5 100644 --- a/src/sim/intel.ts +++ b/src/sim/intel.ts @@ -137,6 +137,51 @@ export function survey( return count; } +/** + * What you already know at the start of a campaign: your own ground. + * + * A fresh map was blank everywhere, including behind your own lines, which made + * the first several sessions read as amnesia rather than as insurgency. You are + * a local with a car — the pocket your own side holds is the part of the map you + * would obviously know, and the war is the part you would not. + * + * It also fixes the board. `offersAt` splits its offers into targets you have + * visited and targets you have not, and with nothing visited every offer came + * up "new" — so the known-against-new comparison the whole mission design turns + * on simply was not on screen until you had wandered for a while. + * + * Strictly bounded to liberated ground. Contested ground is not yours to know. + */ +export function revealHomeGround( + intel: Intel, + heat: HeatState, + roads: RoadNetwork, + now: number, + controlOf: (x: number, z: number) => Control, +): void { + for (let row = 0; row < intel.cellsPerSide; row++) { + for (let col = 0; col < intel.cellsPerSide; col++) { + const x = intel.gridOrigin + (col + 0.5) * CELL_SIZE; + const z = intel.gridOrigin + (row + 0.5) * CELL_SIZE; + if (controlOf(x, z) !== 'liberated') continue; + const index = row * intel.cellsPerSide + col; + intel.explored[index] = 1; + intel.rememberedControl[index] = CONTROLS.indexOf('liberated'); + } + } + + for (const s of roads.segments) { + if (controlOf((s.ax + s.bx) / 2, (s.az + s.bz) / 2) !== 'liberated') continue; + observe(intel, heat, s.id, now); + } + + // Junctions on home ground count as places you have been, which is what makes + // them eligible as the board's "known target". + for (const n of roads.nodes) { + if (controlOf(n.x, n.z) === 'liberated') intel.visitedNodes.add(n.id); + } +} + export interface RouteIntel { /** Worst level remembered anywhere on the route. */ worst: HeatLevel; diff --git a/src/sim/quests.test.ts b/src/sim/quests.test.ts index a0c1277..8c1cc8e 100644 --- a/src/sim/quests.test.ts +++ b/src/sim/quests.test.ts @@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest'; import { generateWorld } from './world'; import { placeBases, baseAt } from './bases'; import { buildGraph, findRoute } from './routing'; -import { createIntel, observe, summariseRoute, survey, hasSeen } from './intel'; +import { createIntel, observe, revealHomeGround, summariseRoute, survey, hasSeen } from './intel'; import { createAreas, createHeat, stepHeat } from './heat'; +import { controlAt, createFront } from './regions'; import { accept, createQuests, @@ -233,3 +234,39 @@ describe('mission lifecycle', () => { expect(state.active!.worked).toBe(0); }); }); + +describe('what a campaign starts out knowing', () => { + const started = () => { + const intel = createIntel(world.roads, world.extent); + const heat = createHeat(world.roads, areas); + const front = createFront(1337, world.extent, world.spawn); + revealHomeGround(intel, heat, world.roads, 0, (x, z) => controlAt(front, x, z)); + return { intel, heat, front }; + }; + + it('knows its own ground and nothing past the line', () => { + const { intel, front } = started(); + let home = 0; + let away = 0; + for (const s of world.roads.segments) { + const liberated = + controlAt(front, (s.ax + s.bx) / 2, (s.az + s.bz) / 2) === 'liberated'; + if (liberated) home += hasSeen(intel, s.id) ? 1 : 0; + else away += hasSeen(intel, s.id) ? 1 : 0; + } + expect(home).toBeGreaterThan(3); + // The war is the part you do not know. Nothing past the line is filled in. + expect(away).toBe(0); + }); + + it('gives the board a known target to weigh a new one against', () => { + // With nothing visited, offersAt has no "visited" pool at all and every + // offer comes up new — so the comparison the mission design turns on was + // not on screen until the player had wandered around for a while. + const { intel, front } = started(); + const offers = offersAt(createQuests(), world.roads, graph, intel, bases[0]!, 0, 99); + expect(offers.some((o) => !o.novel)).toBe(true); + expect(offers.some((o) => o.novel)).toBe(true); + void front; + }); +});