Patrols settle their district too, slowly, and a debug lever for condition
A patrol working a road now takes a little off the district around it as well, at roughly a tenth of the rate it takes off the road. Deliberately feeble: any faster and sitting still while patrols came and went would quietly launder your reputation, and district heat exists precisely to be the part you cannot patrol away. Passed as a position rather than a district id, so the unit sim still does not need to know how areas are gridded. Adds a debug panel under ?debug=1 with buttons to wear the car down or patch it up. Driving a wrecked car is something you want to try while driving, not while typing at a console. An earlier version of the console helper took an amount and ignored it, which would have made it silently useless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9eaa05d7ef
commit
5e557dc93a
15
README.md
15
README.md
@ -77,6 +77,10 @@ bleeds into every road running through it, including ones you have never driven.
|
||||
takes somebody actually using that road.
|
||||
- It **outlasts road heat**. A road is re-secured by one patrol; a district's
|
||||
reputation takes far longer to fade.
|
||||
- A patrol working a road **does** settle the district around it, at about a
|
||||
tenth of the rate it settles the road. Any faster and waiting out a patrol
|
||||
would launder your reputation, and district heat is meant to be the part you
|
||||
cannot patrol away.
|
||||
|
||||
### Speed is conspicuous
|
||||
|
||||
@ -271,6 +275,17 @@ is permanent in the same way everything else is.
|
||||
a target. They open up only past the line *and* on a road they have already
|
||||
checkpointed — that is, a road you personally made notorious.
|
||||
|
||||
## Debug
|
||||
|
||||
Load with `?debug=1` for a `window.__dbtl` handle and a small panel in the
|
||||
corner. The panel has `−` / `+` buttons for the car's condition, because
|
||||
"drive a wrecked car for a minute" is something you want to do *while driving*
|
||||
rather than while typing into a console.
|
||||
|
||||
The handle also steps the simulation by hand, renders on demand, and writes
|
||||
frames to `shots/` via a dev-only endpoint — which is how this build gets looked
|
||||
at at all when the tab is not compositing. `__dbtl.state` exposes the live sim.
|
||||
|
||||
## Sound
|
||||
|
||||
Everything is **synthesised** — there are no audio files. The build has to stay
|
||||
|
||||
62
src/debug.ts
62
src/debug.ts
@ -36,6 +36,60 @@ export interface DebugApi {
|
||||
state: Record<string, unknown>;
|
||||
info: () => Record<string, unknown>;
|
||||
teleport: (x: number, z: number) => void;
|
||||
condition: () => { level: { engine: number; tires: number; chassis: number } };
|
||||
setCondition: (level: number) => unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* An on-screen panel for the levers worth pulling by hand.
|
||||
*
|
||||
* The console handle covers everything, but "drive a wrecked car for a minute"
|
||||
* is a thing you want to do *while driving*, not while typing — so condition
|
||||
* gets buttons. Only ever mounted under `?debug=1`.
|
||||
*/
|
||||
function mountPanel(debug: { nudgeCondition: (by: number) => unknown }, api: DebugApi): void {
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'debug-panel';
|
||||
panel.style.cssText = `
|
||||
position: fixed; right: 12px; bottom: 12px; z-index: 10;
|
||||
display: flex; gap: 6px; align-items: center;
|
||||
font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: #cfd6dd; background: rgba(12,15,19,.9);
|
||||
border: 1px solid #2f3944; border-radius: 4px; padding: 8px 10px;
|
||||
`;
|
||||
|
||||
const readout = document.createElement('span');
|
||||
readout.style.cssText = 'min-width: 74px; color: #8fa3b5;';
|
||||
const refresh = () => {
|
||||
const { engine, tires, chassis } = api.condition().level;
|
||||
readout.textContent = `car ${(((engine + tires + chassis) / 3) * 100).toFixed(0)}%`;
|
||||
};
|
||||
|
||||
const button = (label: string, by: number) => {
|
||||
const element = document.createElement('button');
|
||||
element.textContent = label;
|
||||
element.style.cssText = `
|
||||
font: inherit; color: inherit; cursor: pointer;
|
||||
background: #1b222b; border: 1px solid #37414d; border-radius: 3px;
|
||||
padding: 3px 9px;
|
||||
`;
|
||||
element.addEventListener('click', () => {
|
||||
debug.nudgeCondition(by);
|
||||
refresh();
|
||||
// Keep the keyboard on the game, or space starts pressing this button.
|
||||
element.blur();
|
||||
});
|
||||
panel.append(element);
|
||||
};
|
||||
|
||||
panel.append(document.createTextNode('debug'));
|
||||
button('−', -0.15);
|
||||
button('+', +0.15);
|
||||
panel.append(readout);
|
||||
document.body.append(panel);
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 500);
|
||||
}
|
||||
|
||||
export function exposeDebug(api: DebugApi): void {
|
||||
@ -107,6 +161,13 @@ export function exposeDebug(api: DebugApi): void {
|
||||
},
|
||||
|
||||
teleport: api.teleport,
|
||||
/** 0 is a wreck, 1 is factory fresh. */
|
||||
setCondition: api.setCondition,
|
||||
/** Nudge condition up or down, for feeling out how decline drives. */
|
||||
nudgeCondition(by: number) {
|
||||
const now = api.condition().level;
|
||||
return api.setCondition((now.engine + now.tires + now.chassis) / 3 + by);
|
||||
},
|
||||
info: api.info,
|
||||
state: api.state,
|
||||
three: THREE,
|
||||
@ -114,5 +175,6 @@ export function exposeDebug(api: DebugApi): void {
|
||||
};
|
||||
|
||||
(window as unknown as { __dbtl: typeof debug }).__dbtl = debug;
|
||||
mountPanel(debug, api);
|
||||
console.info('debug handle ready: window.__dbtl');
|
||||
}
|
||||
|
||||
23
src/main.ts
23
src/main.ts
@ -495,6 +495,10 @@ async function boot() {
|
||||
decayHeat: (id, amount) => {
|
||||
heat.value[id] = Math.max(0, heat.value[id]! - amount);
|
||||
},
|
||||
decayArea: (x, z, amount) => {
|
||||
const cell = areaAt(areas, x, z);
|
||||
if (cell !== null) heat.area[cell] = Math.max(0, heat.area[cell]! - amount);
|
||||
},
|
||||
},
|
||||
model.roads,
|
||||
graph,
|
||||
@ -711,6 +715,25 @@ async function boot() {
|
||||
physics,
|
||||
// Live state, so a script can find something interesting and go look at it.
|
||||
state: { units, combat, heat, areas, intel, quests, front, model, bases },
|
||||
condition: () => condition,
|
||||
/**
|
||||
* Force the car's condition, for looking at how a wrecked car drives
|
||||
* without having to spend twenty minutes wrecking one.
|
||||
*/
|
||||
setCondition(level: number) {
|
||||
const clamped = Math.max(0, Math.min(1, level));
|
||||
condition = {
|
||||
level: { engine: clamped, tires: clamped, chassis: clamped },
|
||||
// Raising the ceiling too, so this can undo damage as well as cause
|
||||
// it. It is a debug lever, not a repair: nothing in the game does this.
|
||||
ceiling: {
|
||||
engine: Math.max(clamped, condition.ceiling.engine),
|
||||
tires: Math.max(clamped, condition.ceiling.tires),
|
||||
chassis: Math.max(clamped, condition.ceiling.chassis),
|
||||
},
|
||||
};
|
||||
return condition;
|
||||
},
|
||||
teleport(x, z) {
|
||||
physics.chassis.setTranslation({ x, y: 1.4, z }, true);
|
||||
physics.chassis.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
||||
|
||||
@ -45,6 +45,7 @@ function run(
|
||||
front,
|
||||
heatLevel: () => 'turret',
|
||||
decayHeat: () => {},
|
||||
decayArea: () => {},
|
||||
...overrides,
|
||||
},
|
||||
world.roads,
|
||||
@ -127,6 +128,29 @@ describe('patrols are dispatched, not conjured', () => {
|
||||
expect(cooled).toBeGreaterThan(PATROL_HEAT_DECAY * 20);
|
||||
});
|
||||
|
||||
it('settles the district it is working, but only barely', () => {
|
||||
const state = createUnits();
|
||||
dispatchTo(state, world.roads, graph, segment, 'patrol', makeRng(7));
|
||||
let road = 0;
|
||||
let area = 0;
|
||||
run(state, 120, {
|
||||
player: { x: segment.ax, z: segment.az },
|
||||
decayHeat: (_, amount) => {
|
||||
road += amount;
|
||||
},
|
||||
decayArea: (_x, _z, amount) => {
|
||||
area += amount;
|
||||
},
|
||||
});
|
||||
|
||||
expect(area).toBeGreaterThan(0);
|
||||
// A patrol re-secures the road it was sent to. It barely touches how the
|
||||
// district feels about you — otherwise waiting out a patrol would launder
|
||||
// your reputation, and district heat is meant to be the part you cannot
|
||||
// patrol away.
|
||||
expect(area).toBeLessThan(road / 5);
|
||||
});
|
||||
|
||||
it('leaves once it has done its rounds', () => {
|
||||
const state = createUnits();
|
||||
dispatchTo(state, world.roads, graph, segment, 'patrol', makeRng(7));
|
||||
|
||||
@ -115,6 +115,16 @@ const SPEED: Record<UnitKind, number> = { car: 14, soldier: 2.4 };
|
||||
const PATROL_DURATION = 90;
|
||||
/** Heat removed per second by a patrol actually driving its assigned road. */
|
||||
export const PATROL_HEAT_DECAY = 0.005;
|
||||
/**
|
||||
* District heat a patrol settles per second, just by being present.
|
||||
*
|
||||
* An order of magnitude below the road figure, on purpose. A patrol re-secures
|
||||
* the road it was sent to; it barely touches how the district feels about you.
|
||||
* If this were anywhere near the road rate, sitting still while patrols came and
|
||||
* went would quietly launder your reputation, and the whole point of district
|
||||
* heat is that it is the thing you cannot patrol away.
|
||||
*/
|
||||
export const PATROL_AREA_DECAY = 0.0005;
|
||||
/**
|
||||
* Seconds of an engineer standing on site per stage. Concrete is quicker than a
|
||||
* tower, but neither is instant — arriving mid-build is meant to be a thing that
|
||||
@ -423,6 +433,11 @@ export interface UnitStep {
|
||||
heatLevel: (segmentId: number) => string;
|
||||
/** Called when a patrol is working a road, to cool it down. */
|
||||
decayHeat: (segmentId: number, amount: number) => void;
|
||||
/**
|
||||
* Called for the district a patrol is standing in. Takes a position rather
|
||||
* than a district id so this file never has to know how areas are gridded.
|
||||
*/
|
||||
decayArea: (x: number, z: number, amount: number) => void;
|
||||
}
|
||||
|
||||
export interface UnitEvents {
|
||||
@ -519,6 +534,8 @@ export function stepUnits(
|
||||
// Working the road is what brings it back down. The enemy is not
|
||||
// punishing you; they are re-securing a route and then leaving.
|
||||
step.decayHeat(segment.id, PATROL_HEAT_DECAY * dt);
|
||||
// Presence settles the district a little too, but only a little.
|
||||
step.decayArea(unit.x, unit.z, PATROL_AREA_DECAY * dt);
|
||||
if (unit.onStation > PATROL_DURATION) unit.expires = 0;
|
||||
} else {
|
||||
const stage: BuildStage = unit.building ?? 'barricade';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user