Refactor platform UI and data flow across core pages
All checks were successful
Build Org Website / build (push) Successful in 50s

This commit is contained in:
gitea-actions
2026-07-30 12:22:44 +01:00
parent fe8acac707
commit a0c78929b3
45 changed files with 1168 additions and 348 deletions

161
assets/scripts/pages/princess-lima-scenes.js Normal file → Executable file
View File

@@ -4,6 +4,7 @@
const Data = root.PrincessLimaData;
const Systems = root.PrincessLimaSystems;
const State = root.PrincessLimaState;
const Maps = root.PrincessLimaMaps;
function createSceneClasses(controller) {
class BootScene extends Phaser.Scene {
@@ -12,16 +13,20 @@
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-world-tiles", "/assets/images/play/princess-lima/world-tiles.png", { frameWidth: 32, frameHeight: 32 });
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", "damage", "defeat", "pickup", "quest", "puzzle", "door", "victory"
"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);
this.add.image(500, Data.HEIGHT / 2, "lima-title").setDisplaySize(1000, Data.HEIGHT);
controller.audio.attach(this, "village");
controller.ready();
}
}
@@ -37,6 +42,9 @@
this.previous = null;
this.puzzleInput = [];
this.enemySerial = 0;
this.attacking = false;
this.attackToken = 0;
this.attackHits = new Set();
}
init(data) {
@@ -84,37 +92,8 @@
}
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);
}
this.cameras.main.setBackgroundColor(this.map.palette[0]);
Maps.render(this, this.regionId, this.map.obstacles, this.map.dynamicObstacles, controller.getState().unlockedRoutes);
}
addSolid(shape) {
@@ -223,8 +202,9 @@
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"
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) {
@@ -237,10 +217,11 @@
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;
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)
@@ -289,9 +270,11 @@
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.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() {
@@ -403,29 +386,104 @@
}
attack(time) {
if (time - this.lastAttack < 320 || controller.locked) return;
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(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());
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;
}
);
}
hitEnemy(enemyObject, direction) {
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);
enemyObject.setVelocity(direction.x * 180, direction.y * 180).setTint(0xffc5c5);
this.time.delayedCall(100, () => { if (enemyObject.active) enemyObject.clearTint(); });
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);
}
@@ -448,6 +506,7 @@
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"));