Polish Princess Lima map data and interaction feedback
All checks were successful
Build Org Website / build (push) Successful in 38s

This commit is contained in:
gitea-actions
2026-07-30 13:58:25 +01:00
parent 15b626ee0d
commit a6502b168d
13 changed files with 198 additions and 51 deletions

View File

@@ -234,29 +234,14 @@
}) })
}); });
function edges(openings) { function edges() {
const gap = Object.assign({ top: null, right: null, bottom: null, left: null }, openings || {});
const result = [];
const thickness = 18; const thickness = 18;
[["top", WIDTH, "x"], ["bottom", WIDTH, "x"]].forEach(([side, length]) => { return [
const opening = gap[side]; rect(0, 0, WIDTH, thickness, "edge"),
const y = side === "top" ? 0 : HEIGHT - thickness; rect(0, HEIGHT - thickness, WIDTH, thickness, "edge"),
if (!opening) result.push(rect(0, y, length, thickness, "edge")); rect(0, 0, thickness, HEIGHT, "edge"),
else { rect(WIDTH - thickness, 0, thickness, HEIGHT, "edge")
result.push(rect(0, y, opening[0], thickness, "edge")); ];
result.push(rect(opening[1], y, length - opening[1], thickness, "edge"));
}
});
[["left", HEIGHT, "y"], ["right", HEIGHT, "y"]].forEach(([side, length]) => {
const opening = gap[side];
const x = side === "left" ? 0 : WIDTH - thickness;
if (!opening) result.push(rect(x, 0, thickness, length, "edge"));
else {
result.push(rect(x, 0, thickness, opening[0], "edge"));
result.push(rect(x, opening[1], thickness, length - opening[1], "edge"));
}
});
return result;
} }
/* /*
@@ -323,7 +308,11 @@
pickups: Object.freeze([["moon_coin", 360, 330], ["healing_tonic", 390, 525]]) pickups: Object.freeze([["moon_coin", 360, 330], ["healing_tonic", 390, 525]])
}), }),
mountain: Object.freeze({ mountain: Object.freeze({
spawns: Object.freeze({ forestTrail: { x: 365, y: 660 }, campRoad: { x: 355, y: 55 } }), spawns: Object.freeze({
forestTrail: { x: 365, y: 660 },
campRoad: { x: 355, y: 55 },
bridgeControls: { x: 365, y: 430 }
}),
obstacles: Object.freeze([ obstacles: Object.freeze([
...edges({ top: [315, 405], bottom: [320, 410] }), ...edges({ top: [315, 405], bottom: [320, 410] }),
rect(18, 18, 230, 684, "cliff"), rect(500, 18, 202, 684, "cliff"), rect(18, 18, 230, 684, "cliff"), rect(500, 18, 202, 684, "cliff"),
@@ -337,8 +326,8 @@
exit("camp", 325, 0, 75, 48, "camp", "mountainRoad", "guardian_defeated", "Road to Blackridge Camp") exit("camp", 325, 0, 75, 48, "camp", "mountainRoad", "guardian_defeated", "Road to Blackridge Camp")
]), ]),
npcs: Object.freeze([["bram", 325, 250]]), npcs: Object.freeze([["bram", 325, 250]]),
enemies: Object.freeze([enemy("bat", 355, 455), enemy("guard", 360, 230), enemy("stone_guardian", 365, 115, { quest: "stone_guardian", boss: true, requires: "bridge_repaired", leash: 180 })]), enemies: Object.freeze([enemy("guard", 360, 230), enemy("stone_guardian", 365, 115, { quest: "stone_guardian", boss: true, requires: "bridge_repaired", leash: 180 })]),
puzzle: Object.freeze({ id: "bridge_winches", type: "set", sequence: ["west", "east"], objects: [["west", 330, 290], ["east", 390, 290]] }), puzzle: Object.freeze({ id: "bridge_winches", type: "set", sequence: ["west", "east"], objects: [["west", 335, 405], ["east", 395, 445]] }),
pickups: Object.freeze([["moon_coin", 340, 575], ["healing_tonic", 400, 520]]) pickups: Object.freeze([["moon_coin", 340, 575], ["healing_tonic", 400, 520]])
}), }),
camp: Object.freeze({ camp: Object.freeze({
@@ -426,6 +415,36 @@
Object.freeze(Object.assign({}, LEGACY_MAPS[id], VISUAL_LAYOUT[id])) Object.freeze(Object.assign({}, LEGACY_MAPS[id], VISUAL_LAYOUT[id]))
]))); ])));
function tiledPoint(name, type, x, y, properties) {
return Object.freeze({
name, type, x, y, point: true,
properties: Object.freeze(Object.assign({}, properties || {}))
});
}
function tiledLayer(name, objects) {
return Object.freeze({ type: "objectgroup", name, visible: true, objects: Object.freeze(objects) });
}
/*
* Tiled-compatible object-layer view of every authored region. Runtime
* collision reads these named layers, and the remaining layers give map
* editing/export tools a single inspectable contract for game objects.
*/
const MAP_LAYERS = Object.freeze(Object.fromEntries(Object.entries(MAPS).map(([regionId, map]) => [
regionId,
Object.freeze([
tiledLayer("Collision", map.obstacles),
tiledLayer("Dynamic Collision", map.dynamicObstacles || []),
tiledLayer("Exits", (map.exits || []).map((item) => Object.freeze(Object.assign({ type: "exit" }, item)))),
tiledLayer("NPCs", (map.npcs || []).map(([id, x, y]) => tiledPoint(id, "npc", x, y))),
tiledLayer("Enemies", (map.enemies || []).map((item) => tiledPoint(item.type, "enemy", item.x, item.y, item))),
tiledLayer("Puzzles", map.puzzle ? map.puzzle.objects.map(([id, x, y]) =>
tiledPoint(id, "puzzle", x, y, { puzzle: map.puzzle.id })) : []),
tiledLayer("Items", (map.pickups || []).map(([id, x, y]) => tiledPoint(id, "item", x, y)))
])
])));
const MAP_IDS = Object.freeze(Object.keys(MAPS)); const MAP_IDS = Object.freeze(Object.keys(MAPS));
const QUEST_IDS = Object.freeze(Object.keys(QUESTS)); const QUEST_IDS = Object.freeze(Object.keys(QUESTS));
const ITEM_IDS = Object.freeze(Object.keys(ITEMS)); const ITEM_IDS = Object.freeze(Object.keys(ITEMS));
@@ -433,7 +452,7 @@
const NPC_IDS = Object.freeze(Object.keys(NPCS)); const NPC_IDS = Object.freeze(Object.keys(NPCS));
return Object.freeze({ return Object.freeze({
WIDTH, HEIGHT, ITEMS, QUESTS, ENEMIES, NPCS, MAPS, WIDTH, HEIGHT, ITEMS, QUESTS, ENEMIES, NPCS, MAPS, MAP_LAYERS,
MAP_IDS, QUEST_IDS, ITEM_IDS, ENEMY_IDS, NPC_IDS MAP_IDS, QUEST_IDS, ITEM_IDS, ENEMY_IDS, NPC_IDS
}); });
})); }));

View File

@@ -88,8 +88,11 @@
} }
const debugRegion = localRegionPreview(); const debugRegion = localRegionPreview();
if (debugRegion) { if (debugRegion) {
const firstSpawn = Object.entries(Data.MAPS[debugRegion].spawns)[0]; const map = Data.MAPS[debugRegion];
return State.withPosition(parsed, debugRegion, firstSpawn[0], firstSpawn[1].x, firstSpawn[1].y, "south"); const requestedSpawn = new URLSearchParams(window.location.search).get("spawn");
const spawn = map.spawns[requestedSpawn] ? [requestedSpawn, map.spawns[requestedSpawn]]
: Object.entries(map.spawns)[0];
return State.withPosition(parsed, debugRegion, spawn[0], spawn[1].x, spawn[1].y, "south");
} }
const safe = Systems.nearestSafeSpawn(parsed, parsed.region, parsed.position.x, parsed.position.y); const safe = Systems.nearestSafeSpawn(parsed, parsed.region, parsed.position.x, parsed.position.y);
return State.withPosition(parsed, safe.region, safe.spawn, safe.x, safe.y, parsed.position.facing); return State.withPosition(parsed, safe.region, safe.spawn, safe.x, safe.y, parsed.position.facing);

0
assets/scripts/pages/princess-lima-maps.js Normal file → Executable file
View File

View File

@@ -182,7 +182,8 @@
if (!puzzle || controller.getState().solvedPuzzles.includes(puzzle.id)) return; if (!puzzle || controller.getState().solvedPuzzles.includes(puzzle.id)) return;
puzzle.objects.forEach(([id, x, y]) => { puzzle.objects.forEach(([id, x, y]) => {
const node = this.add.zone(x, y, 38, 38); const node = this.add.zone(x, y, 38, 38);
this.interactables.push({ type: "puzzle", id, puzzle, x, y, sprite: node }); const marker = this.createInteractionMarker(x, y, `${readableId(id)} · E`);
this.interactables.push({ type: "puzzle", id, puzzle, x, y, sprite: node, marker });
}); });
} }
@@ -191,10 +192,22 @@
const chestId = `${this.regionId}-${id}-${index}`; const chestId = `${this.regionId}-${id}-${index}`;
if (controller.getState().openedChests.includes(chestId)) return; if (controller.getState().openedChests.includes(chestId)) return;
const item = this.add.zone(x, y, 34, 34); const item = this.add.zone(x, y, 34, 34);
this.interactables.push({ type: "pickup", id, chestId, x, y, sprite: item }); const marker = this.createInteractionMarker(x, y, `${Data.ITEMS[id].name} · E`);
this.interactables.push({ type: "pickup", id, chestId, x, y, sprite: item, marker });
}); });
} }
createInteractionMarker(x, y, label) {
return this.add.text(x, y - 32, label, {
fontFamily: "monospace",
fontSize: "13px",
fontStyle: "bold",
color: "#fff6ce",
backgroundColor: "#111827",
padding: { x: 7, y: 4 }
}).setOrigin(0.5).setDepth(900).setVisible(false);
}
createInput() { createInput() {
this.cursors = this.input.keyboard.createCursorKeys(); this.cursors = this.input.keyboard.createCursorKeys();
this.keys = this.input.keyboard.addKeys({ this.keys = this.input.keyboard.addKeys({
@@ -253,6 +266,7 @@
let nearest = null; let nearest = null;
this.interactables.forEach((item) => { this.interactables.forEach((item) => {
const distance = Math.hypot(item.x - this.player.x, item.y - this.player.y); const distance = Math.hypot(item.x - this.player.x, item.y - this.player.y);
if (item.marker) item.marker.setVisible(distance <= 92);
const radius = item.type === "exit" ? 70 : 54; const radius = item.type === "exit" ? 70 : 54;
if (distance <= radius && distance < best) { if (distance <= radius && distance < best) {
best = distance; best = distance;
@@ -342,6 +356,7 @@
if (state.solvedPuzzles.includes(puzzle.id)) return; if (state.solvedPuzzles.includes(puzzle.id)) return;
if (puzzle.type === "set") { if (puzzle.type === "set") {
if (!this.puzzleInput.includes(item.id)) this.puzzleInput.push(item.id); if (!this.puzzleInput.includes(item.id)) this.puzzleInput.push(item.id);
if (item.marker) item.marker.setText(`${readableId(item.id)}`);
if (this.puzzleInput.length === puzzle.sequence.length) this.finishPuzzle(puzzle.id); if (this.puzzleInput.length === puzzle.sequence.length) this.finishPuzzle(puzzle.id);
else controller.status(`${this.puzzleInput.length} / ${puzzle.sequence.length} mechanisms active.`); else controller.status(`${this.puzzleInput.length} / ${puzzle.sequence.length} mechanisms active.`);
return; return;
@@ -353,6 +368,7 @@
controller.status("The sequence resets. Look for the environmental clue and try again."); controller.status("The sequence resets. Look for the environmental clue and try again.");
return; return;
} }
if (item.marker) item.marker.setText(`${readableId(item.id)}`);
if (this.puzzleInput.length === puzzle.sequence.length) this.finishPuzzle(puzzle.id); if (this.puzzleInput.length === puzzle.sequence.length) this.finishPuzzle(puzzle.id);
else controller.status(`${item.id} answers. ${this.puzzleInput.length} / ${puzzle.sequence.length}.`); else controller.status(`${item.id} answers. ${this.puzzleInput.length} / ${puzzle.sequence.length}.`);
} }
@@ -377,6 +393,7 @@
controller.setState(state, `${Data.ITEMS[item.id].name} collected.`); controller.setState(state, `${Data.ITEMS[item.id].name} collected.`);
controller.audio.play("pickup"); controller.audio.play("pickup");
item.sprite.destroy(); item.sprite.destroy();
if (item.marker) item.marker.destroy();
this.interactables = this.interactables.filter((entry) => entry !== item); this.interactables = this.interactables.filter((entry) => entry !== item);
} }
@@ -585,5 +602,9 @@
return "Interact · E / Enter"; return "Interact · E / Enter";
} }
function readableId(value) {
return String(value).replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
}
root.PrincessLimaScenes = Object.freeze({ createSceneClasses, playerFrame }); root.PrincessLimaScenes = Object.freeze({ createSceneClasses, playerFrame });
}(typeof globalThis !== "undefined" ? globalThis : this)); }(typeof globalThis !== "undefined" ? globalThis : this));

View File

@@ -40,7 +40,13 @@
function activeObstacles(state, regionId) { function activeObstacles(state, regionId) {
const map = Data.MAPS[regionId]; const map = Data.MAPS[regionId];
if (!map) return []; if (!map) return [];
return map.obstacles.concat((map.dynamicObstacles || []).filter((item) => !state.unlockedRoutes.includes(item.opensWith))); const layers = Data.MAP_LAYERS[regionId] || [];
const collision = layers.find((layer) => layer.name === "Collision");
const dynamic = layers.find((layer) => layer.name === "Dynamic Collision");
return (collision ? collision.objects : map.obstacles).concat(
(dynamic ? dynamic.objects : map.dynamicObstacles || [])
.filter((item) => !state.unlockedRoutes.includes(item.opensWith))
);
} }
function isSafePosition(state, regionId, x, y) { function isSafePosition(state, regionId, x, y) {

View File

@@ -803,3 +803,10 @@ at <ScriptBlock>, /home/zaine/master-folder/org-platform/org_web/build-logs/gite
2026-07-30T12:23:36.7830664+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python. 2026-07-30T12:23:36.7830664+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
2026-07-30T12:23:36.8532292+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0 2026-07-30T12:23:36.8532292+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
2026-07-30T12:23:37.1987243+01:00 [INFO] Sent authoring server test notification. 2026-07-30T12:23:37.1987243+01:00 [INFO] Sent authoring server test notification.
2026-07-30T12:48:57.8025412+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
2026-07-30T12:48:57.8102120+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
2026-07-30T12:48:58.1535872+01:00 [ERROR] Failed to analyze job 783: Cannot bind argument to parameter 'LogText' because it is an empty string.
2026-07-30T12:48:58.6713722+01:00 [INFO] Sent build status notification with 1 embed(s).
2026-07-30T12:48:58.6913478+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
2026-07-30T12:48:58.7600936+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
2026-07-30T12:48:59.0618648+01:00 [INFO] Sent authoring server test notification.

View File

@@ -0,0 +1,58 @@
# Princess Lima map authoring
## Framework
Use [Tiled](https://www.mapeditor.org/) as the visual authoring tool and Phaser
as the runtime. Every region is a 720×720 orthogonal map whose locked image
layer is the corresponding frame from `regional-style-atlas.png`.
The runtime contract mirrors Tiled object groups in
`PrincessLimaData.MAP_LAYERS`. Every map has these named layers:
1. `Collision`
2. `Dynamic Collision`
3. `Exits`
4. `NPCs`
5. `Enemies`
6. `Puzzles`
7. `Items`
Do not paint collision or interaction geometry into the finished artwork.
Production collision objects are invisible.
## Collision rules
- Trace only terrain that is visibly solid: buildings, walls, cliff faces,
deep water, fences, furniture, and the four outer edges.
- Keep paths, doors, bridges, stairs, and floor tiles walkable.
- Use rectangles while the game uses Phaser Arcade Physics. A future switch to
Matter Physics can use Tiled polygon objects without approximating them.
- Every dynamic blocker needs an `opensWith` property matching a validated
route ID.
- Never put a puzzle control, item, actor, or safe spawn inside collision.
- Keep enemies away from puzzle controls and safe spawns.
## Interaction rules
- Exits, NPCs, enemies, puzzles, and items belong on their named object layer.
- Puzzle and item labels appear only within 92 pixels of the player.
- The interaction radius is 54 pixels; exit portals use 70 pixels.
- Every mechanism must be reachable before the state change it triggers.
- Interaction labels must use the object name rather than an unexplained shape.
## Verification
On localhost, use:
```text
/play/rpg.html?region=mountain&spawn=bridgeControls
/play/rpg.html?region=mountain&spawn=bridgeControls&collisionDebug=1
```
Before publishing:
1. Compare the clean rendered map with its locked source image.
2. Enable collision debugging and check every visible boundary.
3. Walk each route in both its locked and unlocked states.
4. Confirm all object-layer points pass the collision-safety tests.
5. Confirm ordinary production URLs show no collision geometry.

View File

@@ -134,7 +134,7 @@ Created with %c on <a href=\"https://www.archlinux.org/\">Arch</a> \
"<meta name=\"description\" content=\"A standalone fantasy RPG about rescuing Princess Lima.\" />\n" "<meta name=\"description\" content=\"A standalone fantasy RPG about rescuing Princess Lima.\" />\n"
"<title>Rescue Princess Lima</title>\n" "<title>Rescue Princess Lima</title>\n"
"<link rel=\"icon\" href=\"/assets/icons/icons8-film-tape-100.png\" />\n" "<link rel=\"icon\" href=\"/assets/icons/icons8-film-tape-100.png\" />\n"
"<link rel=\"stylesheet\" href=\"/assets/styles/pages/princess-lima-rpg.css?v=princess-lima-1.1.1\" />\n" "<link rel=\"stylesheet\" href=\"/assets/styles/pages/princess-lima-rpg.css?v=princess-lima-1.1.2\" />\n"
"</head>\n<body class=\"princess-lima-page\">\n" "</head>\n<body class=\"princess-lima-page\">\n"
body body
"\n</body>\n</html>\n")) "\n</body>\n</html>\n"))

View File

@@ -107,16 +107,16 @@
</noscript> </noscript>
</main> </main>
<link rel="stylesheet" href="/assets/styles/pages/princess-lima-rpg.css?v=princess-lima-1.1.1" /> <link rel="stylesheet" href="/assets/styles/pages/princess-lima-rpg.css?v=princess-lima-1.1.2" />
<script src="/assets/scripts/vendor/phaser-4.1.0.min.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/vendor/phaser-4.1.0.min.js?v=princess-lima-1.1.2" defer></script>
<script src="/assets/scripts/pages/princess-lima-data.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/pages/princess-lima-data.js?v=princess-lima-1.1.2" defer></script>
<script src="/assets/scripts/pages/princess-lima-state.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/pages/princess-lima-state.js?v=princess-lima-1.1.2" defer></script>
<script src="/assets/scripts/pages/princess-lima-systems.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/pages/princess-lima-systems.js?v=princess-lima-1.1.2" defer></script>
<script src="/assets/scripts/pages/princess-lima-audio.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/pages/princess-lima-audio.js?v=princess-lima-1.1.2" defer></script>
<script src="/assets/scripts/pages/princess-lima-ui.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/pages/princess-lima-ui.js?v=princess-lima-1.1.2" defer></script>
<script src="/assets/scripts/pages/princess-lima-maps.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/pages/princess-lima-maps.js?v=princess-lima-1.1.2" defer></script>
<script src="/assets/scripts/pages/princess-lima-intro.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/pages/princess-lima-intro.js?v=princess-lima-1.1.2" defer></script>
<script src="/assets/scripts/pages/princess-lima-scenes.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/pages/princess-lima-scenes.js?v=princess-lima-1.1.2" defer></script>
<script src="/assets/scripts/pages/princess-lima-game.js?v=princess-lima-1.1.1" defer></script> <script src="/assets/scripts/pages/princess-lima-game.js?v=princess-lima-1.1.2" defer></script>
<!-- RPG-STANDALONE-END --> <!-- RPG-STANDALONE-END -->
#+END_EXPORT #+END_EXPORT

View File

@@ -4,7 +4,7 @@
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@ See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
* Posts: * Posts:
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">30-07-2026 12:23</span>@@ - [[file:career/career-list.org][Career List]] @@html:<span class="post-date">30-07-2026 12:42</span>@@
- [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:<span class="post-date">01-06-2026 10:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@ - [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:<span class="post-date">01-06-2026 10:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@ - [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@ - [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@

View File

@@ -112,11 +112,11 @@ flowchart TD
n43 --> n53 n43 --> n53
n54["Tag: insights"] n54["Tag: insights"]
n43 --> n54 n43 --> n54
n55["Tag: emacs"] n55["Tag: reading"]
n43 --> n55 n43 --> n55
n56["Tag: maths"] n56["Tag: emacs"]
n43 --> n56 n43 --> n56
n57["Tag: reading"] n57["Tag: maths"]
n43 --> n57 n43 --> n57
n58{{"posts"}} n58{{"posts"}}
root --> n58 root --> n58
@@ -228,9 +228,9 @@ flowchart TD
click n52 "tags/life.html" "Tag: life" click n52 "tags/life.html" "Tag: life"
click n53 "tags/education.html" "Tag: education" click n53 "tags/education.html" "Tag: education"
click n54 "tags/insights.html" "Tag: insights" click n54 "tags/insights.html" "Tag: insights"
click n55 "tags/emacs.html" "Tag: emacs" click n55 "tags/reading.html" "Tag: reading"
click n56 "tags/maths.html" "Tag: maths" click n56 "tags/emacs.html" "Tag: emacs"
click n57 "tags/reading.html" "Tag: reading" click n57 "tags/maths.html" "Tag: maths"
click n59 "posts/posts-intro.html" "Posts Introduction" click n59 "posts/posts-intro.html" "Posts Introduction"
click n60 "posts/posts-list.html" "Posts List" click n60 "posts/posts-list.html" "Posts List"
click n62 "posts/career/solid-principles.html" "SOLID Principles" click n62 "posts/career/solid-principles.html" "SOLID Principles"
@@ -322,9 +322,9 @@ flowchart TD
- [[file:tags/life.org][Tag: life]] - [[file:tags/life.org][Tag: life]]
- [[file:tags/education.org][Tag: education]] - [[file:tags/education.org][Tag: education]]
- [[file:tags/insights.org][Tag: insights]] - [[file:tags/insights.org][Tag: insights]]
- [[file:tags/reading.org][Tag: reading]]
- [[file:tags/emacs.org][Tag: emacs]] - [[file:tags/emacs.org][Tag: emacs]]
- [[file:tags/maths.org][Tag: maths]] - [[file:tags/maths.org][Tag: maths]]
- [[file:tags/reading.org][Tag: reading]]
- posts - posts
- [[file:posts/posts-intro.org][Posts Introduction]] - [[file:posts/posts-intro.org][Posts Introduction]]
- [[file:posts/posts-list.org][Posts List]] - [[file:posts/posts-list.org][Posts List]]

View File

@@ -48,6 +48,16 @@ test("all authored actors and interaction points sit outside visible terrain col
}); });
}); });
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("keyboard-only page contains one left HUD and no touch controls", () => { test("keyboard-only page contains one left HUD and no touch controls", () => {
assert.match(rpgSource, /class="lima-hud"/); assert.match(rpgSource, /class="lima-hud"/);
assert.match(rpgSource, /Inventory <kbd>I<\/kbd>/); assert.match(rpgSource, /Inventory <kbd>I<\/kbd>/);

View File

@@ -51,6 +51,29 @@ test("all named spawns are collision-safe and invalid positions recover", () =>
assert.equal(systems.isSafePosition(current, recovered.region, recovered.x, recovered.y), true); 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", () => { test("quest progression grants rewards once and unlocks routes", () => {
let current = state.fresh("Ada", "azure"); let current = state.fresh("Ada", "azure");
current = systems.completeQuest(current, "aftermath"); current = systems.completeQuest(current, "aftermath");