Rebuild Princess Lima maps around the new visual atlas
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:
@@ -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));
|
||||
|
||||
@@ -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); }
|
||||
|
||||
0
assets/scripts/pages/princess-lima-intro.js
Normal file → Executable file
0
assets/scripts/pages/princess-lima-intro.js
Normal file → Executable file
@@ -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));
|
||||
|
||||
@@ -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}.`);
|
||||
}
|
||||
|
||||
@@ -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, " ");
|
||||
|
||||
Reference in New Issue
Block a user