152 lines
7.3 KiB
JavaScript
Executable File
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);
|
|
});
|