459 lines
32 KiB
JavaScript
Executable File
459 lines
32 KiB
JavaScript
Executable File
(function (root, factory) {
|
|
"use strict";
|
|
const api = factory();
|
|
if (typeof module === "object" && module.exports) module.exports = api;
|
|
root.PrincessLimaData = api;
|
|
}(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
|
"use strict";
|
|
|
|
const WIDTH = 720;
|
|
const HEIGHT = 720;
|
|
|
|
function rect(x, y, width, height, kind) {
|
|
return Object.freeze({ shape: "rect", x, y, width, height, kind: kind || "wall" });
|
|
}
|
|
|
|
function circle(x, y, radius, kind) {
|
|
return Object.freeze({ shape: "circle", x, y, radius, kind: kind || "rock" });
|
|
}
|
|
|
|
function exit(id, x, y, width, height, target, spawn, requirement, label) {
|
|
return Object.freeze({ id, x, y, width, height, target, spawn, requirement: requirement || null, label });
|
|
}
|
|
|
|
function enemy(type, x, y, options) {
|
|
return Object.freeze(Object.assign({ type, x, y, leash: 170, quest: null, boss: false }, options || {}));
|
|
}
|
|
|
|
const ITEMS = Object.freeze({
|
|
village_sword: Object.freeze({ name: "Wayfarer Sword", type: "weapon", unique: true, description: "A balanced village-forged blade.", attack: 1 }),
|
|
tempered_sword: Object.freeze({ name: "Tempered Sword", type: "weapon", unique: true, description: "Bram's reforged blade. It breaks shadow armour.", attack: 2 }),
|
|
buckler: Object.freeze({ name: "Oak Buckler", type: "armour", unique: true, description: "Reduces incoming damage.", defence: 2 }),
|
|
trail_boots: Object.freeze({ name: "Trail Boots", type: "equipment", unique: true, description: "Quicker acceleration over rough ground." }),
|
|
forest_charm: Object.freeze({ name: "Forest Charm", type: "key", unique: true, description: "Proof that the Whispering Woods accepted your passage." }),
|
|
mountain_key: Object.freeze({ name: "Mountain Key", type: "key", unique: true, description: "Opens the old lift gate." }),
|
|
fortress_emblem: Object.freeze({ name: "Fortress Emblem", type: "key", unique: true, description: "Taken from the camp captain." }),
|
|
healing_tonic: Object.freeze({ name: "Healing Tonic", type: "consumable", stack: 9, description: "Restores 40 health.", heal: 40 }),
|
|
royal_draught: Object.freeze({ name: "Royal Draught", type: "consumable", stack: 3, description: "Fully restores health.", heal: 999 }),
|
|
silver_leaf: Object.freeze({ name: "Silver Leaf", type: "collectable", stack: 12, description: "A moonlit forest herb." }),
|
|
moon_coin: Object.freeze({ name: "Moon Coin", type: "currency", stack: 99, description: "Accepted by travelling merchants." }),
|
|
prison_key: Object.freeze({ name: "Prison Key", type: "quest", unique: true, protected: true, description: "Unlocks the fortress cells." }),
|
|
bridge_gear: Object.freeze({ name: "Bridge Gear", type: "quest", unique: true, protected: true, description: "Repairs the mountain bridge winch." }),
|
|
sun_crystal: Object.freeze({ name: "Sun Crystal", type: "quest", unique: true, protected: true, description: "Weakens the Shadow Lord's veil." })
|
|
});
|
|
|
|
const QUESTS = Object.freeze({
|
|
aftermath: Object.freeze({ title: "After the Black Riders", chapter: 1, region: "Broken Village", main: true, reward: [["village_sword", 1]], description: "Learn what happened to Princess Lima.", target: 1 }),
|
|
village_defence: Object.freeze({ title: "The Second Raid", chapter: 1, region: "Broken Village", main: true, reward: [["healing_tonic", 2], ["buckler", 1]], description: "Defend the square from three attackers.", target: 3 }),
|
|
healer_herbs: Object.freeze({ title: "Silver for the Wounded", chapter: 1, region: "Broken Village", main: false, reward: [["healing_tonic", 2]], description: "Bring two Silver Leaves to Healer Nia.", target: 2 }),
|
|
find_guide: Object.freeze({ title: "The Missing Guide", chapter: 2, region: "Whispering Woods", main: true, reward: [["forest_charm", 1], ["trail_boots", 1]], description: "Follow the standing stones and rescue Tovin.", target: 3 }),
|
|
ruins_light: Object.freeze({ title: "Light Beneath the Roots", chapter: 2, region: "Sunken Ruins", main: true, reward: [["sun_crystal", 1]], description: "Wake the ruin braziers in the marked order.", target: 4 }),
|
|
wolf_miniboss: Object.freeze({ title: "The Briar Wolf", chapter: 2, region: "Whispering Woods", main: true, reward: [["mountain_key", 1]], description: "Defeat the corrupted Briar Wolf.", target: 1 }),
|
|
repair_bridge: Object.freeze({ title: "A Road Across the Sky", chapter: 3, region: "Mountain Pass", main: true, reward: [["bridge_gear", 1], ["tempered_sword", 1]], description: "Restart both winches and repair the bridge.", target: 2 }),
|
|
stone_guardian: Object.freeze({ title: "Guardian of the Pass", chapter: 3, region: "Mountain Pass", main: true, reward: [["royal_draught", 1]], description: "Defeat the awakened Stone Guardian.", target: 1 }),
|
|
free_scout: Object.freeze({ title: "The Captured Scout", chapter: 3, region: "Blackridge Camp", main: true, reward: [["fortress_emblem", 1]], description: "Free Scout Elowen and defeat the camp captain.", target: 2 }),
|
|
free_prisoners: Object.freeze({ title: "No One Left in Shadow", chapter: 4, region: "Shadow Fortress", main: true, reward: [["prison_key", 1]], description: "Open the two prison cells.", target: 2 }),
|
|
break_wards: Object.freeze({ title: "The Three Shadow Wards", chapter: 4, region: "Shadow Fortress", main: true, reward: [["royal_draught", 1]], description: "Disable the three fortress wards.", target: 3 }),
|
|
defeat_malrec: Object.freeze({ title: "The Last Shadow", chapter: 4, region: "Throne of Night", main: true, reward: [], description: "Defeat Lord Malrec and rescue Princess Lima.", target: 1 })
|
|
});
|
|
|
|
const ENEMIES = Object.freeze({
|
|
slime: Object.freeze({ name: "Marsh Slime", health: 3, damage: 8, speed: 58, behaviour: "chase", frame: 9, xp: 8 }),
|
|
wolf: Object.freeze({ name: "Grey Wolf", health: 4, damage: 10, speed: 88, behaviour: "chase", frame: 10, xp: 12 }),
|
|
bandit: Object.freeze({ name: "Road Bandit", health: 5, damage: 12, speed: 66, behaviour: "chase", frame: 11, xp: 15 }),
|
|
bat: Object.freeze({ name: "Cave Bat", health: 3, damage: 9, speed: 96, behaviour: "wander", frame: 12, xp: 10 }),
|
|
guard: Object.freeze({ name: "Shadow Guard", health: 7, damage: 14, speed: 58, behaviour: "guard", frame: 13, xp: 20 }),
|
|
briar_wolf: Object.freeze({ name: "Briar Wolf", health: 22, damage: 15, speed: 92, behaviour: "charge", frame: 10, xp: 90, boss: true, phases: 2 }),
|
|
stone_guardian: Object.freeze({ name: "Stone Guardian", health: 30, damage: 18, speed: 45, behaviour: "slam", frame: 14, xp: 140, boss: true, phases: 2 }),
|
|
captain: Object.freeze({ name: "Captain Veyr", health: 24, damage: 17, speed: 64, behaviour: "guard", frame: 13, xp: 110, boss: true, phases: 2 }),
|
|
malrec: Object.freeze({ name: "Lord Malrec", health: 48, damage: 18, speed: 66, behaviour: "final", frame: 8, xp: 300, boss: true, phases: 3 })
|
|
});
|
|
|
|
const NPCS = Object.freeze({
|
|
elder: Object.freeze({ name: "Elder Corin", frame: 5, dialogue: ["The black riders took Princess Lima toward the northern fortress.", "We are small, traveller, but we are not helpless. Speak to Bram. Take a blade."] }),
|
|
bram: Object.freeze({ name: "Blacksmith Bram", frame: 6, dialogue: ["This sword was meant for a royal guard. Today, it chooses you.", "Bring the mountain forge back to life and I can temper it."] }),
|
|
nia: Object.freeze({ name: "Healer Nia", frame: 7, dialogue: ["The wounded need Silver Leaf. It grows where moonlight reaches the forest floor.", "Keep a tonic ready. Courage is easier with a second chance."] }),
|
|
tovin: Object.freeze({ name: "Guide Tovin", frame: 7, dialogue: ["I followed the riders until the Briar Wolf cornered me.", "Wake the stones from youngest tree to oldest. The true path will answer."] }),
|
|
elowen: Object.freeze({ name: "Scout Elowen", frame: 11, dialogue: ["Malrec's guards sealed the pass, but their captain carries the fortress emblem.", "Princess Lima is alive. She refused Malrec's bargain."] }),
|
|
prisoner: Object.freeze({ name: "Resistance Prisoner", frame: 5, dialogue: ["The wards feed the throne room. Break all three before facing Malrec."] }),
|
|
lima: Object.freeze({ name: "Princess Lima", frame: 4, dialogue: ["You crossed a kingdom for someone you had never met.", "Let us go home—not as legend and princess, but as two people who chose to help."] }),
|
|
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 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 } }),
|
|
obstacles: Object.freeze([
|
|
rect(0, 0, 1280, 34, "edge"), rect(0, 686, 1280, 34, "edge"), rect(0, 0, 34, 720, "edge"), rect(1246, 0, 34, 720, "edge"),
|
|
rect(80, 70, 250, 170, "house"), rect(440, 58, 250, 175, "house"), rect(865, 70, 260, 175, "house"),
|
|
rect(60, 285, 360, 30, "fence"), rect(850, 285, 350, 30, "fence"),
|
|
circle(210, 430, 54, "well"), circle(1035, 475, 48, "rubble")
|
|
]),
|
|
exits: Object.freeze([exit("forest-road", 1210, 300, 60, 120, "forest", "villagePath", "village_defended", "Road to the Whispering Woods")]),
|
|
npcs: Object.freeze([["elder", 640, 330], ["bram", 520, 285], ["nia", 760, 285]]),
|
|
enemies: Object.freeze([
|
|
enemy("slime", 360, 500, { quest: "village_defence", requires: "aftermath_complete" }),
|
|
enemy("bandit", 620, 545, { quest: "village_defence", requires: "aftermath_complete" }),
|
|
enemy("slime", 900, 520, { quest: "village_defence", requires: "aftermath_complete" })
|
|
]),
|
|
pickups: Object.freeze([["silver_leaf", 365, 360], ["silver_leaf", 915, 370], ["moon_coin", 1090, 570]])
|
|
}),
|
|
forest: Object.freeze({
|
|
name: "Whispering Woods", chapter: 2, palette: ["#122d24", "#28513c", "#6f8a4d", "#a7b46b"],
|
|
spawns: Object.freeze({ villagePath: { x: 90, y: 355 }, ruinsPath: { x: 1110, y: 570 }, mountainPath: { x: 1120, y: 120 } }),
|
|
obstacles: Object.freeze([
|
|
rect(0, 0, 1280, 30, "edge"), rect(0, 690, 1280, 30, "edge"), rect(0, 0, 30, 720, "edge"), rect(1250, 0, 30, 720, "edge"),
|
|
rect(160, 40, 90, 245, "trees"), rect(160, 430, 90, 230, "trees"), rect(390, 170, 95, 420, "trees"),
|
|
rect(625, 35, 95, 290, "trees"), rect(625, 455, 95, 230, "trees"), rect(890, 150, 90, 420, "trees"),
|
|
circle(315, 350, 38, "stone"), circle(550, 385, 42, "stone"), circle(805, 350, 44, "stone")
|
|
]),
|
|
exits: Object.freeze([
|
|
exit("village", 20, 305, 60, 105, "village", "forestRoad", null, "Return to the village"),
|
|
exit("ruins", 1180, 525, 75, 120, "ruins", "forestDoor", "guide_found", "Sunken Ruins"),
|
|
exit("mountain", 1070, 20, 130, 65, "mountain", "forestTrail", "briar_defeated", "Mountain trail")
|
|
]),
|
|
npcs: Object.freeze([["tovin", 780, 570]]),
|
|
enemies: Object.freeze([
|
|
enemy("wolf", 320, 140), enemy("slime", 540, 610), enemy("bandit", 800, 160),
|
|
enemy("wolf", 1070, 430), enemy("briar_wolf", 1030, 105, { quest: "wolf_miniboss", boss: true, requires: "ruins_complete", leash: 260 })
|
|
]),
|
|
puzzle: Object.freeze({ id: "forest_stones", type: "sequence", sequence: ["sapling", "oak", "elder"], objects: [["sapling", 315, 350], ["oak", 550, 385], ["elder", 805, 350]] }),
|
|
pickups: Object.freeze([["silver_leaf", 325, 620], ["silver_leaf", 760, 90], ["healing_tonic", 1080, 610]])
|
|
}),
|
|
ruins: Object.freeze({
|
|
name: "Sunken Ruins", chapter: 2, palette: ["#17272d", "#31505a", "#6d7567", "#d49c55"],
|
|
spawns: Object.freeze({ forestDoor: { x: 110, y: 590 } }),
|
|
obstacles: Object.freeze([
|
|
rect(0, 0, 1280, 32, "edge"), rect(0, 688, 1280, 32, "edge"), rect(0, 0, 32, 720, "edge"), rect(1248, 0, 32, 720, "edge"),
|
|
rect(130, 100, 900, 34, "ruin-wall"), rect(130, 100, 34, 410, "ruin-wall"), rect(130, 476, 330, 34, "ruin-wall"),
|
|
rect(570, 476, 460, 34, "ruin-wall"), rect(996, 100, 34, 410, "ruin-wall"),
|
|
rect(340, 250, 110, 80, "water"), rect(700, 250, 110, 80, "water")
|
|
]),
|
|
exits: Object.freeze([exit("forest", 45, 540, 80, 120, "forest", "ruinsPath", null, "Return to the woods")]),
|
|
npcs: Object.freeze([]),
|
|
enemies: Object.freeze([enemy("bat", 300, 190), enemy("bat", 850, 190), enemy("slime", 580, 390)]),
|
|
puzzle: Object.freeze({ id: "ruin_braziers", type: "sequence", sequence: ["dawn", "noon", "dusk", "night"], objects: [["dawn", 250, 410], ["noon", 480, 190], ["dusk", 680, 410], ["night", 900, 190]] }),
|
|
pickups: Object.freeze([["moon_coin", 550, 210], ["healing_tonic", 900, 430]])
|
|
}),
|
|
mountain: Object.freeze({
|
|
name: "Mountain Pass", chapter: 3, palette: ["#202a35", "#465563", "#85909a", "#d4b06a"],
|
|
spawns: Object.freeze({ forestTrail: { x: 100, y: 590 }, campRoad: { x: 1140, y: 560 } }),
|
|
obstacles: Object.freeze([
|
|
rect(0, 0, 1280, 34, "edge"), rect(0, 686, 1280, 34, "edge"), rect(0, 0, 34, 720, "edge"), rect(1246, 0, 34, 720, "edge"),
|
|
rect(170, 60, 150, 430, "cliff"), rect(880, 70, 150, 440, "cliff"),
|
|
rect(350, 520, 110, 90, "boulder"), rect(780, 500, 105, 100, "boulder")
|
|
]),
|
|
dynamicObstacles: Object.freeze([Object.freeze({ id: "bridge", x: 450, y: 225, width: 330, height: 120, kind: "chasm", opensWith: "bridge_repaired" })]),
|
|
exits: Object.freeze([
|
|
exit("forest", 35, 530, 75, 120, "forest", "mountainPath", null, "Return to the woods"),
|
|
exit("camp", 1170, 510, 75, 125, "camp", "mountainRoad", "guardian_defeated", "Road to Blackridge Camp")
|
|
]),
|
|
npcs: Object.freeze([["bram", 350, 160]]),
|
|
enemies: Object.freeze([enemy("bat", 390, 400), enemy("guard", 830, 390), enemy("stone_guardian", 1080, 335, { quest: "stone_guardian", boss: true, requires: "bridge_repaired", leash: 280 })]),
|
|
puzzle: Object.freeze({ id: "bridge_winches", type: "set", sequence: ["west", "east"], objects: [["west", 380, 180], ["east", 845, 180]] }),
|
|
pickups: Object.freeze([["moon_coin", 400, 640], ["healing_tonic", 850, 640]])
|
|
}),
|
|
camp: Object.freeze({
|
|
name: "Blackridge Camp", chapter: 3, palette: ["#2b241f", "#584232", "#8b6a43", "#b98b50"],
|
|
spawns: Object.freeze({ mountainRoad: { x: 100, y: 600 }, fortressRoad: { x: 1160, y: 330 } }),
|
|
obstacles: Object.freeze([
|
|
rect(0, 0, 1280, 30, "edge"), rect(0, 690, 1280, 30, "edge"), rect(0, 0, 30, 720, "edge"), rect(1250, 0, 30, 720, "edge"),
|
|
rect(200, 90, 220, 150, "tent"), rect(520, 80, 220, 160, "tent"), rect(860, 80, 220, 160, "tent"),
|
|
rect(260, 430, 250, 35, "barricade"), rect(730, 430, 280, 35, "barricade"),
|
|
rect(520, 500, 20, 130, "cage"), rect(700, 500, 20, 130, "cage"), rect(520, 500, 200, 20, "cage"),
|
|
rect(520, 610, 70, 20, "cage"), rect(650, 610, 70, 20, "cage")
|
|
]),
|
|
exits: Object.freeze([
|
|
exit("mountain", 35, 550, 75, 120, "mountain", "campRoad", null, "Return to the pass"),
|
|
exit("fortress", 1170, 280, 75, 120, "fortressExterior", "campGate", "emblem_found", "Fortress road")
|
|
]),
|
|
npcs: Object.freeze([["elowen", 620, 555]]),
|
|
enemies: Object.freeze([enemy("guard", 340, 330, { quest: "free_scout" }), enemy("guard", 820, 340, { quest: "free_scout" }), enemy("captain", 1080, 530, { quest: "free_scout", boss: true, leash: 260 })]),
|
|
pickups: Object.freeze([["healing_tonic", 170, 300], ["moon_coin", 1070, 280]])
|
|
}),
|
|
fortressExterior: Object.freeze({
|
|
name: "Shadow Fortress Gate", chapter: 4, palette: ["#15131d", "#30283d", "#554a62", "#8a718f"],
|
|
spawns: Object.freeze({ campGate: { x: 120, y: 590 }, innerGate: { x: 1100, y: 625 } }),
|
|
obstacles: Object.freeze([
|
|
rect(0, 0, 1280, 30, "edge"), rect(0, 690, 1280, 30, "edge"), rect(0, 0, 30, 720, "edge"), rect(1250, 0, 30, 720, "edge"),
|
|
rect(120, 70, 1040, 85, "fortress-wall"), rect(120, 70, 95, 450, "fortress-wall"), rect(1065, 70, 95, 450, "fortress-wall"),
|
|
rect(120, 500, 400, 75, "fortress-wall"), rect(760, 500, 400, 75, "fortress-wall"),
|
|
circle(420, 320, 65, "tower"), circle(860, 320, 65, "tower")
|
|
]),
|
|
exits: Object.freeze([
|
|
exit("camp", 35, 540, 80, 120, "camp", "fortressRoad", null, "Return to Blackridge"),
|
|
exit("interior", 580, 485, 120, 90, "fortressInterior", "frontHall", "emblem_found", "Enter the fortress")
|
|
]),
|
|
npcs: Object.freeze([]),
|
|
enemies: Object.freeze([enemy("guard", 330, 590), enemy("guard", 640, 350), enemy("guard", 950, 590)]),
|
|
pickups: Object.freeze([["healing_tonic", 640, 200]])
|
|
}),
|
|
fortressInterior: Object.freeze({
|
|
name: "Shadow Fortress", chapter: 4, palette: ["#111119", "#272333", "#51465d", "#b58b66"],
|
|
spawns: Object.freeze({ frontHall: { x: 640, y: 620 }, throneDoor: { x: 640, y: 110 } }),
|
|
obstacles: Object.freeze([
|
|
rect(0, 0, 1280, 32, "edge"), rect(0, 688, 1280, 32, "edge"), rect(0, 0, 32, 720, "edge"), rect(1248, 0, 32, 720, "edge"),
|
|
rect(170, 100, 35, 470, "wall"), rect(1075, 100, 35, 470, "wall"), rect(170, 100, 360, 35, "wall"), rect(750, 100, 360, 35, "wall"),
|
|
rect(390, 260, 35, 300, "wall"), rect(855, 260, 35, 300, "wall"),
|
|
rect(205, 420, 185, 35, "cell"), rect(890, 420, 185, 35, "cell")
|
|
]),
|
|
exits: Object.freeze([
|
|
exit("outside", 580, 650, 120, 60, "fortressExterior", "innerGate", null, "Leave the fortress"),
|
|
exit("throne", 580, 70, 120, 70, "bossArena", "entrance", "wards_broken", "Throne of Night")
|
|
]),
|
|
npcs: Object.freeze([["prisoner", 285, 350], ["elowen", 995, 350]]),
|
|
enemies: Object.freeze([enemy("guard", 520, 470), enemy("guard", 760, 470), enemy("bat", 640, 220)]),
|
|
puzzle: Object.freeze({ id: "shadow_wards", type: "set", sequence: ["moon", "crown", "flame"], objects: [["moon", 270, 180], ["crown", 640, 360], ["flame", 1010, 180]] }),
|
|
pickups: Object.freeze([["prison_key", 640, 520], ["healing_tonic", 1010, 560]])
|
|
}),
|
|
bossArena: Object.freeze({
|
|
name: "Throne of Night", chapter: 4, palette: ["#0d0b14", "#21182d", "#51335f", "#b76a85"],
|
|
spawns: Object.freeze({ entrance: { x: 640, y: 620 } }),
|
|
obstacles: Object.freeze([
|
|
rect(0, 0, 1280, 42, "edge"), rect(0, 678, 1280, 42, "edge"), rect(0, 0, 42, 720, "edge"), rect(1238, 0, 42, 720, "edge"),
|
|
circle(210, 180, 52, "pillar"), circle(1070, 180, 52, "pillar"), circle(210, 540, 52, "pillar"), circle(1070, 540, 52, "pillar")
|
|
]),
|
|
exits: Object.freeze([exit("chamber", 570, 30, 140, 70, "chamber", "door", "malrec_defeated", "Princess Lima's chamber")]),
|
|
npcs: Object.freeze([["malrec", 640, 170]]),
|
|
enemies: Object.freeze([enemy("malrec", 640, 260, { quest: "defeat_malrec", boss: true, requires: "boss_started", leash: 500 })]),
|
|
puzzle: Object.freeze({ id: "sun_pedestals", type: "set", sequence: ["west", "east"], objects: [["west", 320, 360], ["east", 960, 360]] }),
|
|
pickups: Object.freeze([])
|
|
}),
|
|
chamber: Object.freeze({
|
|
name: "The Dawn Chamber", chapter: 4, palette: ["#293346", "#58687e", "#d2b878", "#f1e4c4"],
|
|
spawns: Object.freeze({ door: { x: 640, y: 610 } }),
|
|
obstacles: Object.freeze([
|
|
rect(0, 0, 1280, 34, "edge"), rect(0, 686, 1280, 34, "edge"), rect(0, 0, 34, 720, "edge"), rect(1246, 0, 34, 720, "edge"),
|
|
rect(180, 90, 250, 80, "balcony"), rect(850, 90, 250, 80, "balcony"), circle(640, 270, 70, "dais")
|
|
]),
|
|
exits: Object.freeze([]),
|
|
npcs: Object.freeze([["lima", 640, 180]]),
|
|
enemies: Object.freeze([]),
|
|
pickups: Object.freeze([])
|
|
})
|
|
});
|
|
|
|
function edges() {
|
|
const thickness = 18;
|
|
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")
|
|
];
|
|
}
|
|
|
|
/*
|
|
* 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 },
|
|
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"),
|
|
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("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({
|
|
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]))
|
|
])));
|
|
|
|
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));
|
|
const ENEMY_IDS = Object.freeze(Object.keys(ENEMIES));
|
|
const NPC_IDS = Object.freeze(Object.keys(NPCS));
|
|
|
|
return Object.freeze({
|
|
WIDTH, HEIGHT, ITEMS, QUESTS, ENEMIES, NPCS, MAPS, MAP_LAYERS,
|
|
MAP_IDS, QUEST_IDS, ITEM_IDS, ENEMY_IDS, NPC_IDS
|
|
});
|
|
}));
|