Refine Princess Lima for v2 presentation
All checks were successful
Build Org Website / build (push) Successful in 1m2s
All checks were successful
Build Org Website / build (push) Successful in 1m2s
This commit is contained in:
@@ -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");
|
||||
});
|
||||
|
||||
60
tests/princess-lima-v2-state.test.cjs
Normal file
60
tests/princess-lima-v2-state.test.cjs
Normal 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");
|
||||
});
|
||||
Reference in New Issue
Block a user