(function (root) { "use strict"; const Data = root.PrincessLimaData; const Systems = root.PrincessLimaSystems; const State = root.PrincessLimaState; const Maps = root.PrincessLimaMaps; 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 }); 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/"; [ "village-theme", "forest-theme", "mountain-theme", "fortress-theme", "boss-theme", "victory-theme", "step", "attack", "hit", "damage", "defeat", "pickup", "quest", "puzzle", "door", "victory", "intro-narration-1", "intro-narration-2", "intro-narration-3", "intro-narration-4" ].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.audio.attach(this, "village"); 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; this.attacking = false; this.attackToken = 0; this.attackHits = new Set(); } 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() { this.cameras.main.setBackgroundColor(this.map.palette[0]); Maps.render(this, this.regionId); } 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.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 }); }); } createPuzzle() { const puzzle = this.map.puzzle; if (!puzzle || controller.getState().solvedPuzzles.includes(puzzle.id)) return; puzzle.objects.forEach(([id, x, y]) => { const node = this.add.zone(x, y, 38, 38); const marker = this.createInteractionMarker(x, y, `${readableId(id)} · E`); this.interactables.push({ type: "puzzle", id, puzzle, x, y, sprite: node, marker }); }); } 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.zone(x, y, 34, 34); const marker = this.createInteractionMarker(x, y, `${Data.ITEMS[id].name} · E`); this.interactables.push({ type: "pickup", id, chestId, x, y, sprite: item, marker }); }); } createInteractionMarker(x, y, label) { return this.add.text(x, y - 32, label, { fontFamily: "monospace", fontSize: "13px", fontStyle: "bold", color: "#fff6ce", backgroundColor: "#111827", padding: { x: 7, y: 4 } }).setOrigin(0.5).setDepth(900).setVisible(false); } 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", menu: "M", inventory: "I", quests: "J", fullscreen: "F", cycle: "TAB" }); this.input.keyboard.addCapture(["SPACE", "TAB", "UP", "DOWN", "LEFT", "RIGHT"]); } 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) dx -= 1; if (this.cursors.right.isDown || this.keys.right.isDown) dx += 1; if (this.cursors.up.isDown || this.keys.up.isDown) dy -= 1; if (this.cursors.down.isDown || this.keys.down.isDown) dy += 1; if (this.attacking) dx = dy = 0; 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); if (item.marker) item.marker.setVisible(distance <= 92); 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) || Phaser.Input.Keyboard.JustDown(this.keys.menu)) 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"); if (Phaser.Input.Keyboard.JustDown(this.keys.fullscreen)) controller.toggleFullscreen(); if (Phaser.Input.Keyboard.JustDown(this.keys.cycle)) controller.cycleItem(); } 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); if (item.marker) item.marker.setText(`✓ ${readableId(item.id)}`); 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; } if (item.marker) item.marker.setText(`✓ ${readableId(item.id)}`); 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(); if (item.marker) item.marker.destroy(); this.interactables = this.interactables.filter((entry) => entry !== item); } attack(time) { if (time - this.lastAttack < Systems.ATTACK_TIMING.cooldown || controller.locked || this.attacking) return; this.lastAttack = time; this.attacking = true; const token = ++this.attackToken; const facing = this.facing; this.attackHits = new Set(); this.player.setVelocity(0); controller.audio.play("attack"); const direction = faceVector(facing); const blade = this.add.rectangle(0, -22, 6, 42, 0xeaf6ff).setStrokeStyle(2, 0x5e6b78); const guard = this.add.rectangle(0, 1, 20, 6, 0xf2c467).setStrokeStyle(1, 0x5b3d25); const grip = this.add.rectangle(0, 11, 6, 19, 0x70432d); const weapon = this.add.container( this.player.x + direction.x * 19, this.player.y + direction.y * 16, [blade, guard, grip] ).setDepth(690).setAngle(direction.angle + 5); const windupAngle = facing === "west" || facing === "north" ? -12 : 12; this.tweens.add({ targets: this.player, angle: windupAngle, scaleX: 0.29, scaleY: 0.31, duration: Systems.ATTACK_TIMING.windup, ease: "Stepped" }); this.tweens.add({ targets: weapon, angle: direction.angle + 90, duration: Systems.ATTACK_TIMING.windup + Systems.ATTACK_TIMING.active, ease: "Cubic.Out" }); this.time.delayedCall(Systems.ATTACK_TIMING.windup, () => { if (!this.player.active || token !== this.attackToken) return; this.player.setAngle(-windupAngle).setScale(0.32, 0.28); this.performAttackHit(facing, token); }); this.time.delayedCall(Systems.ATTACK_TIMING.windup + Systems.ATTACK_TIMING.active, () => { if (!this.player.active || token !== this.attackToken) return; this.player.setAngle(windupAngle / 2).setScale(0.3); }); this.time.delayedCall( Systems.ATTACK_TIMING.windup + Systems.ATTACK_TIMING.active + Systems.ATTACK_TIMING.recovery, () => { if (!this.player.active || token !== this.attackToken) return; this.player.setAngle(0).setScale(0.3).setFrame(playerFrame(this.facing)); if (weapon.active) weapon.destroy(); this.attacking = false; } ); } performAttackHit(facing, token) { const box = Systems.attackHitbox(facing, this.player.x, this.player.y); const direction = faceVector(facing); const hitbox = this.add.rectangle(box.x + box.width / 2, box.y + box.height / 2, box.width, box.height, 0xff4df3, controller.debugCollision ? 0.28 : 0); this.physics.add.existing(hitbox, true); const slash = this.add.graphics().setDepth(700); slash.lineStyle(9, 0xffefb0, 0.95).beginPath(); slash.arc( this.player.x + direction.x * 30, this.player.y + direction.y * 24, 50, Phaser.Math.DegToRad(direction.angle - 58), Phaser.Math.DegToRad(direction.angle + 58), false ).strokePath(); slash.lineStyle(3, 0xd3f7ff, 0.9).strokePath(); this.physics.overlap(hitbox, this.enemies, (_hit, enemyObject) => this.hitEnemy(enemyObject, direction, token)); this.time.delayedCall(Systems.ATTACK_TIMING.active, () => { if (hitbox.active) hitbox.destroy(); if (slash.active) slash.destroy(); }); } hitEnemy(enemyObject, direction, token) { if (!enemyObject.active || !enemyObject.visible) return; const enemyId = enemyObject.getData("id"); if (this.attackHits.has(enemyId) || token !== this.attackToken) return; this.attackHits.add(enemyId); 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); const pushDistance = controller.reducedMotion() ? 20 : enemyObject.getData("spec").boss ? 24 : 42; const target = Systems.wallSafeKnockback(controller.getState(), this.regionId, enemyObject.x, enemyObject.y, direction.x, direction.y, pushDistance); const push = Systems.normalizedVector(target.x - enemyObject.x, target.y - enemyObject.y, controller.reducedMotion() ? 90 : 190); enemyObject.setData("stunnedUntil", this.time.now + 210); enemyObject.setVelocity(push.x, push.y).setTintFill(0xffe1b0); controller.audio.play("hit", enemyObject.getData("spec").boss ? 1 : 0.78); const impact = this.add.star(enemyObject.x, enemyObject.y - 15, 6, 5, 15, 0xfff1a8, 0.95).setDepth(750); this.time.delayedCall(70, () => { if (impact.active) impact.destroy(); }); this.time.delayedCall(110, () => { if (enemyObject.active) { enemyObject.clearTint(); enemyObject.setVelocity(0); } }); const pause = enemyObject.getData("spec").boss ? 58 : 38; this.physics.world.pause(); this.time.delayedCall(pause, () => { if (this.physics.world) this.physics.world.resume(); }); if (enemyObject.getData("spec").boss && !controller.reducedMotion() && controller.getState().settings.screenShake) { this.cameras.main.shake(70, 0.0025); } 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; if (time < (enemyObject.getData("stunnedUntil") || 0)) 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"; } function readableId(value) { return String(value).replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); } root.PrincessLimaScenes = Object.freeze({ createSceneClasses, playerFrame }); }(typeof globalThis !== "undefined" ? globalThis : this));