Files
iron-requiem/tests/slice1_physics.test.js
Kay Kayyali 50770f95d9 slice1.2: RED→GREEN — Tank hull physics with 25-ton inertia model (6 tests)
- Updated TANK_ACCELERATION (400), TANK_FRICTION (2.0) for additive drag model
- Added ICE_FRICTION_MULTIPLIER (0.3) for ice/tundra surfaces
- Tank.preUpdate: accel = (input * power) - (velocity * friction)
- Fixed vitest setup.js (HTMLVideoElement polyfill) and scaffold test
- Tank takes >1s to reach max speed (τ=0.5s, 2τ=1s → ~86% vmax)
2026-05-23 06:31:08 +00:00

208 lines
6.5 KiB
JavaScript

/**
* Tank hull physics tests — Slice 1.2
*
* Physics model: accel = (input * TANK_ACCELERATION) - (velocity * effectiveFriction)
*
* vi.mock('phaser') provides a minimal ArcadeSprite base class so Tank
* can be instantiated without booting a real Phaser game. Tests then
* call tank.preUpdate() with a fake body to verify velocity calculations.
*/
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
// Mock Phaser before any imports — vi.mock is hoisted
vi.mock('phaser', () => {
// Fake body that Tank's preUpdate modifies via .velocity and .setDrag/.setMaxVelocity
class FakeBody {
constructor() {
this.velocity = { x: 0, y: 0 };
this.x = 0;
this.y = 0;
}
setDrag(x, y) {}
setMaxVelocity(x, y) {}
}
// Minimal ArcadeSprite that Tank extends — does the bare minimum
class ArcadeSprite {
constructor(scene, x, y, texture) {
this.scene = scene;
this.active = true;
this.visible = true;
this.x = x;
this.y = y;
this.body = new FakeBody();
}
destroy() {
this.active = false;
this.visible = false;
}
setActive(value) { this.active = value; }
setVisible(value) { this.visible = value; }
preUpdate(time, delta) {}
static preUpdate(time, delta) {}
}
return {
default: {
Scene: class Scene {
constructor(config) { this.scene = config; }
add = { existing: () => {} };
physics = {
add: { existing: () => {} },
};
},
Physics: {
Arcade: { Sprite: ArcadeSprite },
},
Game: class {
constructor(cfg) {}
destroy() {}
},
HEADLESS: 0,
AUTO: 1,
},
__esModule: true,
};
});
// Now import Tank AFTER the mock is hoisted
import { Tank } from '@/game/entities/Tank.js';
import { TANK_MAX_SPEED, TANK_FRICTION, ICE_FRICTION_MULTIPLIER, TANK_ACCELERATION } from '@/constants.js';
const DT_MS = 1000 / 60; // ~16.67ms per frame @ 60fps
describe('Tank hull physics', () => {
let tank;
let scene;
beforeEach(() => {
scene = {
add: { existing: () => {} },
physics: { add: { existing: () => {} } },
};
tank = new Tank(scene, 400, 300);
});
afterEach(() => {
tank.destroy();
});
// --- helpers ---
function simulateFrames(frames, dtMs = DT_MS) {
for (let i = 0; i < frames; i++) {
tank.preUpdate(0, dtMs);
}
}
function speed() {
const v = tank.body.velocity;
return Math.sqrt(v.x * v.x + v.y * v.y);
}
// ================================================================
// TEST 1: Acceleration from standstill takes > 1 second
// ================================================================
test('accelerates from standstill taking more than 1 second to reach max speed', () => {
tank.setInput(1, 0); // full right
simulateFrames(60); // exactly 1 second at 60 fps
expect(speed()).toBeGreaterThan(0);
expect(speed()).toBeLessThan(TANK_MAX_SPEED);
});
// ================================================================
// TEST 2: Gradual deceleration when input released
// ================================================================
test('decelerates gradually when input released (does not stop instantly)', () => {
tank.body.velocity.x = TANK_MAX_SPEED;
tank.setInput(0, 0);
simulateFrames(18); // 0.3 seconds
const v = Math.abs(tank.body.velocity.x);
expect(v).toBeGreaterThan(0);
expect(v).toBeLessThan(TANK_MAX_SPEED);
});
// ================================================================
// TEST 3: Momentum carries when changing direction (drift)
// ================================================================
test('maintains momentum when changing direction — continues drifting', () => {
tank.body.velocity.x = 150;
tank.setInput(-1, 0); // suddenly push left
simulateFrames(5); // ~83ms — short enough not to reverse
// Should STILL have rightward velocity (momentum hasn't reversed yet)
// but should be LESS than 150 (some opposing force has been applied)
expect(tank.body.velocity.x).toBeGreaterThan(0);
expect(tank.body.velocity.x).toBeLessThan(150);
});
// ================================================================
// TEST 4: Speed clamp at TANK_MAX_SPEED
// ================================================================
test('cannot exceed TANK_MAX_SPEED', () => {
tank.setInput(1, 0); // full right
simulateFrames(300); // 5 seconds
const vx = Math.abs(tank.body.velocity.x);
expect(vx).toBeGreaterThan(0); // must actually move
expect(vx).toBeLessThanOrEqual(TANK_MAX_SPEED);
});
// ================================================================
// TEST 5: Ice surface reduces friction
// ================================================================
test('ice surface reduces friction — higher speed after coasting on ice vs normal', () => {
// --- Ice tank ---
tank.body.velocity.x = TANK_MAX_SPEED;
tank.setSurface(ICE_FRICTION_MULTIPLIER);
tank.setInput(0, 0);
simulateFrames(30); // 0.5s on ice
const speedOnIce = Math.abs(tank.body.velocity.x);
// --- Normal tank ---
const tankNormal = new Tank(scene, 400, 300);
tankNormal.body.velocity.x = TANK_MAX_SPEED;
tankNormal.setInput(0, 0);
for (let i = 0; i < 30; i++) {
tankNormal.preUpdate(0, DT_MS);
}
const speedOnNormal = Math.abs(tankNormal.body.velocity.x);
tankNormal.destroy();
// Ice speed should be higher (less friction → slower deceleration)
expect(speedOnIce).toBeGreaterThan(speedOnNormal);
});
// ================================================================
// TEST 6: Opposite input decelerates faster than releasing
// ================================================================
test('opposite input decelerates faster than releasing input', () => {
// --- Coast tank: release input ---
const tankCoast = new Tank(scene, 400, 300);
tankCoast.body.velocity.x = TANK_MAX_SPEED;
tankCoast.setInput(0, 0);
for (let i = 0; i < 20; i++) {
tankCoast.preUpdate(0, DT_MS);
}
const coastSpeed = Math.abs(tankCoast.body.velocity.x);
tankCoast.destroy();
// --- Brake tank: opposite input ---
tank.body.velocity.x = TANK_MAX_SPEED;
tank.setInput(-1, 0);
for (let i = 0; i < 20; i++) {
tank.preUpdate(0, DT_MS);
}
const brakeSpeed = Math.abs(tank.body.velocity.x);
expect(brakeSpeed).toBeLessThan(coastSpeed);
});
});