- Replace socket.io relay with Colyseus 0.15 authoritative server - GameRoom with GameState schema (players, units, resources) - Pure TS services: CombatResolver, EconomyService, PathfindingService, UnitManager - POST /api/create-room → 4-char invite code - React/MUI LobbyScreen: Create (shows code + START GAME) / Join by code - ColyseusClient: joinOrCreate/join by room type = invite code - Nginx: static assets direct, all else proxied to Colyseus (WS upgrade) - Content-hashed JS bundles for Cloudflare cache-busting - 1-player lobbies: START GAME button bypasses 2-player wait
194 lines
5.3 KiB
TypeScript
194 lines
5.3 KiB
TypeScript
/**
|
|
* Tests for inputHandler — pure functions that process client input messages
|
|
* and delegate to UnitManager. Extracted from GameRoom for testability,
|
|
* same pattern as roomLogic.ts.
|
|
*/
|
|
import { handleInput, ClientMessage } from "../src/rooms/inputHandler";
|
|
import { UnitManager, UnitRecord } from "../src/systems/UnitManager";
|
|
|
|
function freshManager(): UnitManager {
|
|
return new UnitManager();
|
|
}
|
|
|
|
describe("handleInput - spawnUnit", () => {
|
|
it("delegates to UnitManager.spawnUnit and returns the unit", () => {
|
|
const mgr = freshManager();
|
|
const msg: ClientMessage = {
|
|
type: "spawnUnit",
|
|
unitType: "tank",
|
|
position: { x: 50, y: 50 },
|
|
team: "ukraine",
|
|
};
|
|
|
|
const result = handleInput(mgr, "p1", msg) as UnitRecord;
|
|
|
|
expect(result.ownerId).toBe("p1");
|
|
expect(result.type).toBe("tank");
|
|
expect(result.position).toEqual({ x: 50, y: 50 });
|
|
expect(result.team).toBe("ukraine");
|
|
});
|
|
|
|
it("uses infantry if unitType is infantry", () => {
|
|
const mgr = freshManager();
|
|
const msg: ClientMessage = {
|
|
type: "spawnUnit",
|
|
unitType: "infantry",
|
|
position: { x: 0, y: 0 },
|
|
team: "russia",
|
|
};
|
|
|
|
const result = handleInput(mgr, "p2", msg) as UnitRecord;
|
|
expect(result.type).toBe("infantry");
|
|
expect(result.health.max).toBe(100);
|
|
});
|
|
});
|
|
|
|
describe("handleInput - moveUnit", () => {
|
|
it("delegates to UnitManager.moveUnit", () => {
|
|
const mgr = freshManager();
|
|
const unit = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
|
|
|
|
const msg: ClientMessage = {
|
|
type: "moveUnit",
|
|
unitId: unit.id,
|
|
path: [{ x: 10, y: 0 }, { x: 10, y: 10 }],
|
|
};
|
|
|
|
const result = handleInput(mgr, "p1", msg) as UnitRecord;
|
|
|
|
expect(result.state).toBe("MOVING");
|
|
expect(result.path).toEqual(msg.path);
|
|
});
|
|
});
|
|
|
|
describe("handleInput - attackUnit", () => {
|
|
it("delegates to UnitManager.attackUnit", () => {
|
|
const mgr = freshManager();
|
|
const attacker = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
|
|
const target = mgr.spawnUnit("p2", "infantry", { x: 50, y: 50 }, "russia");
|
|
|
|
const msg: ClientMessage = {
|
|
type: "attackUnit",
|
|
unitId: attacker.id,
|
|
targetId: target.id,
|
|
};
|
|
|
|
const result = handleInput(mgr, "p1", msg) as UnitRecord;
|
|
|
|
expect(result.state).toBe("ATTACKING");
|
|
expect(result.targetId).toBe(target.id);
|
|
});
|
|
});
|
|
|
|
describe("handleInput - damageUnit", () => {
|
|
it("delegates to UnitManager.damageUnit", () => {
|
|
const mgr = freshManager();
|
|
const unit = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
|
|
|
|
const msg: ClientMessage = {
|
|
type: "damageUnit",
|
|
unitId: unit.id,
|
|
amount: 30,
|
|
};
|
|
|
|
const result = handleInput(mgr, "p1", msg) as UnitRecord;
|
|
expect(result.health.current).toBe(120);
|
|
});
|
|
});
|
|
|
|
describe("handleInput - removeDeadUnits", () => {
|
|
it("delegates to UnitManager.removeDeadUnits", () => {
|
|
const mgr = freshManager();
|
|
const unit = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
|
|
mgr.damageUnit(unit.id, 999);
|
|
|
|
const msg: ClientMessage = {
|
|
type: "removeDeadUnits",
|
|
};
|
|
|
|
const result = handleInput(mgr, "p1", msg) as string[];
|
|
expect(result).toEqual([unit.id]);
|
|
expect(mgr.getUnit(unit.id)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("handleInput - applyEvent", () => {
|
|
it("applies a UnitEvent to a unit", () => {
|
|
const mgr = freshManager();
|
|
const unit = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
|
|
|
|
const msg: ClientMessage = {
|
|
type: "applyEvent",
|
|
unitId: unit.id,
|
|
event: "MOVE",
|
|
};
|
|
|
|
const result = handleInput(mgr, "p1", msg) as UnitRecord;
|
|
expect(result.state).toBe("MOVING");
|
|
});
|
|
});
|
|
|
|
describe("handleInput - unknown type", () => {
|
|
it("returns null for unrecognized message type", () => {
|
|
const mgr = freshManager();
|
|
const msg = { type: "unknownCommand" } as ClientMessage;
|
|
|
|
expect(handleInput(mgr, "p1", msg)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("handleInput - getUnitsInRange", () => {
|
|
it("returns units in range for the given team", () => {
|
|
const mgr = freshManager();
|
|
mgr.spawnUnit("p2", "infantry", { x: 5, y: 5 }, "russia");
|
|
mgr.spawnUnit("p2", "infantry", { x: 500, y: 500 }, "russia");
|
|
|
|
const msg: ClientMessage = {
|
|
type: "getUnitsInRange",
|
|
position: { x: 0, y: 0 },
|
|
range: 10,
|
|
team: "russia",
|
|
};
|
|
|
|
const result = handleInput(mgr, "p1", msg) as UnitRecord[];
|
|
expect(result.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("handleInput - full client flow", () => {
|
|
it("spawn → move → damage → cleanup", () => {
|
|
const mgr = freshManager();
|
|
|
|
// spawn
|
|
const spawned = handleInput(mgr, "p1", {
|
|
type: "spawnUnit",
|
|
unitType: "tank",
|
|
position: { x: 0, y: 0 },
|
|
team: "ukraine",
|
|
}) as UnitRecord;
|
|
expect(spawned.state).toBe("IDLING");
|
|
|
|
// move
|
|
const moved = handleInput(mgr, "p1", {
|
|
type: "moveUnit",
|
|
unitId: spawned.id,
|
|
path: [{ x: 100, y: 100 }],
|
|
}) as UnitRecord;
|
|
expect(moved.state).toBe("MOVING");
|
|
|
|
// damage to kill
|
|
const damaged = handleInput(mgr, "p1", {
|
|
type: "damageUnit",
|
|
unitId: spawned.id,
|
|
amount: 200,
|
|
}) as UnitRecord;
|
|
expect(damaged.state).toBe("DYING");
|
|
|
|
// cleanup
|
|
const cleaned = handleInput(mgr, "p1", {
|
|
type: "removeDeadUnits",
|
|
}) as string[];
|
|
expect(cleaned).toContain(spawned.id);
|
|
});
|
|
});
|