Files
org_web/tests/princess-lima-systems.test.cjs
gitea-actions a6502b168d
All checks were successful
Build Org Website / build (push) Successful in 38s
Polish Princess Lima map data and interaction feedback
2026-07-30 13:58:25 +01:00

152 lines
7.3 KiB
JavaScript
Executable File

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);
});