Remove obsolete generated assets and dead code
All checks were successful
Build Org Website / build (push) Successful in 43s

This commit is contained in:
gitea-actions
2026-08-19 10:53:30 +01:00
parent 2ee6575539
commit d12edea8de
269 changed files with 58 additions and 85836 deletions

View File

@@ -1,108 +0,0 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const data = require("../assets/scripts/pages/princess-lima-v2-data.js");
const state = require("../assets/scripts/pages/princess-lima-v2-state.js");
const systems = require("../assets/scripts/pages/princess-lima-v2-systems.js");
const root = path.resolve(__dirname, "..");
const source = (file) => fs.readFileSync(path.join(root, file), "utf8");
test("v2 content contracts and all dialogue graphs validate", () => {
assert.deepEqual(data.validateAll(), []);
assert.equal(data.SCHEMA_VERSION, 2);
assert.equal(Object.keys(data.DIALOGUES).length >= 8, true);
Object.values(data.DIALOGUES).forEach((dialogue) => assert.equal(data.validateDialogue(dialogue), true));
});
test("all nine regions are genuine Tiled maps with authored runtime layers", () => {
assert.equal(data.MAP_IDS.length, 9);
data.MAP_IDS.forEach((region) => {
const map = JSON.parse(source(`assets/maps/princess-lima/${region}.json`));
assert.equal(map.type, "map");
assert.equal(map.tilewidth, 32);
assert.equal(map.tileheight, 32);
assert.deepEqual(systems.validateMap(map, data.MAP_IDS), [], region);
assert.deepEqual(map.layers.map((layer) => layer.name), systems.REQUIRED_MAP_LAYERS);
assert.ok(map.layers.find((layer) => layer.name === "Base terrain").data.some((tile) => tile > 0));
});
});
test("the runtime loads Tiled maps rather than regional backdrop frames", () => {
const maps = source("assets/scripts/pages/princess-lima-v2-maps.js");
const scenes = source("assets/scripts/pages/princess-lima-v2-scenes.js");
assert.match(maps, /make\.tilemap/);
assert.match(maps, /createLayer/);
assert.match(scenes, /tilemapTiledJSON/);
assert.doesNotMatch(maps, /regional-style-atlas|REGION_FRAME/);
});
test("character animation, cinematic dialogue and eight enemy roles are present", () => {
const roles = new Set(Object.values(data.ENEMIES).filter((enemy) => !enemy.boss).map((enemy) => enemy.role));
["melee", "shield", "ranged", "fast", "ambush", "caster", "flying", "support", "elite"].forEach((role) => assert.ok(roles.has(role)));
assert.ok(fs.existsSync(path.join(root, "assets/images/play/princess-lima/actor-atlas-v2.png")));
assert.ok(fs.existsSync(path.join(root, "assets/images/play/princess-lima/portrait-atlas-v2.png")));
const ui = source("assets/scripts/pages/princess-lima-v2-ui.js");
assert.match(ui, /lima-cinematic-dialogue/);
assert.match(ui, /aria-label/);
assert.match(ui, /Conversation history/);
});
test("the standalone page loads only the local v2 runtime and accessibility surface", () => {
const rpg = source("play/rpg.org");
assert.match(rpg, /easystar-0\.4\.4\.min\.js/);
assert.match(rpg, /princess-lima-v2-game\.js/);
assert.match(rpg, /data-lima-screenreader/);
assert.match(rpg, /Block Shift/);
assert.doesNotMatch(rpg, /princess-lima-game\.js\?v=/);
assert.doesNotMatch(rpg, /https?:\/\//);
});
test("v1 progress migrates to v2 without overwriting legacy fields", () => {
const legacy = {
version: 1,
player: { name: "Rowan", appearance: "pine" },
chapter: 4,
region: "fortressInterior",
quests: { break_wards: { status: "complete", count: 3, rewarded: true } },
inventory: [{ id: "tempered_sword", quantity: 1 }, { id: "bad_item", quantity: 5 }],
defeatedBosses: ["stone_guardian", "unknown"],
settings: { subtitles: false, highContrast: true },
introSeen: true
};
const migrated = state.migrateLegacy(legacy);
assert.equal(migrated.version, 2);
assert.equal(migrated.player.name, "Rowan");
assert.equal(migrated.quests.break_wards.status, "complete");
assert.equal(migrated.inventory.some((item) => item.id === "bad_item"), false);
assert.equal(migrated.position.spawn, "frontHall");
assert.equal(migrated.settings.highContrast, true);
});
test("combat phases, hitboxes and enemy states are deterministic", () => {
assert.equal(systems.attackPhase(0, "charged"), "windup");
assert.equal(systems.attackPhase(520, "charged"), "active");
assert.equal(systems.attackPhase(670, "charged"), "recovery");
const north = systems.attackHitbox("north", 100, 100, "light1");
assert.ok(north.y + north.height <= 100);
const enemy = data.ENEMIES.raider;
assert.equal(systems.nextEnemyState("patrol", {
distance: 30, homeDistance: 0, spec: enemy, health: 5, leash: 190,
stunned: false, telegraphDone: true, attackDone: true, cooldownDone: true
}), "telegraph");
});
test("Malrec is demanding but leaves readable counterplay", () => {
const malrec = data.ENEMIES.malrec;
assert.equal(malrec.phases, 3);
assert.ok(malrec.health <= 52);
assert.ok(malrec.damage <= 13);
assert.ok(malrec.telegraph >= 750);
assert.ok(malrec.cooldown >= 1500);
const scenes = source("assets/scripts/pages/princess-lima-v2-scenes.js");
assert.match(scenes, /phase \+ 1/);
assert.match(scenes, /state\.health \+ 25/);
assert.match(scenes, /defences_disabled/);
assert.match(scenes, /prisoners_freed/);
});

View File

@@ -1,78 +0,0 @@
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");
test("fresh save uses the Princess Lima schema and safe village start", () => {
const fresh = state.fresh(" Rowan ", "pine");
assert.equal(fresh.version, 1);
assert.equal(state.STORAGE_KEY, "zxh_princess_lima_rpg_v1");
assert.equal(fresh.player.name, "Rowan");
assert.equal(fresh.player.appearance, "pine");
assert.equal(fresh.region, "village");
assert.deepEqual({ x: fresh.position.x, y: fresh.position.y }, data.MAPS.village.spawns.start);
assert.equal(fresh.settings.soundEnabled, false);
assert.equal(fresh.introSeen, false);
assert.equal(fresh.settings.subtitles, true);
assert.equal(fresh.settings.narrationEnabled, true);
});
test("old v1 saves continue without unexpectedly replaying the new introduction", () => {
const candidate = state.fresh("Legacy", "azure");
delete candidate.introSeen;
delete candidate.settings.voice;
delete candidate.settings.subtitles;
delete candidate.settings.narrationEnabled;
const restored = state.normalize(candidate);
assert.equal(restored.introSeen, true);
assert.equal(restored.settings.voice, 0.85);
assert.equal(restored.settings.subtitles, true);
assert.equal(restored.settings.narrationEnabled, true);
});
test("names and appearances validate", () => {
assert.equal(state.validName(" A Traveller "), "A Traveller");
assert.equal(state.validName(""), null);
assert.equal(state.validName("x".repeat(21)), null);
assert.equal(state.normalize({ ...state.fresh("A", "azure"), player: { name: "A", appearance: "bad" } }), null);
});
test("valid saves restore bounded state and derived equipment", () => {
const candidate = state.fresh("Mira", "azure");
candidate.health = 999;
candidate.inventory = [
{ id: "tempered_sword", quantity: 1 },
{ id: "healing_tonic", quantity: 99 },
{ id: "bad", quantity: 4 }
];
candidate.solvedPuzzles = ["forest_stones", "forest_stones", "bad"];
candidate.defeatedBosses = ["captain", "captain", "bad"];
const restored = state.normalize(candidate);
assert.equal(restored.health, restored.maxHealth);
assert.equal(restored.attack, 2);
assert.equal(restored.inventory.find((item) => item.id === "healing_tonic").quantity, 9);
assert.deepEqual(restored.solvedPuzzles, ["forest_stones"]);
assert.deepEqual(restored.defeatedBosses, ["captain"]);
});
test("invalid and unsupported saves recover without throwing", () => {
assert.equal(state.parse("{"), null);
assert.equal(state.parse(JSON.stringify({ version: 99 })), null);
assert.equal(state.parse(JSON.stringify({ version: 1 })), null);
});
test("quest, route, puzzle, boss and settings restoration filters malformed data", () => {
const candidate = state.fresh("Mira", "ember");
candidate.quests.aftermath = { status: "active", count: 999, rewarded: true };
candidate.quests.village_defence = { status: "nonsense", count: -10 };
candidate.unlockedRoutes = ["guide_found", "guide_found", "fake"];
candidate.settings = { master: 4, music: -2, effects: "bad", textSpeed: "instant", highContrast: true };
const restored = state.normalize(candidate);
assert.equal(restored.quests.aftermath.count, 99);
assert.equal(restored.quests.village_defence.status, "locked");
assert.deepEqual(restored.unlockedRoutes, ["guide_found"]);
assert.equal(restored.settings.master, 1);
assert.equal(restored.settings.music, 0);
assert.equal(restored.settings.effects, 0.7);
assert.equal(restored.settings.textSpeed, "instant");
});

View File

@@ -1,151 +0,0 @@
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);
});

View File

@@ -1,60 +0,0 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const data = require("../assets/scripts/pages/princess-lima-v2-data.js");
const state = require("../assets/scripts/pages/princess-lima-v2-state.js");
const systems = require("../assets/scripts/pages/princess-lima-v2-systems.js");
test("fresh saves use the v2 key and complete accessibility defaults", () => {
const fresh = state.fresh("Aster", "azure");
assert.equal(state.STORAGE_KEY, "zxh_princess_lima_rpg_v2");
assert.equal(state.LEGACY_KEY, "zxh_princess_lima_rpg_v1");
assert.equal(fresh.settings.subtitles, true);
assert.equal(fresh.settings.blur, true);
assert.equal(fresh.settings.particles, true);
assert.equal(fresh.settings.screenShake, true);
});
test("loading imports legacy data once and leaves the legacy key untouched", () => {
const values = new Map([[state.LEGACY_KEY, JSON.stringify({
version: 1, player: { name: "Legacy", appearance: "ember" }, chapter: 2,
region: "forest", inventory: [], quests: {}, settings: {}
})]]);
const storage = { getItem: (key) => values.get(key) || null, setItem: (key, value) => values.set(key, value) };
const result = state.load(storage);
assert.equal(result.migrated, true);
assert.equal(result.state.player.name, "Legacy");
assert.ok(values.has(state.LEGACY_KEY));
assert.ok(values.has(state.STORAGE_KEY));
});
test("dialogue choices apply flags and quest effects", () => {
const fresh = state.fresh("Aster", "azure");
const result = systems.dialogueChoice("elder", "choice", 0, fresh);
assert.equal(result.next, "accept");
assert.equal(result.state.flags.promised_village, true);
assert.equal(result.state.quests.aftermath.status, "active");
});
test("quest completion applies rewards and world state transformations", () => {
let current = state.fresh("Aster", "azure");
current = state.startQuest(current, "village_defence");
current = state.progressQuest(current, "village_defence", 3);
assert.equal(current.quests.village_defence.status, "complete");
assert.equal(current.flags.village_resolved, true);
assert.equal(current.flags.village_defended, true);
assert.equal(current.chapter, 2);
assert.equal(current.inventory.some((item) => item.id === "buckler"), true);
});
test("normalization removes invalid IDs and clamps unsafe values", () => {
const candidate = state.fresh("Aster", "azure");
candidate.health = 999;
candidate.inventory.push({ id: "not-real", quantity: 99 });
candidate.region = "missing";
candidate.settings.effectsQuality = "impossible";
const normalized = state.normalize(candidate);
assert.equal(normalized.health, normalized.maxHealth);
assert.equal(normalized.region, "village");
assert.equal(normalized.inventory.some((entry) => !data.ITEMS[entry.id]), false);
assert.equal(normalized.settings.effectsQuality, "full");
});

View File

@@ -1,239 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import {fileURLToPath} from "node:url";
import {
applyEffects, createStore, freshState, normalizeState, relationshipStage,
resolveEnding, requirementMet, RELATIONSHIPS
} from "../../assets/scripts/games/two-rivers/core/state.js";
import {validateContent} from "../../assets/scripts/games/two-rivers/core/content.js";
import {createSaveService, SAVE_KEY} from "../../assets/scripts/games/two-rivers/core/save.js";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const read = (file) => fs.readFileSync(path.join(root, file), "utf8");
const json = (file) => JSON.parse(read(file));
const content = {
assets: json("assets/games/two-rivers/data/assets.json"),
characters: json("assets/games/two-rivers/data/characters.json"),
locations: json("assets/games/two-rivers/data/locations.json"),
items: json("assets/games/two-rivers/data/items.json"),
quests: json("assets/games/two-rivers/data/quests.json"),
achievements: json("assets/games/two-rivers/data/achievements.json"),
endings: json("assets/games/two-rivers/data/endings.json"),
dialogue: {
...json("assets/games/two-rivers/data/dialogue/story.json"),
...json("assets/games/two-rivers/data/dialogue/scenes.json"),
...json("assets/games/two-rivers/data/dialogue/chapters.json")
}
};
test("content contracts, asset paths, and dialogue graphs are complete", () => {
assert.deepEqual(validateContent(content), []);
assert.equal(Object.keys(content.locations).length, 5);
assert.equal(Object.keys(content.endings).length, 4);
assert.equal(Object.keys(content.achievements).length, 12);
assert.equal(content.assets.images.filter((asset) => asset.type === "npc").length, 6);
assert.equal(content.assets.images.filter((asset) => asset.type === "item").length, 18);
assert.equal(new Set(content.assets.images.map((asset) => asset.id)).size, content.assets.images.length);
content.assets.images.forEach((asset) => assert.ok(fs.existsSync(path.join(root, asset.url))));
content.assets.images.forEach((asset) => {
const promptFile = asset.prompt?.split("#")[0];
assert.ok(promptFile && fs.existsSync(path.join(root, "assets/games/two-rivers", promptFile)), `missing prompt for ${asset.id}`);
});
content.assets.audio.forEach((asset) => {
assert.deepEqual(asset.urls.map((url) => path.extname(url)), [".webm", ".mp3"]);
asset.urls.forEach((url) => assert.ok(fs.existsSync(path.join(root, url))));
assert.ok(asset.source && asset.licence);
});
Object.values(content.dialogue).forEach((dialogue) => {
const reachable = new Set();
const visit = (id) => {
if (!id || reachable.has(id)) return;
reachable.add(id);
const node = dialogue.nodes[id];
visit(node.next);
node.choices?.forEach((choice) => visit(choice.next));
};
visit(dialogue.start);
assert.deepEqual([...reachable].sort(), Object.keys(dialogue.nodes).sort());
});
const authoredWords = Object.values(content.dialogue).flatMap((dialogue) => Object.values(dialogue.nodes))
.reduce((total, node) => total + (node.text.match(/\b[\w'-]+\b/g) || []).length +
(node.choices || []).reduce((choiceTotal, choice) => choiceTotal + (choice.text.match(/\b[\w'-]+\b/g) || []).length, 0), 0);
assert.ok(authoredWords >= 5000, `expected a feature-length script, found ${authoredWords} words`);
});
test("seven relationship values are independent, clamped, and qualitative", () => {
const store = createStore(freshState());
const before = store.get().relationships;
store.dispatch({type: "relationship", key: "trust", amount: 200});
assert.equal(store.get().relationships.trust, 100);
RELATIONSHIPS.filter((key) => key !== "trust").forEach((key) => assert.equal(store.get().relationships[key], before[key]));
assert.deepEqual([0, 25, 50, 75].map(relationshipStage), ["New", "Growing", "Warm", "Deep"]);
});
test("quest rewards are idempotent and requirements unlock deterministically", () => {
const store = createStore(freshState());
const quest = content.quests.garland;
content.items.flowers.forEach((item) => store.dispatch({
type: "questProgress", id: "garland", item, target: quest.target, reward: quest.reward
}));
const complete = store.get();
assert.equal(complete.quests.garland.status, "complete");
assert.ok(complete.unlockedCGs.includes("garland"));
const approval = complete.relationships.familyApproval;
store.dispatch({type: "questProgress", id: "garland", item: "jasmine", target: quest.target, reward: quest.reward});
assert.equal(store.get().relationships.familyApproval, approval);
store.dispatch({type: "puzzle", id: "cipher"});
assert.equal(requirementMet(store.get(), "flag:cipher_solved"), true);
});
test("all four guaranteed-romance endings are reachable", () => {
const profiles = {
scholar: {respect: 100, trust: 100, comfort: 90},
laughter: {humour: 100, comfort: 100, adventure: 80},
road: {adventure: 100, trust: 95, humour: 90},
families: {familyApproval: 100, respect: 90, comfort: 90}
};
Object.entries(profiles).forEach(([expected, values]) => {
const state = freshState();
Object.assign(state.relationships, {
trust: 5, respect: 5, comfort: 5, humour: 5,
adventure: 5, romance: 90, familyApproval: 5, ...values
});
state.endingBias[expected] = 12;
assert.equal(resolveEnding(state, content.endings), expected);
});
});
test("save slots validate and reject corruption", () => {
class MemoryStorage {
constructor() { this.values = new Map(); }
getItem(key) { return this.values.has(key) ? this.values.get(key) : null; }
setItem(key, value) { this.values.set(key, String(value)); }
removeItem(key) { this.values.delete(key); }
}
const storage = new MemoryStorage();
const saves = createSaveService(storage);
const state = freshState();
state.location = "garden";
saves.save("auto", state);
assert.equal(saves.load("auto").location, "garden");
saves.save("slot1", state);
assert.ok(saves.list().slot1);
const envelope = JSON.parse(storage.getItem(SAVE_KEY));
envelope.slots.auto.state.location = "unknown";
storage.setItem(SAVE_KEY, JSON.stringify(envelope));
assert.equal(saves.load("auto"), null);
});
test("page uses local pinned libraries and production source is clean", () => {
const page = read("play/two-rivers.org");
assert.match(page, /phaser-3\.90\.0\.min\.js/);
assert.match(page, /gsap-3\.13\.0\.min\.js/);
assert.match(page, /howler-2\.2\.4\.min\.js/);
assert.match(page, /games\/two-rivers\/gate\.js\?v=1\.0\.1/);
assert.match(page, /games\/two-rivers\/main\.js\?v=1\.0\.2/);
assert.doesNotMatch(page, /https?:\/\//);
const source = [
page,
read("assets/scripts/games/two-rivers/main.js"),
read("assets/scripts/games/two-rivers/gate.js"),
read("assets/scripts/games/two-rivers/core/state.js"),
read("assets/scripts/games/two-rivers/ui/interface.js"),
read("assets/scripts/games/two-rivers/scenes/game-scenes.js")
].join("\n");
assert.doesNotMatch(source, /\bTODO\b|placeholder implementation|lorem ipsum/i);
assert.doesNotMatch(source, /\bReact\b|\bVue\b|\bAngular\b|\bjQuery\b/);
assert.doesNotMatch(source, /\.mjs(?:["'?#]|$)/, "production runtime must use the server's JavaScript MIME mapping");
assert.ok(read("assets/styles/pages/two-rivers.css").length > 15000);
});
test("normalization rejects hostile IDs and caps collections", () => {
const state = freshState();
state.inventory = ["valid_item", "<script>", ...Array.from({length: 100}, (_, index) => `item_${index}`)];
const normalized = normalizeState(state);
assert.equal(normalized.inventory.includes("<script>"), false);
assert.ok(normalized.inventory.length <= 80);
});
test("minigame results improve but story rewards cannot be ground repeatedly", () => {
const store = createStore(freshState());
store.dispatch({type: "minigame", id: "archery", score: 35});
applyEffects(store, [{type: "relationship", key: "adventure", amount: 4}]);
const rewarded = store.get().relationships.adventure;
store.dispatch({type: "minigame", id: "archery", score: 82});
assert.equal(store.get().minigames.archery.score, 82);
assert.equal(store.get().relationships.adventure, rewarded);
assert.equal(store.get().minigames.archery.attempts, 2);
});
test("a deterministic complete route reaches a guaranteed romantic ending", () => {
const store = createStore(freshState());
const finishDialogue = (id, choiceIndex = 0) => {
const graph = content.dialogue[id];
let nodeId = graph.start;
const visited = new Set();
while (nodeId) {
assert.ok(!visited.has(nodeId), `cycle in ${id}/${nodeId}`);
visited.add(nodeId);
const node = graph.nodes[nodeId];
applyEffects(store, node.effects);
if (node.choices?.length) {
const choice = node.choices[Math.min(choiceIndex, node.choices.length - 1)];
applyEffects(store, choice.effects);
store.dispatch({type: "choice", id: `${id}:${nodeId}:${choice.next}`});
nodeId = choice.next;
} else nodeId = node.next;
}
store.dispatch({type: "flag", id: `dialogue_${id}`});
};
const exitReady = (location, hotspot) => {
const definition = content.locations[location].hotspots.find((spot) => spot.id === hotspot);
assert.ok(definition.requires.every((requirement) => requirementMet(store.get(), requirement)), `${location}/${hotspot} is blocked`);
};
["arrival", "delegation_walk", "courtyard_preparations", "ishan_protocol"].forEach((id) => finishDialogue(id));
exitReady("courtyard", "to_library");
store.dispatch({type: "goto", location: "library", position: 0.1});
["anwara_memory", "missing_folio", "library_search", "library_balcony"].forEach((id) => finishDialogue(id));
store.dispatch({type: "puzzle", id: "cipher"});
exitReady("library", "to_garden");
store.dispatch({type: "goto", location: "garden", position: 0.08});
["mira_garland", "garden_confidence", "garden_walk", "garden_history"].forEach((id) => finishDialogue(id));
store.dispatch({type: "puzzle", id: "flower_sequence"});
store.dispatch({type: "minigame", id: "archery", score: 70});
finishDialogue("archery_after");
exitReady("garden", "to_market");
store.dispatch({type: "goto", location: "market", position: 0.08});
content.items.flowers.forEach((item) => store.dispatch({type: "questProgress", id: "garland", item, target: 5, reward: content.quests.garland.reward}));
content.items.letters.forEach((item) => store.dispatch({type: "questProgress", id: "letters", item, target: 3, reward: content.quests.letters.reward}));
["nadia_letters", "market_walk", "market_founders", "rayhan_kitchen"].forEach((id) => finishDialogue(id));
store.dispatch({type: "minigame", id: "cooking", score: 60});
finishDialogue("anwara_truth");
exitReady("market", "to_festival");
store.dispatch({type: "goto", location: "pavilion", position: 0.08});
["ishan_stage", "pavilion_waiting", "resolution"].forEach((id) => finishDialogue(id));
store.dispatch({type: "minigame", id: "dance", score: 60});
["dance_after", "moonrise"].forEach((id) => finishDialogue(id));
const ending = resolveEnding(store.get(), content.endings);
assert.ok(["scholar", "laughter", "road", "families"].includes(ending));
assert.ok(store.get().relationships.romance > 0);
});
test("save recovery reports unknown versions and restores the previous valid envelope", () => {
class MemoryStorage {
constructor() { this.values = new Map(); }
getItem(key) { return this.values.has(key) ? this.values.get(key) : null; }
setItem(key, value) { this.values.set(key, String(value)); }
removeItem(key) { this.values.delete(key); }
}
const storage = new MemoryStorage();
const saves = createSaveService(storage);
saves.save("auto", freshState());
storage.setItem(`${SAVE_KEY}:backup`, storage.getItem(SAVE_KEY));
storage.setItem(SAVE_KEY, JSON.stringify({version: 99, slots: {}}));
assert.ok(saves.hasSave());
assert.equal(saves.recoveryInfo().source, "backup");
});