const test = require("node:test"); const assert = require("node:assert/strict"); const data = require("../assets/scripts/pages/princess-lima-data.js"); const state = require("../assets/scripts/pages/princess-lima-state.js"); const systems = require("../assets/scripts/pages/princess-lima-systems.js"); test("movement is responsive and diagonal speed is normalized", () => { let velocity = { x: 0, y: 0 }; for (let frame = 0; frame < 120; frame += 1) { velocity = systems.approachVelocity(velocity.x, velocity.y, 1, 1, 16, false); } assert.equal(Math.round(Math.hypot(velocity.x, velocity.y)), 176); for (let frame = 0; frame < 20; frame += 1) { velocity = systems.approachVelocity(velocity.x, velocity.y, 0, 0, 16, false); } assert.deepEqual(velocity, { x: 0, y: 0 }); }); test("directional attacks separate wind-up, active hit and recovery timing", () => { assert.equal(systems.attackPhase(0), "windup"); assert.equal(systems.attackPhase(systems.ATTACK_TIMING.windup), "active"); assert.equal(systems.attackPhase(systems.ATTACK_TIMING.windup + systems.ATTACK_TIMING.active), "recovery"); assert.equal(systems.attackPhase(999), "complete"); const origin = { x: 500, y: 400 }; const north = systems.attackHitbox("north", origin.x, origin.y); const south = systems.attackHitbox("south", origin.x, origin.y); const east = systems.attackHitbox("east", origin.x, origin.y); const west = systems.attackHitbox("west", origin.x, origin.y); assert.ok(north.y + north.height <= origin.y); assert.ok(south.y >= origin.y); assert.ok(east.x >= origin.x); assert.ok(west.x + west.width <= origin.x); }); test("enemy knockback stops before walls and map boundaries", () => { const current = state.fresh("Ada", "azure"); const safe = systems.wallSafeKnockback(current, "village", 430, 400, 1, 0, 60); assert.equal(systems.isSafePosition(current, "village", safe.x, safe.y), true); const edge = systems.wallSafeKnockback(current, "village", 50, 400, -1, 0, 90); assert.equal(systems.isSafePosition(current, "village", edge.x, edge.y), true); }); test("all named spawns are collision-safe and invalid positions recover", () => { const current = state.fresh("Ada", "azure"); Object.entries(data.MAPS).forEach(([regionId, map]) => { Object.entries(map.spawns).forEach(([spawnId, spawn]) => { assert.equal(systems.isSafePosition(current, regionId, spawn.x, spawn.y), true, `${regionId}:${spawnId}`); }); }); const recovered = systems.nearestSafeSpawn(current, "village", 100, 100); assert.equal(systems.isSafePosition(current, recovered.region, recovered.x, recovered.y), true); }); test("mountain bridge controls are reachable before repair and crossing opens afterward", () => { let current = state.fresh("Ada", "azure"); const mountain = data.MAPS.mountain; const controls = mountain.puzzle.objects.map(([, x, y]) => ({ x, y })); controls.forEach((point) => { assert.equal(systems.isSafePosition(current, "mountain", point.x, point.y), true); }); assert.ok(Math.hypot(controls[0].x - controls[1].x, controls[0].y - controls[1].y) <= 80); assert.equal(systems.isSafePosition(current, "mountain", 365, 345), false); current = systems.solvePuzzle(current, "bridge_winches"); assert.equal(current.unlockedRoutes.includes("bridge_repaired"), true); assert.equal(systems.isSafePosition(current, "mountain", 365, 345), true); }); test("every region has an unbroken outer collision border", () => { const current = state.fresh("Ada", "azure"); data.MAP_IDS.forEach((region) => { [[10, 360], [710, 360], [360, 10], [360, 710]].forEach(([x, y]) => { assert.equal(systems.isSafePosition(current, region, x, y), false, `${region}:${x},${y}`); }); }); }); test("quest progression grants rewards once and unlocks routes", () => { let current = state.fresh("Ada", "azure"); current = systems.completeQuest(current, "aftermath"); assert.equal(systems.quantity(current, "village_sword"), 1); current = systems.progressQuest(current, "village_defence", 3); assert.equal(current.quests.village_defence.status, "complete"); assert.equal(current.unlockedRoutes.includes("village_defended"), true); const tonics = systems.quantity(current, "healing_tonic"); current = systems.completeQuest(current, "village_defence"); assert.equal(systems.quantity(current, "healing_tonic"), tonics); }); test("optional quests remain independent of chapter progression", () => { let current = state.fresh("Ada", "azure"); current = systems.startQuest(current, "healer_herbs"); current = systems.completeQuest(current, "healer_herbs"); assert.equal(current.quests.healer_herbs.status, "complete"); assert.equal(current.chapter, 1); }); test("inventory caps stacks, protects quest items and healing consumes safely", () => { let current = state.fresh("Ada", "azure"); current = systems.addItem(current, "healing_tonic", 30); assert.equal(systems.quantity(current, "healing_tonic"), 9); current = systems.addItem(current, "prison_key", 1); assert.equal(systems.removeItem(current, "prison_key", 1).removed, false); current.health = 30; const result = systems.useItem(current, "healing_tonic"); assert.equal(result.used, true); assert.equal(result.state.health, 70); assert.equal(systems.quantity(result.state, "healing_tonic"), 8); }); test("combat applies defence, invulnerability, defeat and checkpoint respawn", () => { let current = systems.addItem(state.fresh("Ada", "azure"), "buckler", 1); const hit = systems.damage(current, 10, 2000, 0); assert.equal(hit.amount, 8); assert.equal(hit.state.health, 92); assert.equal(systems.damage(hit.state, 10, 2100, 2000).hit, false); const defeated = systems.damage({ ...hit.state, health: 3 }, 20, 4000, 0); assert.equal(defeated.defeated, true); const respawned = systems.respawn(defeated.state); assert.equal(respawned.region, "village"); assert.ok(respawned.health >= 50); }); test("puzzles unlock chapter routes and cannot reward twice", () => { let current = state.fresh("Ada", "azure"); current = systems.solvePuzzle(current, "forest_stones"); assert.equal(current.solvedPuzzles.includes("forest_stones"), true); assert.equal(current.unlockedRoutes.includes("guide_found"), true); const boots = systems.quantity(current, "trail_boots"); current = systems.solvePuzzle(current, "forest_stones"); assert.equal(systems.quantity(current, "trail_boots"), boots); }); test("boss phases and victories are deterministic", () => { assert.equal(systems.bossPhase(48, 48, 3), 1); assert.equal(systems.bossPhase(24, 48, 3), 2); assert.equal(systems.bossPhase(10, 48, 3), 3); let current = state.fresh("Ada", "azure"); current = systems.recordBoss(current, "malrec"); assert.equal(current.defeatedBosses.includes("malrec"), true); assert.equal(current.unlockedRoutes.includes("malrec_defeated"), true); assert.equal(current.story, "rescue"); }); test("complete story path reaches the Princess Lima rescue state", () => { let current = state.fresh("Ada", "azure"); ["aftermath", "village_defence", "find_guide", "ruins_light", "wolf_miniboss", "repair_bridge", "stone_guardian", "free_scout", "free_prisoners", "break_wards", "defeat_malrec"] .forEach((id) => { current = systems.completeQuest(current, id); }); assert.equal(current.chapter, 4); assert.equal(current.unlockedRoutes.includes("wards_broken"), true); assert.equal(current.unlockedRoutes.includes("malrec_defeated"), true); });