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) {
|
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
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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
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;
|
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));
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
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"
|
"<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"))
|
||||||
|
|||||||
22
play/rpg.org
22
play/rpg.org
@@ -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
|
||||||
|
|||||||
@@ -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>@@
|
||||||
|
|||||||
14
sitemap.org
14
sitemap.org
@@ -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]]
|
||||||
|
|||||||
@@ -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>/);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user