Polish Princess Lima map data and interaction feedback
All checks were successful
Build Org Website / build (push) Successful in 38s
All checks were successful
Build Org Website / build (push) Successful in 38s
This commit is contained in:
@@ -234,29 +234,14 @@
|
||||
})
|
||||
});
|
||||
|
||||
function edges(openings) {
|
||||
const gap = Object.assign({ top: null, right: null, bottom: null, left: null }, openings || {});
|
||||
const result = [];
|
||||
function edges() {
|
||||
const thickness = 18;
|
||||
[["top", WIDTH, "x"], ["bottom", WIDTH, "x"]].forEach(([side, length]) => {
|
||||
const opening = gap[side];
|
||||
const y = side === "top" ? 0 : HEIGHT - thickness;
|
||||
if (!opening) result.push(rect(0, y, length, thickness, "edge"));
|
||||
else {
|
||||
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;
|
||||
return [
|
||||
rect(0, 0, WIDTH, thickness, "edge"),
|
||||
rect(0, HEIGHT - thickness, WIDTH, thickness, "edge"),
|
||||
rect(0, 0, thickness, HEIGHT, "edge"),
|
||||
rect(WIDTH - thickness, 0, thickness, HEIGHT, "edge")
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -323,7 +308,11 @@
|
||||
pickups: Object.freeze([["moon_coin", 360, 330], ["healing_tonic", 390, 525]])
|
||||
}),
|
||||
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([
|
||||
...edges({ top: [315, 405], bottom: [320, 410] }),
|
||||
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")
|
||||
]),
|
||||
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 })]),
|
||||
puzzle: Object.freeze({ id: "bridge_winches", type: "set", sequence: ["west", "east"], objects: [["west", 330, 290], ["east", 390, 290]] }),
|
||||
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", 335, 405], ["east", 395, 445]] }),
|
||||
pickups: Object.freeze([["moon_coin", 340, 575], ["healing_tonic", 400, 520]])
|
||||
}),
|
||||
camp: Object.freeze({
|
||||
@@ -426,6 +415,36 @@
|
||||
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 QUEST_IDS = Object.freeze(Object.keys(QUESTS));
|
||||
const ITEM_IDS = Object.freeze(Object.keys(ITEMS));
|
||||
@@ -433,7 +452,7 @@
|
||||
const NPC_IDS = Object.freeze(Object.keys(NPCS));
|
||||
|
||||
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
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -88,8 +88,11 @@
|
||||
}
|
||||
const debugRegion = localRegionPreview();
|
||||
if (debugRegion) {
|
||||
const firstSpawn = Object.entries(Data.MAPS[debugRegion].spawns)[0];
|
||||
return State.withPosition(parsed, debugRegion, firstSpawn[0], firstSpawn[1].x, firstSpawn[1].y, "south");
|
||||
const map = Data.MAPS[debugRegion];
|
||||
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);
|
||||
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
0
assets/scripts/pages/princess-lima-maps.js
Normal file → Executable file
@@ -182,7 +182,8 @@
|
||||
if (!puzzle || controller.getState().solvedPuzzles.includes(puzzle.id)) return;
|
||||
puzzle.objects.forEach(([id, x, y]) => {
|
||||
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}`;
|
||||
if (controller.getState().openedChests.includes(chestId)) return;
|
||||
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() {
|
||||
this.cursors = this.input.keyboard.createCursorKeys();
|
||||
this.keys = this.input.keyboard.addKeys({
|
||||
@@ -253,6 +266,7 @@
|
||||
let nearest = null;
|
||||
this.interactables.forEach((item) => {
|
||||
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;
|
||||
if (distance <= radius && distance < best) {
|
||||
best = distance;
|
||||
@@ -342,6 +356,7 @@
|
||||
if (state.solvedPuzzles.includes(puzzle.id)) return;
|
||||
if (puzzle.type === "set") {
|
||||
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);
|
||||
else controller.status(`${this.puzzleInput.length} / ${puzzle.sequence.length} mechanisms active.`);
|
||||
return;
|
||||
@@ -353,6 +368,7 @@
|
||||
controller.status("The sequence resets. Look for the environmental clue and try again.");
|
||||
return;
|
||||
}
|
||||
if (item.marker) item.marker.setText(`✓ ${readableId(item.id)}`);
|
||||
if (this.puzzleInput.length === puzzle.sequence.length) this.finishPuzzle(puzzle.id);
|
||||
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.audio.play("pickup");
|
||||
item.sprite.destroy();
|
||||
if (item.marker) item.marker.destroy();
|
||||
this.interactables = this.interactables.filter((entry) => entry !== item);
|
||||
}
|
||||
|
||||
@@ -585,5 +602,9 @@
|
||||
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 });
|
||||
}(typeof globalThis !== "undefined" ? globalThis : this));
|
||||
|
||||
@@ -40,7 +40,13 @@
|
||||
function activeObstacles(state, regionId) {
|
||||
const map = Data.MAPS[regionId];
|
||||
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) {
|
||||
|
||||
@@ -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.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: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.
|
||||
|
||||
58
docs/princess-lima-map-authoring.md
Normal file
58
docs/princess-lima-map-authoring.md
Normal 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.
|
||||
@@ -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"
|
||||
"<title>Rescue Princess Lima</title>\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"
|
||||
body
|
||||
"\n</body>\n</html>\n"))
|
||||
|
||||
22
play/rpg.org
22
play/rpg.org
@@ -107,16 +107,16 @@
|
||||
</noscript>
|
||||
</main>
|
||||
|
||||
<link rel="stylesheet" href="/assets/styles/pages/princess-lima-rpg.css?v=princess-lima-1.1.1" />
|
||||
<script src="/assets/scripts/vendor/phaser-4.1.0.min.js?v=princess-lima-1.1.1" 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-state.js?v=princess-lima-1.1.1" 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-audio.js?v=princess-lima-1.1.1" 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-maps.js?v=princess-lima-1.1.1" 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-scenes.js?v=princess-lima-1.1.1" defer></script>
|
||||
<script src="/assets/scripts/pages/princess-lima-game.js?v=princess-lima-1.1.1" defer></script>
|
||||
<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.2" 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.2" 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.2" 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.2" 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.2" defer></script>
|
||||
<script src="/assets/scripts/pages/princess-lima-game.js?v=princess-lima-1.1.2" defer></script>
|
||||
<!-- RPG-STANDALONE-END -->
|
||||
#+END_EXPORT
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||
|
||||
* 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/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>@@
|
||||
|
||||
14
sitemap.org
14
sitemap.org
@@ -112,11 +112,11 @@ flowchart TD
|
||||
n43 --> n53
|
||||
n54["Tag: insights"]
|
||||
n43 --> n54
|
||||
n55["Tag: emacs"]
|
||||
n55["Tag: reading"]
|
||||
n43 --> n55
|
||||
n56["Tag: maths"]
|
||||
n56["Tag: emacs"]
|
||||
n43 --> n56
|
||||
n57["Tag: reading"]
|
||||
n57["Tag: maths"]
|
||||
n43 --> n57
|
||||
n58{{"posts"}}
|
||||
root --> n58
|
||||
@@ -228,9 +228,9 @@ flowchart TD
|
||||
click n52 "tags/life.html" "Tag: life"
|
||||
click n53 "tags/education.html" "Tag: education"
|
||||
click n54 "tags/insights.html" "Tag: insights"
|
||||
click n55 "tags/emacs.html" "Tag: emacs"
|
||||
click n56 "tags/maths.html" "Tag: maths"
|
||||
click n57 "tags/reading.html" "Tag: reading"
|
||||
click n55 "tags/reading.html" "Tag: reading"
|
||||
click n56 "tags/emacs.html" "Tag: emacs"
|
||||
click n57 "tags/maths.html" "Tag: maths"
|
||||
click n59 "posts/posts-intro.html" "Posts Introduction"
|
||||
click n60 "posts/posts-list.html" "Posts List"
|
||||
click n62 "posts/career/solid-principles.html" "SOLID Principles"
|
||||
@@ -322,9 +322,9 @@ flowchart TD
|
||||
- [[file:tags/life.org][Tag: life]]
|
||||
- [[file:tags/education.org][Tag: education]]
|
||||
- [[file:tags/insights.org][Tag: insights]]
|
||||
- [[file:tags/reading.org][Tag: reading]]
|
||||
- [[file:tags/emacs.org][Tag: emacs]]
|
||||
- [[file:tags/maths.org][Tag: maths]]
|
||||
- [[file:tags/reading.org][Tag: reading]]
|
||||
- posts
|
||||
- [[file:posts/posts-intro.org][Posts Introduction]]
|
||||
- [[file:posts/posts-list.org][Posts List]]
|
||||
|
||||
@@ -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", () => {
|
||||
assert.match(rpgSource, /class="lima-hud"/);
|
||||
assert.match(rpgSource, /Inventory <kbd>I<\/kbd>/);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user