(function (root, factory) { "use strict"; const api = factory(); if (typeof module === "object" && module.exports) module.exports = api; root.PrincessLimaV2Data = api; }(typeof globalThis !== "undefined" ? globalThis : this, function () { "use strict"; const SCHEMA_VERSION = 2; const WIDTH = 960; const HEIGHT = 540; const TILE = 32; const MAP_IDS = Object.freeze([ "village", "forest", "ruins", "mountain", "camp", "fortressExterior", "fortressInterior", "bossArena", "chamber" ]); const MAP_NAMES = Object.freeze({ village: "Broken Village", forest: "Whispering Woods", ruins: "Ancient Ruins", mountain: "Frostpeak Mountain", camp: "Resistance Camp", fortressExterior: "Fortress Exterior", fortressInterior: "Fortress Interior", bossArena: "Throne of Night", chamber: "Lima's Chamber" }); const MAP_URLS = Object.freeze(Object.fromEntries(MAP_IDS.map((id) => [ id, `/assets/maps/princess-lima/${id}.json` ]))); const ASSETS = Object.freeze({ schemaVersion: SCHEMA_VERSION, shared: Object.freeze([ { type: "image", key: "world-tiles", url: "/assets/images/play/princess-lima/world-tiles-v2.png", required: true }, { type: "spritesheet", key: "actors-v2", url: "/assets/images/play/princess-lima/actor-atlas-v2.png", frameWidth: 48, frameHeight: 64, required: true }, { type: "spritesheet", key: "effects-v2", url: "/assets/images/play/princess-lima/effects-atlas-v2.png", frameWidth: 32, frameHeight: 32, required: false }, { type: "image", key: "lima-title", url: "/assets/images/play/princess-lima/title-landscape.png", required: true } ]), portraits: "/assets/images/play/princess-lima/portrait-atlas-v2.png", maps: MAP_URLS }); const ACTORS = Object.freeze({ player: { row: 0, name: "Traveller" }, lima: { row: 1, name: "Princess Lima" }, elder: { row: 2, name: "Elder Corin" }, bram: { row: 3, name: "Blacksmith Bram" }, nia: { row: 4, name: "Healer Nia" }, tovin: { row: 5, name: "Guide Tovin" }, malrec: { row: 6, name: "Lord Malrec" }, slime: { row: 7, name: "Marsh Slime" }, wolf: { row: 8, name: "Grey Wolf" }, raider: { row: 9, name: "Blackroad Raider" }, flying: { row: 10, name: "Cave Bat" }, shield: { row: 11, name: "Shadow Guard" }, stone_guardian: { row: 12, name: "Stone Guardian" }, archer: { row: 13, name: "Shadow Archer" }, elowen: { row: 14, name: "Commander Elowen" }, prisoner: { row: 15, name: "Resistance Prisoner" }, ambusher: { row: 8, name: "Briar Stalker" }, caster: { row: 6, name: "Night Caster" }, support: { row: 4, name: "Shadow Mender" }, elite: { row: 11, name: "Blackguard Elite" }, captain: { row: 11, name: "Captain Veyr" }, briar_wolf: { row: 8, name: "Briar Wolf" } }); const ENEMIES = Object.freeze({ raider: { role: "melee", health: 5, damage: 9, speed: 86, awareness: 210, attackRange: 42, cooldown: 900, telegraph: 230 }, shield: { role: "shield", health: 9, damage: 11, speed: 58, awareness: 190, attackRange: 42, cooldown: 1250, telegraph: 360 }, archer: { role: "ranged", health: 5, damage: 10, speed: 64, awareness: 310, attackRange: 230, cooldown: 1550, telegraph: 450 }, wolf: { role: "fast", health: 4, damage: 8, speed: 122, awareness: 250, attackRange: 38, cooldown: 760, telegraph: 180 }, ambusher: { role: "ambush", health: 5, damage: 12, speed: 104, awareness: 135, attackRange: 38, cooldown: 1100, telegraph: 280 }, caster: { role: "caster", health: 6, damage: 11, speed: 50, awareness: 280, attackRange: 185, cooldown: 1800, telegraph: 600 }, flying: { role: "flying", health: 3, damage: 7, speed: 112, awareness: 240, attackRange: 34, cooldown: 800, telegraph: 160 }, support: { role: "support", health: 5, damage: 5, speed: 58, awareness: 240, attackRange: 150, cooldown: 1900, telegraph: 420 }, elite: { role: "elite", health: 15, damage: 15, speed: 72, awareness: 260, attackRange: 48, cooldown: 1100, telegraph: 300 }, briar_wolf: { role: "boss", boss: true, phases: 2, health: 30, damage: 14, speed: 118, awareness: 430, attackRange: 56, cooldown: 950, telegraph: 380 }, stone_guardian: { role: "boss", boss: true, phases: 3, health: 42, damage: 17, speed: 54, awareness: 430, attackRange: 74, cooldown: 1350, telegraph: 620 }, captain: { role: "boss", boss: true, phases: 2, health: 36, damage: 15, speed: 78, awareness: 430, attackRange: 54, cooldown: 1050, telegraph: 360 }, malrec: { role: "boss", boss: true, phases: 3, health: 52, damage: 13, speed: 60, awareness: 520, attackRange: 190, cooldown: 1600, telegraph: 760 } }); const ITEMS = Object.freeze({ village_sword: { name: "Wayfarer Sword", type: "weapon", attack: 2, unique: true }, tempered_sword: { name: "Sun-tempered Sword", type: "weapon", attack: 4, unique: true }, buckler: { name: "Oak Buckler", type: "armour", defence: 2, unique: true }, reinforced_buckler: { name: "Resistance Buckler", type: "armour", defence: 4, unique: true }, trail_boots: { name: "Trail Boots", type: "equipment", unique: true }, forest_charm: { name: "Forest Charm", type: "charm", unique: true }, sun_crystal: { name: "Sun Crystal", type: "quest", unique: true }, fortress_emblem: { name: "Fortress Emblem", type: "quest", unique: true }, prison_key: { name: "Prison Key", type: "quest", unique: true }, smoke_bomb: { name: "Smoke Bomb", type: "consumable", stack: 5 }, healing_tonic: { name: "Healing Tonic", type: "consumable", heal: 40, stack: 9 }, royal_draught: { name: "Royal Draught", type: "consumable", heal: 999, stack: 3 }, silver_leaf: { name: "Silver Leaf", type: "collectable", stack: 12 }, moon_coin: { name: "Moon Coin", type: "currency", stack: 99 } }); const QUESTS = Object.freeze({ aftermath: { title: "After the Black Riders", chapter: 1, region: "village", main: true, target: 1, reward: [["village_sword", 1]] }, village_defence: { title: "The Second Raid", chapter: 1, region: "village", main: true, target: 3, reward: [["buckler", 1], ["healing_tonic", 2]] }, healer_herbs: { title: "Silver for the Wounded", chapter: 1, region: "village", main: false, target: 2, reward: [["healing_tonic", 2]] }, find_guide: { title: "The Missing Guide", chapter: 2, region: "forest", main: true, target: 2, reward: [["trail_boots", 1], ["forest_charm", 1]] }, ruins_light: { title: "Light Beneath the Roots", chapter: 2, region: "ruins", main: true, target: 4, reward: [["sun_crystal", 1]] }, wolf_miniboss: { title: "The Briar Wolf", chapter: 2, region: "forest", main: true, target: 1, reward: [] }, repair_bridge: { title: "A Road Across the Sky", chapter: 3, region: "mountain", main: true, target: 2, reward: [["tempered_sword", 1]] }, stone_guardian: { title: "Guardian of Frostpeak", chapter: 3, region: "mountain", main: true, target: 1, reward: [["royal_draught", 1]] }, rally_resistance: { title: "A Camp Rekindled", chapter: 3, region: "camp", main: true, target: 2, reward: [["fortress_emblem", 1], ["reinforced_buckler", 1]] }, disable_defences: { title: "Blind the Fortress", chapter: 4, region: "fortressExterior", main: false, target: 2, reward: [["smoke_bomb", 2]] }, free_prisoners: { title: "No One Left in Shadow", chapter: 4, region: "fortressInterior", main: true, target: 2, reward: [["prison_key", 1]] }, break_wards: { title: "The Three Shadow Wards", chapter: 4, region: "fortressInterior", main: true, target: 3, reward: [] }, defeat_malrec: { title: "The Last Shadow", chapter: 4, region: "bossArena", main: true, target: 1, reward: [] } }); const PORTRAITS = Object.freeze({ lima: { determined: 0, relieved: 1 }, malrec: { enraged: 2, cold: 3 }, elder: { worried: 4, thoughtful: 5 }, bram: { neutral: 6, determined: 7 }, nia: { gentle: 8, worried: 9 }, tovin: { alert: 10, surprised: 11 }, elowen: { neutral: 12, determined: 13 }, player: { neutral: 14, injured: 15 }, prisoner: { worried: 4, relieved: 1 } }); const DIALOGUES = Object.freeze({ elder: { id: "elder", start: "arrival", essential: true, nodes: { arrival: { speaker: "elder", expression: "worried", text: "The black riders took Princess Lima north. She stood between Malrec and every soul in this square.", camera: "two-shot", next: "choice" }, choice: { speaker: "elder", expression: "thoughtful", text: "We need more than a sword. Will you help us stand before you follow?", choices: [ { text: "No one is left behind.", next: "accept", effects: [{ type: "startQuest", id: "aftermath" }, { type: "flag", id: "promised_village", value: true }] }, { text: "Tell me where Malrec went.", next: "direct", effects: [{ type: "startQuest", id: "aftermath" }] } ] }, accept: { speaker: "player", expression: "neutral", text: "I will help the village—and then I will bring Lima home.", next: "end" }, direct: { speaker: "elder", expression: "worried", text: "Through the Whispering Woods, over Frostpeak, into the Fortress of Shadows. But Bram's blade must go with you.", next: "end" }, end: { speaker: "elder", expression: "thoughtful", text: "Then hope has arrived before dawn after all.", effects: [{ type: "completeQuest", id: "aftermath" }] } } }, bram: { id: "bram", start: "start", essential: false, nodes: { start: { speaker: "bram", expression: "neutral", text: "This blade was forged for a royal guard. Today it chooses the road, not the rank.", next: "end" }, end: { speaker: "bram", expression: "determined", text: "Bring it to Frostpeak's old forge and I will teach sunlight to live in steel." } } }, nia: { id: "nia", start: "start", essential: false, nodes: { start: { speaker: "nia", expression: "gentle", text: "Silver Leaf grows where moonlight reaches the forest floor. Two sprigs would save a fevered child.", choices: [ { text: "I will look for it.", next: "thanks", effects: [{ type: "startQuest", id: "healer_herbs" }] }, { text: "I cannot promise, but I will remember.", next: "thanks" } ] }, thanks: { speaker: "nia", expression: "worried", text: "Courage is easier when it remembers tenderness." } } }, tovin: { id: "tovin", start: "start", essential: true, nodes: { start: { speaker: "tovin", expression: "surprised", text: "You found me. The woods hid the true path after Malrec poisoned the old stones.", next: "clue" }, clue: { speaker: "tovin", expression: "alert", text: "Wake dawn, noon, dusk, then night beneath the ruins. The forest will open when its memory is whole.", effects: [{ type: "startQuest", id: "find_guide" }] } } }, elowen: { id: "elowen", start: "start", essential: true, nodes: { start: { speaker: "elowen", expression: "determined", text: "We can hit the gate, slip through the drain, or blind the detection wards first.", choices: [ { text: "Direct assault.", next: "route", effects: [{ type: "flag", id: "route_assault", value: true }] }, { text: "Use the hidden drain.", next: "route", effects: [{ type: "flag", id: "route_infiltration", value: true }] }, { text: "Disable every defence.", next: "route", effects: [{ type: "startQuest", id: "disable_defences" }, { type: "flag", id: "route_sabotage", value: true }] } ] }, route: { speaker: "elowen", expression: "neutral", text: "Then that is our road. Lima held the kingdom together alone long enough." } } }, prisoner: { id: "prisoner", start: "start", essential: false, nodes: { start: { speaker: "prisoner", expression: "worried", text: "The three wards feed the throne. Break moon, crown, and flame before Malrec can draw on them.", next: "end" }, end: { speaker: "prisoner", expression: "relieved", text: "Open the cells and we will make sure no guard reaches your back.", effects: [{ type: "startQuest", id: "free_prisoners" }, { type: "startQuest", id: "break_wards" }] } } }, malrec: { id: "malrec", start: "start", essential: true, nodes: { start: { speaker: "malrec", expression: "cold", text: "Lima's oath could end a century of quarrels. You call it freedom when every lord may choose another war.", next: "reply" }, reply: { speaker: "player", expression: "neutral", text: "Peace forced by shadow is only silence.", next: "end" }, end: { speaker: "malrec", expression: "enraged", text: "Then listen closely as hope learns to break.", effects: [{ type: "startQuest", id: "defeat_malrec" }, { type: "flag", id: "boss_started", value: true }] } } }, lima: { id: "lima", start: "start", essential: true, nodes: { start: { speaker: "lima", expression: "determined", text: "You crossed a wounded kingdom for someone you had never met.", choices: [ { text: "Every person I helped led me here.", next: "hope", effects: [{ type: "flag", id: "ending_community", value: true }] }, { text: "No throne belongs to shadow.", next: "hope", effects: [{ type: "flag", id: "ending_defiance", value: true }] } ] }, hope: { speaker: "lima", expression: "relieved", text: "Then let us go home—not as legend and princess, but as two people who chose to help.", effects: [{ type: "flag", id: "rescued", value: true }] } } } }); const CUTSCENES = Object.freeze({ intro: [ { command: "fade", direction: "in", duration: 800 }, { command: "caption", text: "Before shadow crossed the northern road, Princess Lima listened before she ruled." }, { command: "music", key: "village-theme" }, { command: "caption", text: "Lord Malrec came for the royal oath. Lima refused him, and the kingdom paid for her courage." }, { command: "shake", duration: 180, intensity: 0.003 }, { command: "caption", text: "At dawn, one traveller reached the broken village." }, { command: "title", text: "RESCUE PRINCESS LIMA" }, { command: "transition", scene: "LimaWorld", region: "village" } ], malrecEntrance: [ { command: "lock", reason: "cutscene" }, { command: "camera", target: "malrec", zoom: 1.18, duration: 700 }, { command: "lighting", preset: "shadow" }, { command: "dialogue", id: "malrec" }, { command: "cameraRestore", duration: 500 }, { command: "unlock", reason: "cutscene" } ] }); function validateDialogue(dialogue) { if (!dialogue || !dialogue.id || !dialogue.start || !dialogue.nodes || !dialogue.nodes[dialogue.start]) return false; return Object.values(dialogue.nodes).every((node) => { if (!node.speaker || typeof node.text !== "string") return false; if (node.next && !dialogue.nodes[node.next]) return false; return !node.choices || node.choices.every((choice) => choice.text && dialogue.nodes[choice.next]); }); } function validateAssetManifest(manifest) { return Boolean(manifest && manifest.schemaVersion === SCHEMA_VERSION && Array.isArray(manifest.shared) && manifest.shared.every((item) => item.key && item.url && item.type)); } function validateAll() { const errors = []; if (!validateAssetManifest(ASSETS)) errors.push("asset-manifest"); Object.entries(DIALOGUES).forEach(([id, dialogue]) => { if (!validateDialogue(dialogue)) errors.push(`dialogue:${id}`); }); Object.entries(ENEMIES).forEach(([id, enemy]) => { if (!ACTORS[id] || enemy.health <= 0 || enemy.speed <= 0) errors.push(`enemy:${id}`); }); MAP_IDS.forEach((id) => { if (!MAP_URLS[id] || !MAP_NAMES[id]) errors.push(`map:${id}`); }); return errors; } return Object.freeze({ SCHEMA_VERSION, WIDTH, HEIGHT, TILE, MAP_IDS, MAP_NAMES, MAP_URLS, ASSETS, ACTORS, ENEMIES, ITEMS, QUESTS, PORTRAITS, DIALOGUES, CUTSCENES, validateDialogue, validateAssetManifest, validateAll }); }));