Steering. The falloff with speed was linear, so the car had already lost a third of its lock by 20km/h — exactly the speed you take a right-angle junction at, on a map made of right-angle junctions. It is quadratic now, which leaves low speeds nearly untouched and still calms things down at pace, plus a faster rack and a little more lock. Pinned with a test that drives an actual quarter turn and measures its radius. A first attempt asserted that fast corners take longer, which is simply false: a fast car swings through ninety degrees quicker, just across far more tarmac. Radius is what a corner costs you. Contact. Units were points that ignored everything, so there was nothing to hit. Vehicles now carry kinematic bodies and ramming one is a real collision that damages them and shoves you. People get no collider on purpose — a capsule means snagging on pedestrians or launching them — so you drive through them and they go down, and running over a civilian is counted for later. Territory. Liberated ground covered more than half the map because the player started in the middle of it, so most of the world was safe by geometry. The starting base is now at the friendly edge and the bands are anchored to give roughly 15/25/27/33 across liberated, contested, occupied and frontier. Measured in the running game rather than guessed, since the axis runs diagonally across a square and area is not linear in depth. Also fixes a fragile test that compared reward rates across whichever targets a board happened to offer, and so mostly measured route length rather than the novelty premium it claimed to check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
4.9 KiB
TypeScript
148 lines
4.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { createUnitBodies } from './unitBodies';
|
|
import { createPhysics } from './physics/physics';
|
|
import { createUnits, type Unit, type UnitState } from './sim/units';
|
|
import { generateWorld } from './sim/world';
|
|
|
|
const world = generateWorld(1, 0);
|
|
|
|
function place(state: UnitState, overrides: Partial<Unit>): Unit {
|
|
const unit: Unit = {
|
|
id: state.nextId++,
|
|
kind: 'soldier',
|
|
faction: 'enemy',
|
|
role: 'fighter',
|
|
x: 0,
|
|
z: 0,
|
|
heading: 0,
|
|
speed: 0,
|
|
hp: 30,
|
|
path: [],
|
|
expires: 999,
|
|
assigned: null,
|
|
onStation: 0,
|
|
cooldown: 0,
|
|
elevation: 1.2,
|
|
...overrides,
|
|
};
|
|
state.units.push(unit);
|
|
return unit;
|
|
}
|
|
|
|
describe('running people over', () => {
|
|
it('puts down someone you drive through at speed', async () => {
|
|
const physics = await createPhysics(world);
|
|
const contacts = createUnitBodies(physics);
|
|
const units = createUnits();
|
|
const victim = place(units, { x: 1, z: 0 });
|
|
|
|
const events = contacts.sync(units, { x: 0, z: 0 }, 14, 1 / 60);
|
|
expect(events.ranOver).toContain(victim);
|
|
expect(victim.hp).toBe(0);
|
|
});
|
|
|
|
it('does not hurt anyone you creep past', async () => {
|
|
const physics = await createPhysics(world);
|
|
const contacts = createUnitBodies(physics);
|
|
const units = createUnits();
|
|
const bystander = place(units, { x: 1, z: 0 });
|
|
|
|
// Walking pace. Brushing past somebody is not running them over.
|
|
const events = contacts.sync(units, { x: 0, z: 0 }, 1.5, 1 / 60);
|
|
expect(events.ranOver).toHaveLength(0);
|
|
expect(bystander.hp).toBe(30);
|
|
});
|
|
|
|
it('leaves people you merely drove near alone', async () => {
|
|
const physics = await createPhysics(world);
|
|
const contacts = createUnitBodies(physics);
|
|
const units = createUnits();
|
|
const bystander = place(units, { x: 9, z: 0 });
|
|
|
|
expect(contacts.sync(units, { x: 0, z: 0 }, 25, 1 / 60).ranOver).toHaveLength(0);
|
|
expect(bystander.hp).toBe(30);
|
|
});
|
|
|
|
it('gives people no collider, so the car is never hung up on a crowd', async () => {
|
|
const physics = await createPhysics(world);
|
|
const contacts = createUnitBodies(physics);
|
|
const units = createUnits();
|
|
for (let i = 0; i < 12; i++) place(units, { x: i * 2, z: 4 });
|
|
|
|
contacts.sync(units, { x: 0, z: 0 }, 0, 1 / 60);
|
|
expect(contacts.count).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('hitting other vehicles', () => {
|
|
const car = (state: UnitState, at: { x: number; z: number }) =>
|
|
place(state, { kind: 'car', role: 'traffic', faction: 'civilian', hp: 60, ...at });
|
|
|
|
it('gives nearby vehicles a body to collide with', async () => {
|
|
const physics = await createPhysics(world);
|
|
const contacts = createUnitBodies(physics);
|
|
const units = createUnits();
|
|
car(units, { x: 12, z: 0 });
|
|
|
|
const before = physics.rapier.bodies.len();
|
|
contacts.sync(units, { x: 0, z: 0 }, 0, 1 / 60);
|
|
expect(contacts.count).toBe(1);
|
|
expect(physics.rapier.bodies.len()).toBe(before + 1);
|
|
});
|
|
|
|
it('damages a vehicle you ram, and not one you pull up behind', async () => {
|
|
const physics = await createPhysics(world);
|
|
const contacts = createUnitBodies(physics);
|
|
const units = createUnits();
|
|
const rammed = car(units, { x: 3, z: 0 });
|
|
const parked = car(units, { x: 60, z: 0 });
|
|
|
|
for (let i = 0; i < 30; i++) contacts.sync(units, { x: 0, z: 0 }, 20, 1 / 60);
|
|
expect(rammed.hp).toBeLessThan(60);
|
|
expect(parked.hp).toBe(60);
|
|
});
|
|
|
|
it('does not damage a vehicle you are simply sitting next to', async () => {
|
|
const physics = await createPhysics(world);
|
|
const contacts = createUnitBodies(physics);
|
|
const units = createUnits();
|
|
const neighbour = car(units, { x: 3, z: 0 });
|
|
|
|
for (let i = 0; i < 60; i++) contacts.sync(units, { x: 0, z: 0 }, 1, 1 / 60);
|
|
expect(neighbour.hp).toBe(60);
|
|
});
|
|
|
|
it('takes bodies away again as traffic drives out of range', async () => {
|
|
const physics = await createPhysics(world);
|
|
const contacts = createUnitBodies(physics);
|
|
const units = createUnits();
|
|
const passing = car(units, { x: 20, z: 0 });
|
|
|
|
contacts.sync(units, { x: 0, z: 0 }, 0, 1 / 60);
|
|
const withBody = physics.rapier.bodies.len();
|
|
expect(contacts.count).toBe(1);
|
|
|
|
passing.x = 900;
|
|
contacts.sync(units, { x: 0, z: 0 }, 0, 1 / 60);
|
|
expect(contacts.count).toBe(0);
|
|
expect(physics.rapier.bodies.len()).toBe(withBody - 1);
|
|
});
|
|
|
|
it('does not leak bodies as traffic comes and goes', async () => {
|
|
const physics = await createPhysics(world);
|
|
const contacts = createUnitBodies(physics);
|
|
const units = createUnits();
|
|
const base = physics.rapier.bodies.len();
|
|
|
|
for (let cycle = 0; cycle < 5; cycle++) {
|
|
units.units = [];
|
|
for (let i = 0; i < 6; i++) car(units, { x: 10 + i * 6, z: 0 });
|
|
contacts.sync(units, { x: 0, z: 0 }, 0, 1 / 60);
|
|
expect(contacts.count).toBe(6);
|
|
units.units = [];
|
|
contacts.sync(units, { x: 0, z: 0 }, 0, 1 / 60);
|
|
expect(physics.rapier.bodies.len()).toBe(base);
|
|
}
|
|
});
|
|
});
|