Files
org_web/assets/scripts/games/two-rivers/scenes/game-scenes.js
gitea-actions 2ee6575539
All checks were successful
Build Org Website / build (push) Successful in 1m4s
updates
2026-08-19 10:45:57 +01:00

225 lines
10 KiB
JavaScript
Executable File

import {requirementMet} from "../core/state.js";
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];
}