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) {
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
});
}));

View File

@@ -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
View File

View 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));

View File

@@ -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) {