Serve Two Rivers modules with production MIME type
All checks were successful
Build Org Website / build (push) Successful in 45s
All checks were successful
Build Org Website / build (push) Successful in 45s
This commit is contained in:
224
assets/scripts/games/two-rivers/scenes/game-scenes.js
Normal file
224
assets/scripts/games/two-rivers/scenes/game-scenes.js
Normal file
@@ -0,0 +1,224 @@
|
||||
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];
|
||||
}
|
||||
Reference in New Issue
Block a user