import {requirementMet} from "../core/state.mjs"; const WIDTH = 1280; const HEIGHT = 720; export function createScenes(controller) { class BootScene extends Phaser.Scene { constructor() { super("Boot"); } create() { this.scene.start("Preload"); } } class PreloadScene extends Phaser.Scene { constructor() { super("Preload"); } preload() { const images = controller.content.assets.images.filter((record) => record.type === "character" || record.id === "courtyard"); images.forEach((record) => this.load.image(record.id, record.url)); this.load.on("progress", (value) => controller.loading(Math.round(value * 100), "Painting the city…")); this.load.on("loaderror", (file) => controller.assetError(file.src)); } create() { controller.ready(); this.scene.sleep(); } } class StoryScene extends Phaser.Scene { constructor() { super("Story"); this.locked = false; this.near = null; this.move = 0; this.velocity = 0; } create() { this.location = controller.content.locations[controller.store.get().location]; const bundle = [this.location.background, ...(this.location.actors || []).map((actor) => actor.id)]; const missing = bundle.filter((id) => !this.textures.exists(id)); if (missing.length) { controller.locationLoading(true, `Painting ${this.location.name}…`); missing.forEach((id) => { const record = controller.content.assets.images.find((asset) => asset.id === id); if (record) this.load.image(record.id, record.url); }); this.load.once("complete", () => { controller.locationLoading(false); this.createWorld(); }); this.load.once("loaderror", (file) => controller.assetError(file.src)); this.load.start(); return; } this.createWorld(); } createWorld() { this.background = this.add.image(WIDTH / 2, HEIGHT / 2, this.location.background).setDisplaySize(WIDTH, HEIGHT); this.background.setTint(this.location.background === "pavilion" ? 0xc9d5ff : 0xffffff); const nightAlpha = this.location.act.startsWith("Act III") ? 0.2 : this.location.name === "Moon Garden" ? 0.1 : 0; if (nightAlpha) this.add.rectangle(WIDTH / 2, HEIGHT / 2, WIDTH, HEIGHT, 0x18315f, nightAlpha).setDepth(2); this.player = this.add.image(WIDTH * controller.store.get().position, 577, "z").setOrigin(0.5, 1).setDisplaySize(142, 300).setDepth(5); this.companion = this.add.image(Math.max(160, this.player.x - 128), 577, "lima").setOrigin(0.5, 1).setDisplaySize(146, 305).setDepth(4); this.location.actors?.forEach((actor, index) => { this.add.image(WIDTH * actor.x, 577, actor.id) .setOrigin(0.5, 1) .setDisplaySize(118, 270) .setDepth(3 + (index % 2) * 0.1); }); this.hotspots = this.location.hotspots.map((definition) => ({ definition, x: WIDTH * definition.x })); this.cursors = this.input.keyboard.createCursorKeys(); this.keys = this.input.keyboard.addKeys("W,A,S,D,E,ENTER,J,ESC"); this.input.on("pointerdown", (pointer) => { if (this.locked || controller.uiBusy()) return; const target = Phaser.Math.Clamp(pointer.x, 56, WIDTH - 56); if (Math.abs(target - this.player.x) < 48) this.interact(); else this.walkTarget = target; }); this.input.keyboard.on("keydown-E", () => this.interact()); this.input.keyboard.on("keydown-ENTER", () => this.interact()); this.input.keyboard.on("keydown-J", () => controller.openJournal()); this.input.keyboard.on("keydown-ESC", () => controller.pause()); controller.locationReady(this.location); this.createForeground(); this.createAtmosphere(); this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { controller.store.dispatch({type: "position", value: this.player.x / WIDTH}); }); } createAtmosphere() { if (controller.store.get().settings.effectsQuality === "minimal" || controller.store.get().settings.reducedMotion) return; const texture = this.make.graphics({x: 0, y: 0, add: false}); texture.fillStyle(0xffe8a3, 1); texture.fillCircle(5, 5, 4); texture.generateTexture("tr-firefly", 10, 10); texture.destroy(); const particles = this.add.particles(0, 0, "tr-firefly", { x: {min: 0, max: WIDTH}, y: {min: 120, max: 580}, lifespan: {min: 2800, max: 5200}, speedX: {min: -6, max: 6}, speedY: {min: -7, max: 2}, alpha: {start: 0, ease: "Sine.easeInOut", yoyo: true, end: 0.8}, scale: {start: 0.25, end: 0.7}, frequency: controller.store.get().settings.effectsQuality === "full" ? 420 : 850, blendMode: "ADD" }).setDepth(3); if (!particles) return; } createForeground() { const foreground = this.add.graphics().setDepth(8); foreground.fillStyle(this.location.name === "Royal Library" ? 0x16110d : 0x071c17, 0.72); foreground.fillEllipse(30, 710, 250, 170); foreground.fillEllipse(1245, 710, 290, 190); foreground.fillStyle(0x102d25, 0.58); for (let index = 0; index < 8; index += 1) { foreground.fillEllipse(index < 4 ? 42 + index * 25 : 1160 + index * 18, 620 + (index % 3) * 34, 44, 115); } } update(_time, delta) { if (!this.player || !this.cursors || this.locked || controller.uiBusy()) return; const left = this.cursors.left.isDown || this.keys.A.isDown; const right = this.cursors.right.isDown || this.keys.D.isDown; if (left || right) { this.walkTarget = null; this.move = left ? -1 : 1; } else if (this.walkTarget != null) { const distance = this.walkTarget - this.player.x; this.move = Math.abs(distance) < 7 ? 0 : Math.sign(distance); if (!this.move) this.walkTarget = null; } else this.move = controller.touchDirection; const targetVelocity = this.move * 0.31; this.velocity = Phaser.Math.Linear(this.velocity, targetVelocity, Math.min(1, delta / (this.move ? 130 : 90))); if (Math.abs(this.velocity) < 0.008 && !this.move) this.velocity = 0; this.player.x = Phaser.Math.Clamp(this.player.x + this.velocity * delta, 52, WIDTH - 52); if (this.velocity) this.player.scaleX = Math.abs(this.player.scaleX) * (this.velocity < 0 ? -1 : 1); const companionTarget = this.player.x - 105 * (this.velocity < 0 ? -1 : 1); this.companion.x = Phaser.Math.Linear(this.companion.x, companionTarget, Math.min(1, delta / 260)); this.companion.scaleX = Math.abs(this.companion.scaleX) * (this.player.x < this.companion.x ? -1 : 1); const bob = this.velocity ? Math.sin(performance.now() / 115) * 3 : Math.sin(performance.now() / 600) * 1.1; this.player.y = 577 + bob; this.companion.y = 577 - bob * 0.55; if (this.velocity && _time > (this.nextFootstep || 0)) { controller.audio.sfx("footstep"); this.nextFootstep = _time + 390; } if (!controller.store.get().settings.reducedMotion) this.background.x = WIDTH / 2 - (this.player.x - WIDTH / 2) * 0.025; this.updateNearest(); } updateNearest() { const state = controller.store.get(); const candidates = this.hotspots.filter(({definition, x}) => (!definition.once || !state.flags[`done_${definition.id}`]) && (definition.requires || []).every((requirement) => requirementMet(state, requirement)) && Math.abs(x - this.player.x) < 92); const priority = (definition) => { if (Number.isFinite(definition.priority)) return definition.priority; if (definition.dialogue || definition.puzzle || definition.minigame) return 0; if (definition.gift) return 1; if (definition.collect) return 2; return 3; }; candidates.sort((a, b) => priority(a.definition) - priority(b.definition) || Math.abs(a.x - this.player.x) - Math.abs(b.x - this.player.x)); const near = candidates[0] || null; if (near?.definition.id !== this.near?.definition.id) { this.near = near; controller.interaction(near?.definition.label || null); } } interact() { if (!this.near || this.locked || controller.uiBusy()) return; const definition = this.near.definition; this.locked = true; controller.interaction(null); const release = () => { if (definition.once) controller.store.dispatch({type: "flag", id: `done_${definition.id}`}); this.locked = false; this.updateNearest(); }; if (definition.dialogue) controller.dialogue(definition.dialogue, (ending) => { if (ending) { controller.ending(); return; } release(); }); else if (definition.goto) controller.goto(definition.goto); else if (definition.puzzle) controller.puzzle(definition.puzzle, release); else if (definition.minigame) controller.minigame(definition.minigame, release); else if (definition.gift) controller.gifts(release); else if (definition.collect) { const quest = controller.content.quests[definition.quest]; const before = controller.store.get().quests[definition.quest].status; controller.store.dispatch({ type: "questProgress", id: definition.quest, item: definition.collect, target: quest.target, reward: quest.reward }); controller.audio.sfx("pickup"); const after = controller.store.get().quests[definition.quest].status; controller.toast(after === "complete" && before !== "complete" ? `${quest.name} complete.` : `${definition.label.replace("Gather ", "").replace("Recover ", "")} added to the journal.`); controller.autosave(); release(); } else release(); } } class ArcheryScene extends Phaser.Scene { constructor() { super("Archery"); } create() { controller.runMiniOverlay("archery", () => this.scene.stop()); } } class CookingScene extends Phaser.Scene { constructor() { super("Cooking"); } create() { controller.runMiniOverlay("cooking", () => this.scene.stop()); } } class DanceScene extends Phaser.Scene { constructor() { super("Dance"); } create() { controller.runMiniOverlay("dance", () => this.scene.stop()); } } class EndingScene extends Phaser.Scene { constructor() { super("Ending"); } create() { controller.runEnding(); } } return [BootScene, PreloadScene, StoryScene, ArcheryScene, CookingScene, DanceScene, EndingScene]; }