Rebuild Princess Lima maps around the new visual atlas
All checks were successful
Build Org Website / build (push) Successful in 38s

This commit is contained in:
gitea-actions
2026-07-30 12:48:17 +01:00
parent a0c78929b3
commit 15b626ee0d
21 changed files with 305 additions and 194 deletions

View File

@@ -6,7 +6,7 @@
}(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const WIDTH = 1280;
const WIDTH = 720;
const HEIGHT = 720;
function rect(x, y, width, height, kind) {
@@ -80,7 +80,7 @@
malrec: Object.freeze({ name: "Lord Malrec", frame: 8, dialogue: ["Lima's oath could command every border lord. With it, I would end their endless quarrels.", "If the kingdom will not accept peace, shadow will make it obey."] })
});
const MAPS = Object.freeze({
const LEGACY_MAPS = Object.freeze({
village: Object.freeze({
name: "Broken Village", chapter: 1, palette: ["#263b2d", "#596b3a", "#b49355", "#3b2d2a"],
spawns: Object.freeze({ start: { x: 160, y: 570 }, square: { x: 640, y: 420 }, forestRoad: { x: 1110, y: 350 } }),
@@ -234,6 +234,198 @@
})
});
function edges(openings) {
const gap = Object.assign({ top: null, right: null, bottom: null, left: null }, openings || {});
const result = [];
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;
}
/*
* Coordinates below are authored against the nine 418x418 atlas panels
* displayed at 720x720. They describe only visible, solid terrain. The
* geometry is never rendered in production; ?collisionDebug=1 reveals it.
*/
const VISUAL_LAYOUT = Object.freeze({
village: Object.freeze({
spawns: Object.freeze({ start: { x: 365, y: 665 }, square: { x: 365, y: 355 }, forestRoad: { x: 365, y: 52 } }),
obstacles: Object.freeze([
...edges({ top: [318, 414], bottom: [330, 430] }),
rect(18, 18, 176, 205, "house"), rect(454, 18, 248, 205, "house"),
rect(18, 248, 118, 132, "cart"), rect(18, 475, 286, 227, "house"),
rect(498, 338, 98, 104, "well"), rect(466, 465, 236, 92, "wall"),
rect(548, 560, 154, 142, "cart")
]),
exits: Object.freeze([exit("forest-road", 330, 0, 70, 48, "forest", "villagePath", "village_defended", "Road to the Whispering Woods")]),
npcs: Object.freeze([["elder", 365, 292], ["bram", 270, 350], ["nia", 466, 350]]),
enemies: Object.freeze([
enemy("slime", 260, 435, { quest: "village_defence", requires: "aftermath_complete" }),
enemy("bandit", 365, 500, { quest: "village_defence", requires: "aftermath_complete" }),
enemy("slime", 470, 420, { quest: "village_defence", requires: "aftermath_complete" })
]),
pickups: Object.freeze([["silver_leaf", 225, 265], ["silver_leaf", 515, 280], ["moon_coin", 440, 625]])
}),
forest: Object.freeze({
spawns: Object.freeze({ villagePath: { x: 355, y: 660 }, ruinsPath: { x: 660, y: 392 }, mountainPath: { x: 355, y: 55 } }),
obstacles: Object.freeze([
...edges({ top: [315, 405], right: [348, 438], bottom: [318, 408] }),
rect(18, 18, 220, 330, "trees"), rect(18, 418, 218, 284, "trees"),
rect(470, 18, 232, 184, "trees"), rect(535, 202, 167, 146, "water"),
rect(535, 438, 167, 264, "water"), rect(238, 18, 80, 178, "trees"),
rect(235, 515, 72, 187, "trees"), rect(438, 475, 97, 227, "trees"),
rect(470, 250, 65, 105, "trees")
]),
exits: Object.freeze([
exit("village", 325, 672, 75, 48, "village", "forestRoad", null, "Return to the village"),
exit("ruins", 665, 360, 55, 72, "ruins", "forestDoor", "guide_found", "Sunken Ruins"),
exit("mountain", 325, 0, 72, 48, "mountain", "forestTrail", "briar_defeated", "Mountain trail")
]),
npcs: Object.freeze([["tovin", 390, 535]]),
enemies: Object.freeze([
enemy("wolf", 330, 225), enemy("slime", 420, 580), enemy("bandit", 430, 310),
enemy("wolf", 560, 400), enemy("briar_wolf", 360, 110, { quest: "wolf_miniboss", boss: true, requires: "ruins_complete", leash: 190 })
]),
puzzle: Object.freeze({ id: "forest_stones", type: "sequence", sequence: ["sapling", "oak", "elder"], objects: [["sapling", 340, 565], ["oak", 365, 420], ["elder", 405, 270]] }),
pickups: Object.freeze([["silver_leaf", 350, 610], ["silver_leaf", 445, 180], ["healing_tonic", 585, 390]])
}),
ruins: Object.freeze({
spawns: Object.freeze({ forestDoor: { x: 360, y: 660 } }),
obstacles: Object.freeze([
...edges({ bottom: [320, 410] }),
rect(18, 18, 248, 268, "water"), rect(454, 18, 248, 268, "water"),
rect(18, 286, 180, 416, "water"), rect(522, 286, 180, 416, "water"),
rect(198, 460, 92, 242, "water"), rect(430, 460, 92, 242, "water"),
rect(266, 18, 55, 172, "ruin-wall"), rect(399, 18, 55, 172, "ruin-wall"),
rect(198, 286, 92, 82, "water"), rect(430, 286, 92, 82, "water")
]),
exits: Object.freeze([exit("forest", 325, 672, 75, 48, "forest", "ruinsPath", null, "Return to the woods")]),
npcs: Object.freeze([]),
enemies: Object.freeze([enemy("bat", 350, 220), enemy("bat", 400, 370), enemy("slime", 350, 500)]),
puzzle: Object.freeze({ id: "ruin_braziers", type: "sequence", sequence: ["dawn", "noon", "dusk", "night"], objects: [["dawn", 270, 430], ["noon", 325, 245], ["dusk", 450, 430], ["night", 395, 245]] }),
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 } }),
obstacles: Object.freeze([
...edges({ top: [315, 405], bottom: [320, 410] }),
rect(18, 18, 230, 684, "cliff"), rect(500, 18, 202, 684, "cliff"),
rect(248, 18, 67, 208, "cliff"), rect(405, 18, 95, 245, "cliff"),
rect(248, 430, 62, 272, "cliff"), rect(430, 455, 70, 247, "cliff"),
rect(248, 295, 58, 85, "cliff"), rect(440, 315, 60, 92, "cliff")
]),
dynamicObstacles: Object.freeze([Object.freeze({ id: "bridge", x: 306, y: 315, width: 134, height: 58, kind: "chasm", opensWith: "bridge_repaired" })]),
exits: Object.freeze([
exit("forest", 325, 672, 75, 48, "forest", "mountainPath", null, "Return to the woods"),
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]] }),
pickups: Object.freeze([["moon_coin", 340, 575], ["healing_tonic", 400, 520]])
}),
camp: Object.freeze({
spawns: Object.freeze({ mountainRoad: { x: 360, y: 660 }, fortressRoad: { x: 360, y: 55 } }),
obstacles: Object.freeze([
...edges({ top: [320, 405], bottom: [320, 405] }),
rect(18, 18, 255, 175, "tent"), rect(455, 18, 247, 190, "tower"),
rect(18, 518, 250, 184, "fence"), rect(468, 515, 234, 187, "fence"),
rect(18, 193, 42, 325, "fence"), rect(660, 208, 42, 307, "fence"),
rect(90, 365, 155, 75, "weapons"), rect(482, 380, 150, 78, "tent")
]),
exits: Object.freeze([
exit("mountain", 325, 672, 75, 48, "mountain", "campRoad", null, "Return to the pass"),
exit("fortress", 325, 0, 75, 48, "fortressExterior", "campGate", "emblem_found", "Fortress road")
]),
npcs: Object.freeze([["elowen", 440, 500]]),
enemies: Object.freeze([enemy("guard", 250, 300, { quest: "free_scout" }), enemy("guard", 495, 300, { quest: "free_scout" }), enemy("captain", 420, 585, { quest: "free_scout", boss: true, leash: 180 })]),
pickups: Object.freeze([["healing_tonic", 120, 260], ["moon_coin", 585, 250]])
}),
fortressExterior: Object.freeze({
spawns: Object.freeze({ campGate: { x: 360, y: 660 }, innerGate: { x: 360, y: 455 } }),
obstacles: Object.freeze([
...edges({ bottom: [315, 405] }),
rect(18, 18, 286, 430, "fortress-wall"), rect(416, 18, 286, 430, "fortress-wall"),
rect(18, 448, 266, 254, "chasm"), rect(436, 448, 266, 254, "chasm"),
rect(284, 18, 56, 310, "tower"), rect(380, 18, 56, 310, "tower")
]),
exits: Object.freeze([
exit("camp", 325, 672, 75, 48, "camp", "fortressRoad", null, "Return to Blackridge"),
exit("interior", 335, 285, 50, 64, "fortressInterior", "frontHall", "emblem_found", "Enter the fortress")
]),
npcs: Object.freeze([]),
enemies: Object.freeze([enemy("guard", 330, 570), enemy("guard", 390, 445), enemy("guard", 350, 365)]),
pickups: Object.freeze([["healing_tonic", 410, 520]])
}),
fortressInterior: Object.freeze({
spawns: Object.freeze({ frontHall: { x: 360, y: 660 }, throneDoor: { x: 360, y: 95 } }),
obstacles: Object.freeze([
...edges({ top: [320, 400], bottom: [320, 400] }),
rect(18, 18, 300, 160, "wall"), rect(402, 18, 300, 160, "wall"),
rect(18, 178, 250, 175, "cell"), rect(452, 178, 250, 175, "cell"),
rect(18, 420, 250, 282, "cell"), rect(452, 420, 250, 282, "cell"),
rect(268, 178, 50, 110, "wall"), rect(402, 178, 50, 110, "wall")
]),
exits: Object.freeze([
exit("outside", 325, 672, 75, 48, "fortressExterior", "innerGate", null, "Leave the fortress"),
exit("throne", 325, 0, 75, 48, "bossArena", "entrance", "wards_broken", "Throne of Night")
]),
npcs: Object.freeze([["prisoner", 280, 375], ["elowen", 440, 375]]),
enemies: Object.freeze([enemy("guard", 320, 500), enemy("guard", 400, 500), enemy("bat", 360, 240)]),
puzzle: Object.freeze({ id: "shadow_wards", type: "set", sequence: ["moon", "crown", "flame"], objects: [["moon", 335, 205], ["crown", 360, 390], ["flame", 385, 205]] }),
pickups: Object.freeze([["prison_key", 360, 540], ["healing_tonic", 425, 580]])
}),
bossArena: Object.freeze({
spawns: Object.freeze({ entrance: { x: 360, y: 650 } }),
obstacles: Object.freeze([
...edges({ top: [320, 400] }),
rect(18, 18, 175, 235, "wall"), rect(527, 18, 175, 235, "wall"),
rect(18, 545, 190, 157, "wall"), rect(512, 545, 190, 157, "wall"),
rect(250, 18, 220, 104, "throne")
]),
exits: Object.freeze([exit("chamber", 330, 0, 60, 48, "chamber", "door", "malrec_defeated", "Princess Lima's chamber")]),
npcs: Object.freeze([["malrec", 360, 165]]),
enemies: Object.freeze([enemy("malrec", 360, 245, { quest: "defeat_malrec", boss: true, requires: "boss_started", leash: 300 })]),
puzzle: Object.freeze({ id: "sun_pedestals", type: "set", sequence: ["west", "east"], objects: [["west", 220, 390], ["east", 500, 390]] }),
pickups: Object.freeze([])
}),
chamber: Object.freeze({
spawns: Object.freeze({ door: { x: 360, y: 660 } }),
obstacles: Object.freeze([
...edges({ bottom: [320, 400] }),
rect(18, 18, 265, 120, "wall"), rect(438, 18, 264, 120, "wall"),
rect(465, 155, 220, 190, "bed"), rect(460, 475, 225, 190, "table"),
rect(35, 475, 125, 190, "fountain")
]),
exits: Object.freeze([]),
npcs: Object.freeze([["lima", 360, 180]]),
enemies: Object.freeze([]),
pickups: Object.freeze([])
})
});
const MAPS = Object.freeze(Object.fromEntries(Object.keys(LEGACY_MAPS).map((id) => [
id,
Object.freeze(Object.assign({}, LEGACY_MAPS[id], VISUAL_LAYOUT[id]))
])));
const MAP_IDS = Object.freeze(Object.keys(MAPS));
const QUEST_IDS = Object.freeze(Object.keys(QUESTS));
const ITEM_IDS = Object.freeze(Object.keys(ITEMS));