diff --git a/assets/audio/princess-lima/hit.wav b/assets/audio/princess-lima/hit.wav old mode 100644 new mode 100755 diff --git a/assets/audio/princess-lima/intro-narration-1.wav b/assets/audio/princess-lima/intro-narration-1.wav old mode 100644 new mode 100755 diff --git a/assets/audio/princess-lima/intro-narration-2.wav b/assets/audio/princess-lima/intro-narration-2.wav old mode 100644 new mode 100755 diff --git a/assets/audio/princess-lima/intro-narration-3.wav b/assets/audio/princess-lima/intro-narration-3.wav old mode 100644 new mode 100755 diff --git a/assets/audio/princess-lima/intro-narration-4.wav b/assets/audio/princess-lima/intro-narration-4.wav old mode 100644 new mode 100755 diff --git a/assets/images/play/princess-lima/intro-storyboard.png b/assets/images/play/princess-lima/intro-storyboard.png old mode 100644 new mode 100755 diff --git a/assets/images/play/princess-lima/regional-style-atlas.png b/assets/images/play/princess-lima/regional-style-atlas.png old mode 100644 new mode 100755 diff --git a/assets/images/play/princess-lima/world-tiles.png b/assets/images/play/princess-lima/world-tiles.png old mode 100644 new mode 100755 diff --git a/assets/scripts/pages/princess-lima-data.js b/assets/scripts/pages/princess-lima-data.js index 6c19059..5f61e83 100755 --- a/assets/scripts/pages/princess-lima-data.js +++ b/assets/scripts/pages/princess-lima-data.js @@ -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)); diff --git a/assets/scripts/pages/princess-lima-game.js b/assets/scripts/pages/princess-lima-game.js index 0b6f317..36818f1 100755 --- a/assets/scripts/pages/princess-lima-game.js +++ b/assets/scripts/pages/princess-lima-game.js @@ -86,8 +86,7 @@ queueStatus("The old save was invalid, so it was ignored safely."); return null; } - const params = new URLSearchParams(window.location.search); - const debugRegion = localDebug() && Data.MAPS[params.get("region")] ? params.get("region") : null; + 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"); @@ -105,14 +104,14 @@ const classes = Scenes.createSceneClasses(controller); controller.game = new Phaser.Game({ type: Phaser.AUTO, - width: 1000, + width: Data.WIDTH, height: Data.HEIGHT, parent: "princess-lima-game", pixelArt: true, roundPixels: true, backgroundColor: "#0a0b12", physics: { default: "arcade", arcade: { debug: localDebug(), gravity: { x: 0, y: 0 } } }, - scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, width: 1000, height: Data.HEIGHT }, + scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, width: Data.WIDTH, height: Data.HEIGHT }, scene: classes, render: { antialias: false, pixelArt: true } }); @@ -393,6 +392,12 @@ return ["localhost", "127.0.0.1"].includes(window.location.hostname) && params.get("collisionDebug") === "1"; } + function localRegionPreview() { + if (!["localhost", "127.0.0.1"].includes(window.location.hostname)) return null; + const region = new URLSearchParams(window.location.search).get("region"); + return Data.MAPS[region] ? region : null; + } + let queuedStatus = ""; function queueStatus(message) { queuedStatus = message; } function flushQueuedStatus() { if (queuedStatus) status(queuedStatus); } diff --git a/assets/scripts/pages/princess-lima-intro.js b/assets/scripts/pages/princess-lima-intro.js old mode 100644 new mode 100755 diff --git a/assets/scripts/pages/princess-lima-maps.js b/assets/scripts/pages/princess-lima-maps.js index fc997c6..6734439 100644 --- a/assets/scripts/pages/princess-lima-maps.js +++ b/assets/scripts/pages/princess-lima-maps.js @@ -2,155 +2,37 @@ "use strict"; const Data = root.PrincessLimaData; - const TILE = 32; - const REGION_ROW = Object.freeze({ - village: 0, forest: 1, ruins: 2, mountain: 3, camp: 4, - fortressExterior: 5, fortressInterior: 6, bossArena: 7, chamber: 8 + const REGION_FRAME = Object.freeze({ + village: 0, + forest: 1, + ruins: 2, + mountain: 3, + camp: 4, + fortressExterior: 5, + fortressInterior: 6, + bossArena: 7, + chamber: 8 }); - const ART = Object.freeze({ - village: { - paths: [[1, 10, 38, 4], [17, 7, 6, 15], [3, 16, 34, 3]], - decor: [["garden", 1120, 575], ["cart", 955, 350], ["smoke", 180, 62], ["forge", 520, 250], ["flowers", 785, 610]] - }, - forest: { - paths: [[1, 9, 37, 4], [8, 4, 4, 16], [16, 8, 4, 14], [24, 3, 4, 16], [31, 2, 5, 19]], - decor: [["stream", 1040, 300], ["bridge", 1030, 375], ["log", 760, 500], ["flowers", 520, 110], ["mushrooms", 1010, 610]] - }, - ruins: { - paths: [[3, 16, 33, 4], [15, 4, 5, 13], [25, 4, 5, 13]], - decor: [["mosaic", 575, 350], ["statue", 230, 190], ["statue", 930, 190], ["arch", 570, 115], ["rubble", 1080, 520]] - }, - mountain: { - paths: [[1, 16, 38, 4], [11, 5, 5, 13], [23, 5, 5, 13], [31, 10, 6, 9]], - decor: [["ice", 560, 580], ["pine", 110, 160], ["pine", 1140, 150], ["icicles", 630, 70], ["cave", 1080, 335]] - }, - camp: { - paths: [[1, 16, 38, 4], [17, 5, 6, 14], [5, 10, 30, 4]], - decor: [["fire", 640, 345], ["crates", 1120, 570], ["training", 210, 385], ["lookout", 1120, 150], ["banner", 640, 55]] - }, - fortressExterior: { - paths: [[1, 17, 38, 4], [18, 4, 5, 17], [10, 9, 20, 5]], - decor: [["moat", 640, 625], ["stairs", 640, 465], ["banner", 640, 120], ["siege", 220, 620], ["siege", 1060, 620]] - }, - fortressInterior: { - paths: [[18, 2, 5, 20], [5, 14, 30, 5], [7, 5, 27, 5]], - decor: [["carpet", 640, 360], ["torch", 225, 230], ["torch", 1055, 230], ["crates", 1000, 610], ["banner", 640, 80]] - }, - bossArena: { - paths: [[5, 3, 30, 18]], - decor: [["throne", 640, 90], ["rune", 640, 360], ["brazier", 330, 155], ["brazier", 950, 155], ["chains", 640, 45]] - }, - chamber: { - paths: [[4, 3, 32, 18]], - decor: [["carpet", 640, 410], ["bed", 1000, 230], ["flowers", 220, 210], ["fountain", 300, 560], ["table", 985, 515]] - } - }); - - function tileIndex(regionId, kind) { - return REGION_ROW[regionId] * 4 + kind; - } - - function createTilemap(scene, regionId) { - const config = ART[regionId]; - const rows = Math.ceil(Data.HEIGHT / TILE); - const columns = Math.ceil(Data.WIDTH / TILE); - const base = tileIndex(regionId, 0); - const detail = tileIndex(regionId, 1); - const path = tileIndex(regionId, 2); - const data = Array.from({ length: rows }, (_, y) => - Array.from({ length: columns }, (_, x) => ((x * 7 + y * 11) % 13 === 0 ? detail : base))); - config.paths.forEach(([left, top, width, height]) => { - for (let y = top; y < Math.min(rows, top + height); y += 1) { - for (let x = left; x < Math.min(columns, left + width); x += 1) data[y][x] = path; - } - }); - const map = scene.make.tilemap({ data, tileWidth: TILE, tileHeight: TILE }); - const tiles = map.addTilesetImage("lima-world-tiles", "lima-world-tiles", TILE, TILE, 0, 0); - const layer = map.createLayer(0, tiles, 0, 0).setDepth(0); - return { map, layer }; - } - - function drawObstacle(scene, regionId, shape) { - const frame = tileIndex(regionId, shape.kind === "water" || shape.kind === "chasm" ? 3 : 1); - if (shape.shape === "circle") { - const color = Phaser.Display.Color.HexStringToColor(Data.MAPS[regionId].palette[2]).color; - const edge = Phaser.Display.Color.HexStringToColor(Data.MAPS[regionId].palette[3]).color; - const object = scene.add.circle(shape.x, shape.y, shape.radius, color, 0.98).setStrokeStyle(5, edge).setDepth(8); - scene.add.circle(shape.x - shape.radius * 0.22, shape.y - shape.radius * 0.28, Math.max(4, shape.radius * 0.18), 0xffffff, 0.18).setDepth(9); - return object; - } - const object = scene.add.tileSprite( - shape.x + shape.width / 2, shape.y + shape.height / 2, - shape.width, shape.height, "lima-world-tiles", frame - ).setDepth(8); - const graphics = scene.add.graphics().setDepth(9); - const outline = Phaser.Display.Color.HexStringToColor(Data.MAPS[regionId].palette[3]).color; - graphics.lineStyle(3, outline, 0.95).strokeRect(shape.x, shape.y, shape.width, shape.height); - addSilhouette(scene, shape); - return object; - } - - function addSilhouette(scene, shape) { - const x = shape.x; - const y = shape.y; - const width = shape.width; - const height = shape.height; - const ink = 0x0b0d13; - if (shape.kind === "house" || shape.kind === "tent") { - scene.add.triangle(x + width / 2, y + 6, 4, Math.min(50, height * 0.35), width / 2, 0, width - 4, Math.min(50, height * 0.35), 0x3e2430, 0.92).setDepth(10); - scene.add.rectangle(x + width / 2, y + height - 14, Math.min(34, width * 0.2), 28, ink, 0.85).setDepth(10); - } else if (shape.kind === "trees") { - for (let py = y + 18; py < y + height; py += 44) { - scene.add.circle(x + width / 2 + ((py / 44) % 2 ? -12 : 12), py, 22, 0x173f2b, 0.96).setStrokeStyle(2, 0x82a957).setDepth(10); - } - } else if (/wall|cell|fence|barricade|cage/.test(shape.kind || "")) { - for (let px = x + 8; px < x + width; px += 18) scene.add.rectangle(px, y + height / 2, 5, height - 5, ink, 0.32).setDepth(10); - } else if (shape.kind === "cliff") { - for (let py = y + 14; py < y + height; py += 34) scene.add.triangle(x + width - 12, py, 0, 0, 22, 8, 4, 22, 0xdce6ee, 0.5).setDepth(10); - } - } - - function drawDecor(scene, regionId) { - ART[regionId].decor.forEach(([type, x, y], index) => { - const depth = y + 5; - if (["flowers", "mushrooms"].includes(type)) { - const colors = type === "flowers" ? [0xffd26b, 0xd6759e, 0xf6f1cf] : [0xd95c69, 0xf0c66c]; - colors.forEach((color, offset) => scene.add.circle(x + offset * 10, y + (offset % 2) * 5, 4, color).setDepth(depth)); - } else if (["fire", "torch", "brazier"].includes(type)) { - scene.add.circle(x, y, type === "fire" ? 18 : 10, 0xff9d3d, 0.92).setStrokeStyle(3, 0xffe18b).setDepth(depth); - scene.add.rectangle(x, y + 15, type === "fire" ? 34 : 8, 7, 0x5b3324).setDepth(depth - 1); - } else if (["stream", "ice", "moat"].includes(type)) { - scene.add.ellipse(x, y, type === "moat" ? 420 : 150, type === "moat" ? 38 : 64, type === "ice" ? 0x9dd9e8 : 0x28758b, 0.72).setStrokeStyle(3, 0xb7eff1).setDepth(3); - } else if (["bridge", "stairs", "carpet"].includes(type)) { - scene.add.rectangle(x, y, type === "carpet" ? 100 : 130, type === "stairs" ? 48 : 40, type === "carpet" ? 0x7f2940 : 0x9a7548, 0.95).setStrokeStyle(3, 0xe0bd75).setDepth(depth); - } else if (["pine", "smoke", "statue", "arch", "lookout", "throne", "fountain"].includes(type)) { - const color = { pine: 0x275437, smoke: 0x70737c, statue: 0x8f9990, arch: 0x767d72, lookout: 0x68452d, throne: 0x613152, fountain: 0x4c99b5 }[type]; - scene.add.triangle(x, y, 0, 42, 26, 0, 52, 42, color, 0.94).setStrokeStyle(2, 0xe4c779).setDepth(depth); - } else { - const color = ["rune", "chains"].includes(type) ? 0x8c4aa0 : 0x73513a; - scene.add.rectangle(x, y, 48 + index % 2 * 16, 30, color, 0.88).setStrokeStyle(2, 0xd1ac66).setDepth(depth); - } - }); - } - - function render(scene, regionId, obstacles, dynamicObstacles, unlockedRoutes) { - createTilemap(scene, regionId); - scene.add.image(Data.WIDTH / 2, Data.HEIGHT / 2, "lima-region-atlas", REGION_ROW[regionId]) + /* + * The generated atlas is the finished map artwork, not a texture reference. + * Keep it square and opaque. Collision is supplied separately by the region + * data so production rendering never paints debug geometry over the image. + */ + function render(scene, regionId) { + return scene.add.image( + Data.WIDTH / 2, + Data.HEIGHT / 2, + "lima-region-atlas", + REGION_FRAME[regionId] + ) .setDisplaySize(Data.WIDTH, Data.HEIGHT) - .setAlpha(regionId === "chamber" ? 0.52 : 0.42) - .setDepth(1); - obstacles.forEach((shape) => drawObstacle(scene, regionId, shape)); - (dynamicObstacles || []).forEach((shape) => { - if (!unlockedRoutes.includes(shape.opensWith)) drawObstacle(scene, regionId, shape); - else if (shape.id === "bridge") { - scene.add.tileSprite(shape.x + shape.width / 2, shape.y + shape.height / 2, shape.width, 70, "lima-world-tiles", tileIndex(regionId, 2)).setDepth(7); - } - }); - drawDecor(scene, regionId); + .setOrigin(0.5) + .setAlpha(1) + .setDepth(0); } - const api = Object.freeze({ ART, REGION_ROW, createTilemap, drawObstacle, render, tileIndex }); + const api = Object.freeze({ REGION_FRAME, render }); if (typeof module === "object" && module.exports) module.exports = api; root.PrincessLimaMaps = api; }(typeof globalThis !== "undefined" ? globalThis : this)); diff --git a/assets/scripts/pages/princess-lima-scenes.js b/assets/scripts/pages/princess-lima-scenes.js index 7a0ac00..247b89b 100755 --- a/assets/scripts/pages/princess-lima-scenes.js +++ b/assets/scripts/pages/princess-lima-scenes.js @@ -13,7 +13,6 @@ preload() { this.load.image("lima-title", "/assets/images/play/princess-lima/title-landscape.png"); this.load.spritesheet("lima-cast", "/assets/images/play/princess-lima/cast-atlas.png", { frameWidth: 314, frameHeight: 314 }); - this.load.spritesheet("lima-world-tiles", "/assets/images/play/princess-lima/world-tiles.png", { frameWidth: 32, frameHeight: 32 }); this.load.spritesheet("lima-region-atlas", "/assets/images/play/princess-lima/regional-style-atlas.png", { frameWidth: 418, frameHeight: 418 }); const audio = "/assets/audio/princess-lima/"; [ @@ -25,7 +24,7 @@ } create() { - this.add.image(500, Data.HEIGHT / 2, "lima-title").setDisplaySize(1000, Data.HEIGHT); + this.add.image(Data.WIDTH / 2, Data.HEIGHT / 2, "lima-title").setDisplaySize(Data.WIDTH, Data.HEIGHT); controller.audio.attach(this, "village"); controller.ready(); } @@ -93,7 +92,7 @@ drawMap() { this.cameras.main.setBackgroundColor(this.map.palette[0]); - Maps.render(this, this.regionId, this.map.obstacles, this.map.dynamicObstacles, controller.getState().unlockedRoutes); + Maps.render(this, this.regionId); } addSolid(shape) { @@ -173,8 +172,7 @@ createExits() { (this.map.exits || []).forEach((item) => { - const zone = this.add.rectangle(item.x + item.width / 2, item.y + item.height / 2, item.width, item.height, 0xf6d17a, 0.13) - .setStrokeStyle(2, 0xf6d17a, 0.65).setDepth(4); + const zone = this.add.zone(item.x + item.width / 2, item.y + item.height / 2, item.width, item.height); this.interactables.push({ type: "exit", id: item.id, x: item.x + item.width / 2, y: item.y + item.height / 2, data: item, sprite: zone }); }); } @@ -182,9 +180,8 @@ createPuzzle() { const puzzle = this.map.puzzle; if (!puzzle || controller.getState().solvedPuzzles.includes(puzzle.id)) return; - puzzle.objects.forEach(([id, x, y], index) => { - const node = this.add.circle(x, y, 22, 0xd7a949, 0.55).setStrokeStyle(3, 0xffe8a1).setDepth(5); - this.add.text(x, y, String(index + 1), { fontFamily: "monospace", fontSize: "16px", color: "#171117" }).setOrigin(0.5).setDepth(6); + 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 }); }); } @@ -193,7 +190,7 @@ (this.map.pickups || []).forEach(([id, x, y], index) => { const chestId = `${this.regionId}-${id}-${index}`; if (controller.getState().openedChests.includes(chestId)) return; - const item = this.add.star(x, y, 5, 7, 15, 0xf4cf67, 0.9).setStrokeStyle(2, 0xfff0ae).setDepth(6); + const item = this.add.zone(x, y, 34, 34); this.interactables.push({ type: "pickup", id, chestId, x, y, sprite: item }); }); } @@ -345,7 +342,6 @@ if (state.solvedPuzzles.includes(puzzle.id)) return; if (puzzle.type === "set") { if (!this.puzzleInput.includes(item.id)) this.puzzleInput.push(item.id); - item.sprite.setFillStyle(0xf7e7a1, 0.95); if (this.puzzleInput.length === puzzle.sequence.length) this.finishPuzzle(puzzle.id); else controller.status(`${this.puzzleInput.length} / ${puzzle.sequence.length} mechanisms active.`); return; @@ -357,7 +353,6 @@ controller.status("The sequence resets. Look for the environmental clue and try again."); return; } - item.sprite.setFillStyle(0xf7e7a1, 0.95); if (this.puzzleInput.length === puzzle.sequence.length) this.finishPuzzle(puzzle.id); else controller.status(`${item.id} answers. ${this.puzzleInput.length} / ${puzzle.sequence.length}.`); } diff --git a/assets/scripts/pages/princess-lima-state.js b/assets/scripts/pages/princess-lima-state.js index 50fd43c..650b3c7 100755 --- a/assets/scripts/pages/princess-lima-state.js +++ b/assets/scripts/pages/princess-lima-state.js @@ -11,7 +11,13 @@ const STORAGE_KEY = "zxh_princess_lima_rpg_v1"; const APPEARANCES = Object.freeze(["azure", "ember", "pine"]); const FACES = Object.freeze(["north", "south", "east", "west"]); - const START = Object.freeze({ region: "village", spawn: "start", x: 160, y: 570, facing: "east" }); + const START = Object.freeze({ + region: "village", + spawn: "start", + x: Data.MAPS.village.spawns.start.x, + y: Data.MAPS.village.spawns.start.y, + facing: "north" + }); function validName(value) { const name = String(value || "").trim().replace(/\s+/g, " "); diff --git a/build-logs/gitea-build-monitor.log b/build-logs/gitea-build-monitor.log index b3d163d..75f759c 100755 --- a/build-logs/gitea-build-monitor.log +++ b/build-logs/gitea-build-monitor.log @@ -797,3 +797,9 @@ at , /home/zaine/master-folder/org-platform/org_web/build-logs/gite 2026-07-30T11:30:05.9654967+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python. 2026-07-30T11:30:06.0418011+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0 2026-07-30T11:30:06.4084140+01:00 [INFO] Sent authoring server test notification. +2026-07-30T12:23:35.6692441+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success +2026-07-30T12:23:35.6768439+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web. +2026-07-30T12:23:36.7638101+01:00 [INFO] Sent build status notification with 1 embed(s). +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. diff --git a/lisp/build-html.el b/lisp/build-html.el index c2dd838..ad99160 100755 --- a/lisp/build-html.el +++ b/lisp/build-html.el @@ -134,7 +134,7 @@ Created with %c on Arch \ "\n" "Rescue Princess Lima\n" "\n" - "\n" + "\n" "\n\n" body "\n\n\n")) diff --git a/play/rpg.org b/play/rpg.org index 2a5e7e3..b832b46 100755 --- a/play/rpg.org +++ b/play/rpg.org @@ -107,16 +107,16 @@ - - - - - - - - - - - + + + + + + + + + + + #+END_EXPORT diff --git a/posts/posts-list.org b/posts/posts-list.org index 5586f71..c356e39 100755 --- a/posts/posts-list.org +++ b/posts/posts-list.org @@ -4,7 +4,7 @@ See the categories: @@html:Categories@@ * Posts: -- [[file:career/career-list.org][Career List]] @@html:30-07-2026 10:22@@ +- [[file:career/career-list.org][Career List]] @@html:30-07-2026 12:23@@ - [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:01-06-2026 10:21@@ @@html:@@ @@html:@@ - [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:11-03-2026 17:18@@ @@html:@@ @@html:@@ - [[file:career/javascript.org][Understands the Javascript language]] @@html:11-03-2026 16:52@@ @@html:@@ @@html:@@ diff --git a/sitemap.org b/sitemap.org index 7a65447..4158ffb 100755 --- a/sitemap.org +++ b/sitemap.org @@ -104,19 +104,19 @@ flowchart TD n43 --> n49 n50["Tag: website"] n43 --> n50 - n51["Tag: life"] + n51["Tag: update"] n43 --> n51 - n52["Tag: update"] + n52["Tag: life"] n43 --> n52 - n53["Tag: insights"] + n53["Tag: education"] n43 --> n53 - n54["Tag: emacs"] + n54["Tag: insights"] n43 --> n54 - n55["Tag: education"] + n55["Tag: emacs"] n43 --> n55 - n56["Tag: reading"] + n56["Tag: maths"] n43 --> n56 - n57["Tag: maths"] + n57["Tag: reading"] n43 --> n57 n58{{"posts"}} root --> n58 @@ -224,13 +224,13 @@ flowchart TD click n48 "tags/notes.html" "Tag: notes" click n49 "tags/review.html" "Tag: review" click n50 "tags/website.html" "Tag: website" - click n51 "tags/life.html" "Tag: life" - click n52 "tags/update.html" "Tag: update" - click n53 "tags/insights.html" "Tag: insights" - click n54 "tags/emacs.html" "Tag: emacs" - click n55 "tags/education.html" "Tag: education" - click n56 "tags/reading.html" "Tag: reading" - click n57 "tags/maths.html" "Tag: maths" + click n51 "tags/update.html" "Tag: update" + 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 n59 "posts/posts-intro.html" "Posts Introduction" click n60 "posts/posts-list.html" "Posts List" click n62 "posts/career/solid-principles.html" "SOLID Principles" @@ -318,13 +318,13 @@ flowchart TD - [[file:tags/notes.org][Tag: notes]] - [[file:tags/review.org][Tag: review]] - [[file:tags/website.org][Tag: website]] - - [[file:tags/life.org][Tag: life]] - [[file:tags/update.org][Tag: update]] + - [[file:tags/life.org][Tag: life]] + - [[file:tags/education.org][Tag: education]] - [[file:tags/insights.org][Tag: insights]] - [[file:tags/emacs.org][Tag: emacs]] - - [[file:tags/education.org][Tag: education]] - - [[file:tags/reading.org][Tag: reading]] - [[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]] diff --git a/tests/princess-lima-polish.test.cjs b/tests/princess-lima-polish.test.cjs old mode 100644 new mode 100755 index 52a09ae..521aa8e --- a/tests/princess-lima-polish.test.cjs +++ b/tests/princess-lima-polish.test.cjs @@ -3,15 +3,18 @@ const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const data = require("../assets/scripts/pages/princess-lima-data.js"); +const state = require("../assets/scripts/pages/princess-lima-state.js"); +const systems = require("../assets/scripts/pages/princess-lima-systems.js"); const intro = require("../assets/scripts/pages/princess-lima-intro.js"); const root = path.resolve(__dirname, ".."); const rpgSource = fs.readFileSync(path.join(root, "play/rpg.org"), "utf8"); +const sceneSource = fs.readFileSync(path.join(root, "assets/scripts/pages/princess-lima-scenes.js"), "utf8"); -test("all nine regions have authored art data and valid collision-safe transitions", () => { +test("all nine regions use the exact atlas artwork and valid collision-safe transitions", () => { const mapsSource = fs.readFileSync(path.join(root, "assets/scripts/pages/princess-lima-maps.js"), "utf8"); data.MAP_IDS.forEach((region) => { - assert.match(mapsSource, new RegExp(`${region}:\\s*\\{`)); + assert.match(mapsSource, new RegExp(`${region}:\\s*\\d`)); Object.values(data.MAPS[region].spawns).forEach((spawn) => { assert.ok(spawn.x > 0 && spawn.x < data.WIDTH); assert.ok(spawn.y > 0 && spawn.y < data.HEIGHT); @@ -21,6 +24,28 @@ test("all nine regions have authored art data and valid collision-safe transitio assert.ok(data.MAPS[exit.target].spawns[exit.spawn]); }); }); + assert.equal(data.WIDTH, data.HEIGHT); + assert.match(mapsSource, /\.setAlpha\(1\)/); + assert.doesNotMatch(mapsSource, /tilemap|tileSprite|add\.circle|add\.ellipse|drawDecor|drawObstacle/); + assert.doesNotMatch(sceneSource, /lima-world-tiles|world-tiles\.png/); +}); + +test("all authored actors and interaction points sit outside visible terrain collision", () => { + const auditState = state.fresh("Collision audit", "azure"); + auditState.unlockedRoutes = ["bridge_repaired"]; + data.MAP_IDS.forEach((region) => { + const map = data.MAPS[region]; + const points = [ + ...Object.entries(map.spawns).map(([name, point]) => [`spawn:${name}`, point.x, point.y]), + ...(map.npcs || []).map(([name, x, y]) => [`npc:${name}`, x, y]), + ...(map.enemies || []).map((enemy, index) => [`enemy:${enemy.type}:${index}`, enemy.x, enemy.y]), + ...(map.pickups || []).map(([name, x, y], index) => [`pickup:${name}:${index}`, x, y]), + ...(map.puzzle ? map.puzzle.objects.map(([name, x, y]) => [`puzzle:${name}`, x, y]) : []) + ]; + points.forEach(([name, x, y]) => { + assert.equal(systems.isSafePosition(auditState, region, x, y), true, `${region} ${name} must be collision-safe`); + }); + }); }); test("keyboard-only page contains one left HUD and no touch controls", () => { diff --git a/tools/generate-princess-lima-assets.py b/tools/generate-princess-lima-assets.py old mode 100644 new mode 100755