Refine Princess Lima for v2 presentation
All checks were successful
Build Org Website / build (push) Successful in 1m2s

This commit is contained in:
gitea-actions
2026-07-30 15:00:45 +01:00
parent a6502b168d
commit 6066f486de
35 changed files with 74848 additions and 141 deletions

View File

@@ -2,74 +2,93 @@ 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-data.js");
const state = require("../assets/scripts/pages/princess-lima-state.js");
const systems = require("../assets/scripts/pages/princess-lima-systems.js");
const intro = require("../assets/scripts/pages/princess-lima-intro.js");
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 rpgSource = fs.readFileSync(path.join(root, "play/rpg.org"), "utf8");
const sceneSource = fs.readFileSync(path.join(root, "assets/scripts/pages/princess-lima-scenes.js"), "utf8");
const source = (file) => fs.readFileSync(path.join(root, file), "utf8");
test("all nine regions use the exact atlas artwork and valid collision-safe transitions", () => {
const mapsSource = fs.readFileSync(path.join(root, "assets/scripts/pages/princess-lima-maps.js"), "utf8");
data.MAP_IDS.forEach((region) => {
assert.match(mapsSource, new RegExp(`${region}:\\s*\\d`));
Object.values(data.MAPS[region].spawns).forEach((spawn) => {
assert.ok(spawn.x > 0 && spawn.x < data.WIDTH);
assert.ok(spawn.y > 0 && spawn.y < data.HEIGHT);
});
data.MAPS[region].exits.forEach((exit) => {
assert.ok(data.MAPS[exit.target]);
assert.ok(data.MAPS[exit.target].spawns[exit.spawn]);
});
});
assert.equal(data.WIDTH, data.HEIGHT);
assert.match(mapsSource, /\.setAlpha\(1\)/);
assert.doesNotMatch(mapsSource, /tilemap|tileSprite|add\.circle|add\.ellipse|drawDecor|drawObstacle/);
assert.doesNotMatch(sceneSource, /lima-world-tiles|world-tiles\.png/);
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 authored actors and interaction points sit outside visible terrain collision", () => {
const auditState = state.fresh("Collision audit", "azure");
auditState.unlockedRoutes = ["bridge_repaired"];
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 = data.MAPS[region];
const points = [
...Object.entries(map.spawns).map(([name, point]) => [`spawn:${name}`, point.x, point.y]),
...(map.npcs || []).map(([name, x, y]) => [`npc:${name}`, x, y]),
...(map.enemies || []).map((enemy, index) => [`enemy:${enemy.type}:${index}`, enemy.x, enemy.y]),
...(map.pickups || []).map(([name, x, y], index) => [`pickup:${name}:${index}`, x, y]),
...(map.puzzle ? map.puzzle.objects.map(([name, x, y]) => [`puzzle:${name}`, x, y]) : [])
];
points.forEach(([name, x, y]) => {
assert.equal(systems.isSafePosition(auditState, region, x, y), true, `${region} ${name} must be collision-safe`);
});
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("every region exposes named Tiled-compatible collision and object layers", () => {
const expected = ["Collision", "Dynamic Collision", "Exits", "NPCs", "Enemies", "Puzzles", "Items"];
data.MAP_IDS.forEach((region) => {
const layers = data.MAP_LAYERS[region];
assert.deepEqual(layers.map((layer) => layer.name), expected);
assert.ok(layers.every((layer) => layer.type === "objectgroup"));
assert.equal(layers.find((layer) => layer.name === "Collision").objects, data.MAPS[region].obstacles);
});
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("keyboard-only page contains one left HUD and no touch controls", () => {
assert.match(rpgSource, /class="lima-hud"/);
assert.match(rpgSource, /Inventory <kbd>I<\/kbd>/);
assert.match(rpgSource, /Menu <kbd>M<\/kbd>/);
assert.doesNotMatch(rpgSource, /data-lima-touch|data-lima-move|Touch controls|data-lima-attack/);
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("opening cinematic is complete, replayable and accessible", () => {
assert.equal(intro.BEATS.length, 5);
assert.ok(intro.BEATS.reduce((total, beat) => total + beat.duration, 0) >= 60000);
assert.ok(intro.BEATS.every((beat) => beat.subtitle.length > 20));
assert.match(rpgSource, /data-lima-replay-intro/);
assert.match(rpgSource, /data-lima-intro-pause/);
assert.match(rpgSource, /Hold Escape to skip/);
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");
});

View File

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