A campaign starts knowing its own ground
The map was blank everywhere on a fresh start, including behind your own lines, which read as amnesia rather than insurgency. You are a local with a car: the pocket your 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 was not on screen until you had wandered around for a while. The first board of a fresh campaign now reads: a known target at 0.14 parts against new ones at 0.31 and 0.41. Strictly bounded to liberated ground. Contested ground is not yours to know, and none of this survives the line moving - the map still only remembers who held a place when you last stood in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
35f5b1c465
commit
3787350d4a
@ -5,7 +5,7 @@ import { areaAt, createAreas, createHeat, effectiveHeat, speedAttention, stepHea
|
|||||||
import { ROAD_ATTENTION, routeAt, segmentAt } from './sim/roads';
|
import { ROAD_ATTENTION, routeAt, segmentAt } from './sim/roads';
|
||||||
import { baseAt, placeBases } from './sim/bases';
|
import { baseAt, placeBases } from './sim/bases';
|
||||||
import { buildGraph } from './sim/routing';
|
import { buildGraph } from './sim/routing';
|
||||||
import { createIntel, observe, reveal, survey } from './sim/intel';
|
import { createIntel, observe, reveal, revealHomeGround, survey } from './sim/intel';
|
||||||
import {
|
import {
|
||||||
applyMissionImpact,
|
applyMissionImpact,
|
||||||
controlAt,
|
controlAt,
|
||||||
@ -293,6 +293,11 @@ async function boot() {
|
|||||||
heatProps.finished(segment.id);
|
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.
|
// Chassis transform at the end of the last two fixed steps, for interpolation.
|
||||||
|
|||||||
@ -137,6 +137,51 @@ export function survey(
|
|||||||
return count;
|
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 {
|
export interface RouteIntel {
|
||||||
/** Worst level remembered anywhere on the route. */
|
/** Worst level remembered anywhere on the route. */
|
||||||
worst: HeatLevel;
|
worst: HeatLevel;
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import { generateWorld } from './world';
|
import { generateWorld } from './world';
|
||||||
import { placeBases, baseAt } from './bases';
|
import { placeBases, baseAt } from './bases';
|
||||||
import { buildGraph, findRoute } from './routing';
|
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 { createAreas, createHeat, stepHeat } from './heat';
|
||||||
|
import { controlAt, createFront } from './regions';
|
||||||
import {
|
import {
|
||||||
accept,
|
accept,
|
||||||
createQuests,
|
createQuests,
|
||||||
@ -233,3 +234,39 @@ describe('mission lifecycle', () => {
|
|||||||
expect(state.active!.worked).toBe(0);
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user