- 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)
208 lines
6.5 KiB
JavaScript
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);
|
|
});
|
|
});
|