Refactor platform UI and data flow across core pages
All checks were successful
Build Org Website / build (push) Successful in 50s
All checks were successful
Build Org Website / build (push) Successful in 50s
This commit is contained in:
161
assets/scripts/pages/princess-lima-scenes.js
Normal file → Executable file
161
assets/scripts/pages/princess-lima-scenes.js
Normal file → Executable 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"));
|
||||
|
||||
Reference in New Issue
Block a user