Files
org_web/assets/scripts/pages/princess-lima-v2-data.js
gitea-actions 6066f486de
All checks were successful
Build Org Website / build (push) Successful in 1m2s
Refine Princess Lima for v2 presentation
2026-07-30 15:00:45 +01:00

235 lines
16 KiB
JavaScript

(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: 64, damage: 18, speed: 70, awareness: 520, attackRange: 190, cooldown: 1200, telegraph: 520 }
});
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
});
}));