Files
org_web/assets/scripts/pages/princess-lima-scenes.js
gitea-actions fe8acac707
All checks were successful
Build Org Website / build (push) Successful in 39s
Refactor org web platform and remove legacy code
2026-07-30 11:29:20 +01:00

536 lines
26 KiB
JavaScript

(function (root) {
"use strict";
const Data = root.PrincessLimaData;
const Systems = root.PrincessLimaSystems;
const State = root.PrincessLimaState;
function createSceneClasses(controller) {
class BootScene extends Phaser.Scene {
constructor() { super("LimaBoot"); }
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 });
const audio = "/assets/audio/princess-lima/";
[
"village-theme", "forest-theme", "mountain-theme", "fortress-theme", "boss-theme", "victory-theme",
"step", "attack", "damage", "defeat", "pickup", "quest", "puzzle", "door", "victory"
].forEach((key) => this.load.audio(key, `${audio}${key}.wav`));
this.load.on("loaderror", (file) => controller.devWarn(`Optional asset failed: ${file.key}`));
}
create() {
this.add.image(Data.WIDTH / 2, Data.HEIGHT / 2, "lima-title").setDisplaySize(Data.WIDTH, Data.HEIGHT);
controller.ready();
}
}
class WorldScene extends Phaser.Scene {
constructor() {
super("LimaWorld");
this.regionId = "village";
this.facing = "east";
this.lastAttack = 0;
this.lastHit = 0;
this.stepDistance = 0;
this.previous = null;
this.puzzleInput = [];
this.enemySerial = 0;
}
init(data) {
this.regionId = Data.MAPS[data && data.region] ? data.region : controller.getState().region;
}
create() {
controller.scene = this;
controller.transitioning = false;
this.map = Data.MAPS[this.regionId];
this.physics.world.setBounds(0, 0, Data.WIDTH, Data.HEIGHT);
this.drawMap();
this.solids = this.physics.add.staticGroup();
Systems.activeObstacles(controller.getState(), this.regionId).forEach((shape) => this.addSolid(shape));
this.interactables = [];
this.createExits();
this.createPuzzle();
this.createPickups();
this.createNpcs();
this.createPlayer();
this.createEnemies();
this.createInput();
this.projectiles = this.physics.add.group();
this.physics.add.collider(this.projectiles, this.solids, (projectile) => projectile.destroy());
this.physics.add.overlap(this.player, this.projectiles, (_player, projectile) => {
this.hurtPlayer(projectile.getData("damage") || 10, projectile.x, projectile.y);
projectile.destroy();
});
this.cameras.main.setBounds(0, 0, Data.WIDTH, Data.HEIGHT);
this.cameras.main.startFollow(this.player, true, controller.reducedMotion() ? 1 : 0.2, controller.reducedMotion() ? 1 : 0.2);
this.cameras.main.setDeadzone(90, 60);
controller.audio.attach(this, this.regionId);
controller.updateHud();
controller.status(`${this.map.name}. ${Systems.currentObjective(controller.getState())}`);
controller.persistPosition(true);
if (this.regionId === "bossArena" && !controller.getState().defeatedBosses.includes("malrec")) {
controller.openDialogue("malrec", () => {
let state = controller.getState();
state.flags.boss_started = true;
state = Systems.startQuest(state, "defeat_malrec");
controller.setState(state, "The final battle begins.");
this.refreshRequiredEnemies();
});
}
}
drawMap() {
const [ground, path, solid, accent] = this.map.palette;
const groundColor = Phaser.Display.Color.HexStringToColor(ground).color;
const pathColor = Phaser.Display.Color.HexStringToColor(path).color;
this.cameras.main.setBackgroundColor(ground);
this.add.rectangle(Data.WIDTH / 2, Data.HEIGHT / 2, Data.WIDTH, Data.HEIGHT, groundColor).setDepth(0);
const grid = this.add.graphics().setDepth(1);
grid.lineStyle(1, pathColor, 0.18);
for (let x = 0; x <= Data.WIDTH; x += 40) grid.lineBetween(x, 0, x, Data.HEIGHT);
for (let y = 0; y <= Data.HEIGHT; y += 40) grid.lineBetween(0, y, Data.WIDTH, y);
this.add.rectangle(Data.WIDTH / 2, Data.HEIGHT / 2, Data.WIDTH - 110, 116, pathColor, 0.55).setDepth(1);
this.add.rectangle(Data.WIDTH / 2, Data.HEIGHT / 2, 110, Data.HEIGHT - 90, pathColor, 0.38).setDepth(1);
this.map.obstacles.forEach((shape) => this.drawShape(shape, solid, accent));
(this.map.dynamicObstacles || []).forEach((shape) => {
if (!controller.getState().unlockedRoutes.includes(shape.opensWith)) this.drawShape(shape, solid, accent);
else if (shape.id === "bridge") this.add.rectangle(shape.x + shape.width / 2, shape.y + shape.height / 2, shape.width, 74, 0x8d6d46).setDepth(2);
});
this.add.text(24, 20, `${this.map.name} · Chapter ${this.map.chapter}`, {
fontFamily: "monospace", fontSize: "18px", color: "#fff5d6",
backgroundColor: "#090b12cc", padding: { x: 10, y: 6 }
}).setScrollFactor(0).setDepth(900);
}
drawShape(shape, solid, accent) {
const main = Phaser.Display.Color.HexStringToColor(solid).color;
const edge = Phaser.Display.Color.HexStringToColor(accent).color;
if (shape.shape === "circle") {
this.add.circle(shape.x, shape.y, shape.radius, main).setStrokeStyle(4, edge, 0.85).setDepth(3);
} else {
this.add.rectangle(shape.x + shape.width / 2, shape.y + shape.height / 2, shape.width, shape.height, main)
.setStrokeStyle(4, edge, 0.75).setDepth(3);
}
}
addSolid(shape) {
let object;
if (shape.shape === "circle") {
object = this.add.circle(shape.x, shape.y, shape.radius, 0xffffff, 0);
this.physics.add.existing(object, true);
object.body.setCircle(shape.radius);
} else {
object = this.add.rectangle(shape.x + shape.width / 2, shape.y + shape.height / 2, shape.width, shape.height, 0xffffff, 0);
this.physics.add.existing(object, true);
}
this.solids.add(object);
}
createPlayer() {
const state = controller.getState();
const saved = state.region === this.regionId ? state.position : { x: this.map.spawns[Object.keys(this.map.spawns)[0]].x, y: this.map.spawns[Object.keys(this.map.spawns)[0]].y };
const safe = Systems.nearestSafeSpawn(state, this.regionId, saved.x, saved.y);
this.facing = state.position.facing || "east";
this.player = this.physics.add.sprite(safe.x, safe.y, "lima-cast", playerFrame(this.facing)).setScale(0.3).setDepth(100);
this.player.body.setSize(70, 42).setOffset(122, 215);
this.player.setCollideWorldBounds(true).setMaxVelocity(176, 176);
this.physics.add.collider(this.player, this.solids);
this.physics.add.collider(this.player, this.npcGroup);
this.previous = { x: this.player.x, y: this.player.y };
}
createNpcs() {
this.npcGroup = this.physics.add.staticGroup();
(this.map.npcs || []).forEach(([id, x, y]) => {
const npc = this.npcGroup.create(x, y, "lima-cast", Data.NPCS[id].frame).setScale(0.29).setDepth(y + 40);
npc.refreshBody();
npc.body.setSize(70, 42).setOffset(122, 215);
this.interactables.push({ type: "npc", id, x, y, sprite: npc });
});
}
createEnemies() {
this.enemies = this.physics.add.group();
this.physics.add.collider(this.enemies, this.solids);
this.physics.add.collider(this.enemies, this.enemies);
this.physics.add.collider(this.enemies, this.npcGroup);
this.physics.add.overlap(this.player, this.enemies, (_player, enemyObject) => {
this.hurtPlayer(enemyObject.getData("spec").damage, enemyObject.x, enemyObject.y);
});
(this.map.enemies || []).forEach((spawn) => {
if (spawn.boss && controller.getState().defeatedBosses.includes(spawn.type)) return;
this.spawnEnemy(spawn);
});
}
spawnEnemy(spawn) {
const spec = Data.ENEMIES[spawn.type];
const enemyObject = this.physics.add.sprite(spawn.x, spawn.y, "lima-cast", spec.frame)
.setScale(spec.boss ? 0.34 : 0.25).setDepth(spawn.y + 30);
enemyObject.body.setSize(spec.boss ? 130 : 95, spec.boss ? 78 : 58).setOffset(spec.boss ? 92 : 110, spec.boss ? 200 : 205);
enemyObject.setData({
id: `${spawn.type}-${++this.enemySerial}`, type: spawn.type, spec, spawn,
health: spec.health, homeX: spawn.x, homeY: spawn.y, nextAction: this.time.now + 900, phase: 1
});
this.enemies.add(enemyObject);
this.applyEnemyRequirement(enemyObject);
}
applyEnemyRequirement(enemyObject) {
const requirement = enemyObject.getData("spawn").requires;
const available = !requirement || controller.getState().flags[requirement]
|| controller.getState().unlockedRoutes.includes(requirement);
enemyObject.setVisible(available);
enemyObject.body.enable = available;
}
refreshRequiredEnemies() {
this.enemies.getChildren().forEach((enemyObject) => this.applyEnemyRequirement(enemyObject));
}
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);
this.interactables.push({ type: "exit", id: item.id, x: item.x + item.width / 2, y: item.y + item.height / 2, data: item, sprite: zone });
});
}
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);
this.interactables.push({ type: "puzzle", id, puzzle, x, y, sprite: node });
});
}
createPickups() {
(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);
this.interactables.push({ type: "pickup", id, chestId, x, y, sprite: item });
});
}
createInput() {
this.cursors = this.input.keyboard.createCursorKeys();
this.keys = this.input.keyboard.addKeys({
up: "W", down: "S", left: "A", right: "D", interact: "E", enter: "ENTER",
item: "Q", pause: "ESC", inventory: "I", quests: "J"
});
}
update(time, delta) {
if (!this.player) return;
if (controller.locked || controller.transitioning) {
this.player.setVelocity(0);
return;
}
const traveled = Math.hypot(this.player.x - this.previous.x, this.player.y - this.previous.y);
this.previous = { x: this.player.x, y: this.player.y };
let dx = 0;
let dy = 0;
if (this.cursors.left.isDown || this.keys.left.isDown || controller.move.left) dx -= 1;
if (this.cursors.right.isDown || this.keys.right.isDown || controller.move.right) dx += 1;
if (this.cursors.up.isDown || this.keys.up.isDown || controller.move.up) dy -= 1;
if (this.cursors.down.isDown || this.keys.down.isDown || controller.move.down) dy += 1;
const velocity = Systems.approachVelocity(
this.player.body.velocity.x, this.player.body.velocity.y, dx, dy, delta,
Boolean(controller.getState().equipment.boots)
);
this.player.setVelocity(velocity.x, velocity.y);
if (dx || dy) {
this.facing = Math.abs(dx) > Math.abs(dy) ? (dx < 0 ? "west" : "east") : (dy < 0 ? "north" : "south");
this.player.setFrame(playerFrame(this.facing));
const bob = controller.reducedMotion() ? 1 : 1 + Math.sin(time / 90) * 0.018;
this.player.setScale(0.3, 0.3 * bob);
this.stepDistance += traveled;
if (this.stepDistance >= 48) {
controller.audio.play("step", 0.45);
this.stepDistance = 0;
}
} else {
this.player.setScale(0.3);
this.stepDistance = 0;
}
this.player.setDepth(this.player.y + 80);
this.updateNearest();
this.updateEnemies(time);
this.handleKeys(time);
if ((dx || dy) && time - controller.lastSaveAt > 900) {
controller.lastSaveAt = time;
controller.persistPosition(false);
}
}
updateNearest() {
let best = Infinity;
let nearest = null;
this.interactables.forEach((item) => {
const distance = Math.hypot(item.x - this.player.x, item.y - this.player.y);
const radius = item.type === "exit" ? 70 : 54;
if (distance <= radius && distance < best) {
best = distance;
nearest = item;
}
});
this.nearest = nearest;
controller.prompt(nearest ? promptFor(nearest) : "");
}
handleKeys(time) {
if (Phaser.Input.Keyboard.JustDown(this.keys.interact) || Phaser.Input.Keyboard.JustDown(this.keys.enter)) this.interact();
if (Phaser.Input.Keyboard.JustDown(this.cursors.space)) this.attack(time);
if (Phaser.Input.Keyboard.JustDown(this.keys.item)) controller.useTonic();
if (Phaser.Input.Keyboard.JustDown(this.keys.pause)) controller.openPanel("pause");
if (Phaser.Input.Keyboard.JustDown(this.keys.inventory)) controller.openPanel("inventory");
if (Phaser.Input.Keyboard.JustDown(this.keys.quests)) controller.openPanel("quests");
}
interact() {
const item = this.nearest;
if (!item) return controller.status("There is nothing close enough to interact with.");
if (item.type === "exit") {
if (item.data.requirement && !controller.getState().unlockedRoutes.includes(item.data.requirement)) {
return controller.status(`The route is blocked. ${Systems.currentObjective(controller.getState())}`);
}
controller.travel(item.data.target, item.data.spawn);
} else if (item.type === "npc") this.interactNpc(item);
else if (item.type === "puzzle") this.activatePuzzle(item);
else if (item.type === "pickup") this.collect(item);
}
interactNpc(item) {
const dx = item.x - this.player.x;
const dy = item.y - this.player.y;
this.facing = Math.abs(dx) > Math.abs(dy) ? (dx < 0 ? "west" : "east") : (dy < 0 ? "north" : "south");
this.player.setFrame(playerFrame(this.facing));
controller.openDialogue(item.id, () => this.resolveNpc(item.id));
}
resolveNpc(id) {
let state = controller.getState();
if (id === "elder" && !state.flags.aftermath_complete) {
state = Systems.completeQuest(Systems.startQuest(state, "aftermath"), "aftermath");
state = Systems.startQuest(state, "village_defence");
controller.setState(state, "Wayfarer Sword acquired. The second raid has begun.");
this.refreshRequiredEnemies();
} else if (id === "nia") {
state = Systems.startQuest(state, "healer_herbs");
if (Systems.quantity(state, "silver_leaf") >= 2) {
state = Systems.completeQuest(state, "healer_herbs");
controller.setState(state, "Nia brews two healing tonics.");
} else controller.setState(state, "Optional quest started: collect two Silver Leaves.");
} else if (id === "tovin") {
state = Systems.startQuest(state, "find_guide");
controller.setState(state, "Wake the three standing stones from youngest tree to oldest.");
} else if (id === "bram" && this.regionId === "mountain") {
state = Systems.startQuest(state, "repair_bridge");
controller.setState(state, "Restart the west and east bridge winches.");
} else if (id === "elowen" && this.regionId === "camp") {
state = Systems.startQuest(state, "free_scout");
state = Systems.progressQuest(state, "free_scout", 1);
controller.setState(state, "Elowen is free. Defeat Captain Veyr for his emblem.");
} else if (id === "elowen" && this.regionId === "fortressInterior") {
state = Systems.startQuest(state, "free_prisoners");
state = Systems.progressQuest(state, "free_prisoners", 1);
state = Systems.startQuest(state, "break_wards");
controller.setState(state, "Elowen's captured ally is free. Disable the fortress wards.");
} else if (id === "prisoner") {
state = Systems.startQuest(state, "free_prisoners");
state = Systems.progressQuest(state, "free_prisoners", 1);
state = Systems.startQuest(state, "break_wards");
controller.setState(state, "A prisoner is free. Disable the fortress wards.");
} else if (id === "lima" && state.defeatedBosses.includes("malrec")) {
state.rescued = true;
state.story = "complete";
controller.setState(State.normalize(state), "Princess Lima is safe.");
controller.openEnding();
}
}
activatePuzzle(item) {
const puzzle = item.puzzle;
const state = controller.getState();
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;
}
this.puzzleInput.push(item.id);
const valid = this.puzzleInput.every((value, index) => value === puzzle.sequence[index]);
if (!valid) {
this.puzzleInput = [];
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}.`);
}
finishPuzzle(id) {
let state = Systems.solvePuzzle(controller.getState(), id);
if (id === "sun_pedestals") {
state.solvedPuzzles.push(id);
state.flags.sun_veil_broken = true;
state = State.normalize(state);
}
controller.setState(state, "Puzzle complete. A sealed route opens.");
controller.audio.play("puzzle");
if (id === "bridge_winches") controller.reloadRegion();
}
collect(item) {
let state = controller.getState();
if (state.openedChests.includes(item.chestId)) return;
state.openedChests.push(item.chestId);
state = Systems.addItem(state, item.id, 1);
controller.setState(state, `${Data.ITEMS[item.id].name} collected.`);
controller.audio.play("pickup");
item.sprite.destroy();
this.interactables = this.interactables.filter((entry) => entry !== item);
}
attack(time) {
if (time - this.lastAttack < 320 || controller.locked) return;
this.lastAttack = time;
controller.audio.play("attack");
const direction = faceVector(this.facing);
const arc = this.add.arc(
this.player.x + direction.x * 46, this.player.y + direction.y * 38,
38, direction.angle - 58, direction.angle + 58, false, 0xffe29a, 0.52
).setDepth(500);
this.physics.add.existing(arc);
this.physics.overlap(arc, this.enemies, (_hit, enemyObject) => this.hitEnemy(enemyObject, direction));
this.time.delayedCall(controller.reducedMotion() ? 75 : 120, () => arc.destroy());
}
hitEnemy(enemyObject, direction) {
if (!enemyObject.active || !enemyObject.visible) return;
if (enemyObject.getData("type") === "malrec" && enemyObject.getData("phase") >= 3 && !controller.getState().flags.sun_veil_broken) {
controller.status("Malrec's final veil holds. Activate both Sun Crystal pedestals.");
return;
}
const health = enemyObject.getData("health") - controller.getState().attack;
enemyObject.setData("health", health);
enemyObject.setVelocity(direction.x * 180, direction.y * 180).setTint(0xffc5c5);
this.time.delayedCall(100, () => { if (enemyObject.active) enemyObject.clearTint(); });
if (health <= 0) this.defeatEnemy(enemyObject);
}
defeatEnemy(enemyObject) {
const type = enemyObject.getData("type");
const spawn = enemyObject.getData("spawn");
const wasBoss = enemyObject.getData("spec").boss;
enemyObject.destroy();
controller.audio.play("defeat");
let state = controller.getState();
if (spawn.quest === "village_defence") state = Systems.progressQuest(state, "village_defence", 1);
if (wasBoss) state = Systems.recordBoss(state, type);
controller.setState(state, wasBoss ? `${Data.ENEMIES[type].name} defeated. The route is open.` : `${Data.ENEMIES[type].name} defeated.`);
if (type === "malrec") {
controller.audio.play("victory");
this.time.delayedCall(500, () => controller.travel("chamber", "door"));
}
}
updateEnemies(time) {
this.enemies.getChildren().forEach((enemyObject) => {
if (!enemyObject.active || !enemyObject.body.enable) return;
const spec = enemyObject.getData("spec");
const distance = Math.hypot(enemyObject.x - this.player.x, enemyObject.y - this.player.y);
const homeDistance = Math.hypot(enemyObject.x - enemyObject.getData("homeX"), enemyObject.y - enemyObject.getData("homeY"));
if (distance > 270 || homeDistance > enemyObject.getData("spawn").leash) {
this.physics.moveTo(enemyObject, enemyObject.getData("homeX"), enemyObject.getData("homeY"), spec.speed);
return;
}
if (spec.behaviour === "wander" && time > enemyObject.getData("nextAction")) {
const angle = Math.random() * Math.PI * 2;
enemyObject.setVelocity(Math.cos(angle) * spec.speed, Math.sin(angle) * spec.speed);
enemyObject.setData("nextAction", time + 650);
} else if (["charge", "slam", "final"].includes(spec.behaviour)) this.updateBoss(enemyObject, time, distance);
else this.physics.moveToObject(enemyObject, this.player, spec.speed);
enemyObject.setDepth(enemyObject.y + 60);
});
}
updateBoss(enemyObject, time, distance) {
const spec = enemyObject.getData("spec");
const ratio = enemyObject.getData("health") / spec.health;
const phase = Systems.bossPhase(enemyObject.getData("health"), spec.health, spec.phases || 1);
enemyObject.setData("phase", phase);
controller.boss(spec.name, enemyObject.getData("health"), spec.health);
if (time < enemyObject.getData("nextAction")) return;
enemyObject.setVelocity(0).setTint(0xf5c96e);
const telegraph = controller.getState().settings.reducedMotion ? 780 : 560;
enemyObject.setData("nextAction", time + 1500);
this.time.delayedCall(telegraph, () => {
if (!enemyObject.active) return;
enemyObject.clearTint();
if (spec.behaviour === "final" && phase >= 2) this.fireRadial(enemyObject, phase === 3 ? 8 : 5);
else if (distance < 340) this.physics.moveToObject(enemyObject, this.player, spec.speed * 2.1);
});
}
fireRadial(enemyObject, count) {
for (let index = 0; index < count; index += 1) {
const angle = Math.PI * 2 * index / count;
const shot = this.add.circle(enemyObject.x, enemyObject.y, 8, 0x6f3d89).setStrokeStyle(2, 0xf1b4ff).setDepth(400);
this.physics.add.existing(shot);
shot.body.setVelocity(Math.cos(angle) * 150, Math.sin(angle) * 150);
shot.setData("damage", 13);
this.projectiles.add(shot);
this.time.delayedCall(3600, () => { if (shot.active) shot.destroy(); });
}
}
hurtPlayer(amount, fromX, fromY) {
const result = Systems.damage(controller.getState(), amount, this.time.now, this.lastHit);
if (!result.hit) return;
this.lastHit = this.time.now;
controller.setState(result.state, `You take ${result.amount} damage.`);
controller.audio.play("damage");
const push = Systems.normalizedVector(this.player.x - fromX, this.player.y - fromY, 210);
this.player.setVelocity(push.x, push.y).setTint(0xff8c8c);
this.time.delayedCall(170, () => { if (this.player.active) this.player.clearTint(); });
if (!controller.reducedMotion() && controller.getState().settings.screenShake) this.cameras.main.shake(100, 0.003);
if (result.defeated) controller.gameOver();
}
}
return [BootScene, WorldScene];
}
function playerFrame(facing) {
return { south: 0, east: 1, north: 2, west: 3 }[facing] || 0;
}
function faceVector(facing) {
return {
north: { x: 0, y: -1, angle: 270 }, south: { x: 0, y: 1, angle: 90 },
west: { x: -1, y: 0, angle: 180 }, east: { x: 1, y: 0, angle: 0 }
}[facing];
}
function promptFor(item) {
if (item.type === "exit") return `${item.data.label} · E / Enter`;
if (item.type === "npc") return `Speak with ${Data.NPCS[item.id].name} · E / Enter`;
if (item.type === "puzzle") return `Activate ${item.id} · E / Enter`;
if (item.type === "pickup") return `Collect ${Data.ITEMS[item.id].name} · E / Enter`;
return "Interact · E / Enter";
}
root.PrincessLimaScenes = Object.freeze({ createSceneClasses, playerFrame });
}(typeof globalThis !== "undefined" ? globalThis : this));