Refine Archive World navigation and gate progression
All checks were successful
Build Org Website / build (push) Successful in 38s
All checks were successful
Build Org Website / build (push) Successful in 38s
This commit is contained in:
@@ -44,6 +44,9 @@
|
||||
this.enemySerial = 0;
|
||||
this.bossPuzzle = [];
|
||||
this.bossPuzzleSolved = false;
|
||||
this.controlLockedUntil = 0;
|
||||
this.stepDistance = 0;
|
||||
this.previousPlayerPosition = null;
|
||||
}
|
||||
|
||||
init(data) {
|
||||
@@ -55,6 +58,7 @@
|
||||
const area = Data.AREAS[this.areaId];
|
||||
this.area = area;
|
||||
this.physics.world.setBounds(0, 0, area.width, area.height);
|
||||
this.cameras.main.setBackgroundColor("#0b1518");
|
||||
if (area.background === "town") this.add.image(0, 0, "town").setOrigin(0).setDepth(0);
|
||||
else this.add.image(0, 0, "areas", area.frame).setOrigin(0).setDepth(0);
|
||||
|
||||
@@ -67,6 +71,17 @@
|
||||
this.physics.add.existing(block, true);
|
||||
this.collisions.add(block);
|
||||
});
|
||||
this.gateBlocks = [];
|
||||
(area.gates || []).forEach((gate) => {
|
||||
if (controller.getState().sigils.includes(gate.requiresSigil)) return;
|
||||
const block = this.add.rectangle(
|
||||
gate.x + gate.width / 2, gate.y + gate.height / 2,
|
||||
gate.width, gate.height, 0xf4c96b, debug ? 0.2 : 0
|
||||
).setDepth(debug ? 51 : 2);
|
||||
this.physics.add.existing(block, true);
|
||||
this.collisions.add(block);
|
||||
this.gateBlocks.push({ gate, block });
|
||||
});
|
||||
|
||||
this.createAnimations();
|
||||
this.npcSprites = [];
|
||||
@@ -78,7 +93,8 @@
|
||||
this.player = this.physics.add.sprite(safe.x, safe.y, `traveler-${state.palette}`, frameFor(this.lastFacing))
|
||||
.setScale(0.3).setDepth(20);
|
||||
this.player.body.setSize(72, 42).setOffset(92, 190);
|
||||
this.player.setCollideWorldBounds(true).setDrag(900, 900).setMaxVelocity(190, 190);
|
||||
this.player.setCollideWorldBounds(true).setDrag(0, 0).setMaxVelocity(230, 230);
|
||||
this.previousPlayerPosition = { x: this.player.x, y: this.player.y };
|
||||
this.physics.add.collider(this.player, this.collisions);
|
||||
|
||||
this.interactables = [];
|
||||
@@ -98,8 +114,10 @@
|
||||
});
|
||||
|
||||
this.cameras.main.setBounds(0, 0, area.width, area.height);
|
||||
this.cameras.main.setZoom(this.areaId === "town" ? 1 : 1.32);
|
||||
this.cameras.main.startFollow(this.player, true,
|
||||
controller.reducedMotion() ? 1 : 0.14, controller.reducedMotion() ? 1 : 0.14);
|
||||
controller.reducedMotion() ? 1 : 0.24, controller.reducedMotion() ? 1 : 0.24);
|
||||
this.cameras.main.setDeadzone(72, 48);
|
||||
controller.audio.attach(this, this.areaId);
|
||||
controller.renderHud();
|
||||
controller.status(this.areaId === "town"
|
||||
@@ -152,17 +170,17 @@
|
||||
this.interactables.push(npc.getData("interaction"));
|
||||
if (!controller.reducedMotion()) this.tweens.add({ targets: npc, y: y - 2, duration: 1200 + Data.NPCS[id].frame * 30, yoyo: true, repeat: -1 });
|
||||
});
|
||||
this.addChest("town-garden", 115, 550, "memory_fragment", 2);
|
||||
this.addChest("town-wall", 1280, 870, "ink_vial", 2);
|
||||
this.addChest("town-garden", 205, 565, "memory_fragment", 2);
|
||||
this.addChest("town-wall", 1235, 840, "ink_vial", 2);
|
||||
} else {
|
||||
this.addPortal("town", this.area.width / 2, this.area.height - 38, "Return to Archive Town");
|
||||
if (this.areaId === "vault") {
|
||||
this.addPortal("boss", this.area.width / 2, 95, controller.getState().story.ending ? "Visit the restored core" : "Confront The Redactor");
|
||||
this.addPortal("boss", this.area.width / 2, 120, controller.getState().story.ending ? "Visit the restored core" : "Confront The Redactor");
|
||||
this.addChest("vault-lore", 100, 420, "lore_page", 2);
|
||||
}
|
||||
if (this.areaId === "playroom") {
|
||||
this.interactables.push({ type: "challenge", id: "playroom", x: this.area.width / 2, y: 110 });
|
||||
this.add.circle(this.area.width / 2, 110, 22, 0xf8d879, 0.45).setDepth(5);
|
||||
this.interactables.push({ type: "challenge", id: "playroom", x: this.area.width / 2, y: 255 });
|
||||
this.add.circle(this.area.width / 2, 255, 22, 0xf8d879, 0.45).setDepth(5);
|
||||
}
|
||||
if (this.areaId === "boss") this.createBossPedestals();
|
||||
}
|
||||
@@ -197,19 +215,26 @@
|
||||
createEnemies() {
|
||||
this.enemies = this.physics.add.group();
|
||||
this.physics.add.collider(this.enemies, this.collisions);
|
||||
this.area.enemies.forEach(([type, x, y]) => {
|
||||
this.physics.add.collider(this.enemies, this.enemies);
|
||||
this.area.enemies.forEach((entry) => {
|
||||
const spawn = Array.isArray(entry)
|
||||
? { type: entry[0], x: entry[1], y: entry[2] }
|
||||
: entry;
|
||||
const { type, x, y } = spawn;
|
||||
if ((type === "sentinel" || type === "redactor") && controller.getState().defeatedBosses.includes(type)) return;
|
||||
this.spawnEnemy(type, x, y);
|
||||
this.spawnEnemy(type, x, y, spawn);
|
||||
});
|
||||
}
|
||||
|
||||
spawnEnemy(type, x, y) {
|
||||
spawnEnemy(type, x, y, spawn) {
|
||||
const spec = Data.ENEMIES[type];
|
||||
const enemy = this.physics.add.sprite(x, y, "cast", spec.frame).setScale(spec.boss ? 0.31 : 0.21).setDepth(y + 30);
|
||||
enemy.body.setSize(spec.boss ? 155 : 115, spec.boss ? 85 : 65).setOffset(spec.boss ? 79 : 99, spec.boss ? 196 : 200);
|
||||
enemy.setData({
|
||||
id: `${type}-${++this.enemySerial}`, type, spec, health: spec.health,
|
||||
originX: x, originY: y, nextAction: this.time.now + 900, phase: 1
|
||||
originX: x, originY: y, leash: spawn && spawn.leash || 150,
|
||||
engage: spawn && spawn.engage || 245, requiresSigil: spawn && spawn.requiresSigil || null,
|
||||
questId: spawn && spawn.questId || null, nextAction: this.time.now + 900, phase: 1
|
||||
});
|
||||
this.enemies.add(enemy);
|
||||
return enemy;
|
||||
@@ -218,10 +243,15 @@
|
||||
update(time, delta) {
|
||||
if (!this.player) return;
|
||||
if (controller.locked()) {
|
||||
this.player.setAcceleration(0).setVelocity(0);
|
||||
this.player.setVelocity(0);
|
||||
this.player.anims.stop();
|
||||
return;
|
||||
}
|
||||
const traveled = Math.hypot(
|
||||
this.player.x - this.previousPlayerPosition.x,
|
||||
this.player.y - this.previousPlayerPosition.y
|
||||
);
|
||||
this.previousPlayerPosition = { 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;
|
||||
@@ -230,20 +260,33 @@
|
||||
if (this.cursors.down.isDown || this.keys.down.isDown || controller.move.down) dy += 1;
|
||||
const moving = dx !== 0 || dy !== 0;
|
||||
const state = controller.getState();
|
||||
const acceleration = state.equipment.boots ? 980 : 760;
|
||||
if (moving) {
|
||||
const vector = Systems.normalizedVector(dx, dy, acceleration);
|
||||
this.player.setAcceleration(vector.x, vector.y);
|
||||
this.gateBlocks = this.gateBlocks.filter((entry) => {
|
||||
if (!state.sigils.includes(entry.gate.requiresSigil)) return true;
|
||||
entry.block.destroy();
|
||||
controller.status("The Town Gate opens. Archive Town is ready to be explored.");
|
||||
return false;
|
||||
});
|
||||
const acceptingMovement = time >= this.controlLockedUntil;
|
||||
if (acceptingMovement) {
|
||||
const velocity = Systems.approachVelocity(
|
||||
this.player.body.velocity.x, this.player.body.velocity.y,
|
||||
dx, dy, this.area.movement, delta, state.equipment.boots
|
||||
);
|
||||
this.player.setVelocity(velocity.x, velocity.y);
|
||||
}
|
||||
if (moving && acceptingMovement) {
|
||||
this.lastFacing = Math.abs(dx) > Math.abs(dy) ? (dx < 0 ? "west" : "east") : (dy < 0 ? "north" : "south");
|
||||
this.player.anims.play(`traveler-${state.palette}-walk-${this.lastFacing}`, true);
|
||||
if (state.settings.soundEnabled && time - this.lastStepAt > 350) {
|
||||
this.stepDistance += traveled;
|
||||
if (state.settings.soundEnabled && this.stepDistance >= 44) {
|
||||
controller.audio.play("step", { rate: 0.96 + Math.random() * 0.08 });
|
||||
this.lastStepAt = time;
|
||||
this.stepDistance = 0;
|
||||
}
|
||||
} else {
|
||||
this.player.setAcceleration(0);
|
||||
this.player.anims.stop();
|
||||
this.player.setFrame(frameFor(this.lastFacing));
|
||||
if (!moving) this.stepDistance = 0;
|
||||
}
|
||||
this.player.setDepth(this.player.y + 60);
|
||||
this.npcSprites.forEach((npc) => npc.setFlipX(this.player.x < npc.x));
|
||||
@@ -351,7 +394,7 @@
|
||||
controller.audio.play(type === "redactor" ? "victory" : "defeat");
|
||||
let state = Systems.grantXp(controller.getState(), spec.xp);
|
||||
if (type === "sentinel" || type === "redactor") state = Systems.recordBoss(state, type);
|
||||
if (state.quests.study.status === "active" && this.areaId === "town") {
|
||||
if (enemy.getData("questId") === "study" && state.quests.study.status === "active") {
|
||||
state = Systems.progressQuest(state, "study", 1);
|
||||
if (state.quests.study.count >= Data.QUESTS.study.target) state = Systems.completeQuest(state, "study");
|
||||
}
|
||||
@@ -368,22 +411,40 @@
|
||||
this.enemies.getChildren().forEach((enemy) => {
|
||||
if (!enemy.active) return;
|
||||
const spec = enemy.getData("spec");
|
||||
const required = enemy.getData("requiresSigil");
|
||||
const available = !required || controller.getState().sigils.includes(required);
|
||||
enemy.setVisible(available);
|
||||
enemy.body.enable = available;
|
||||
if (!available) return;
|
||||
const distance = Phaser.Math.Distance.Between(enemy.x, enemy.y, this.player.x, this.player.y);
|
||||
const homeDistance = Phaser.Math.Distance.Between(
|
||||
enemy.x, enemy.y, enemy.getData("originX"), enemy.getData("originY")
|
||||
);
|
||||
const playerInSafeZone = Systems.isCombatSafe(this.areaId, this.player.x, this.player.y);
|
||||
if (playerInSafeZone || homeDistance > enemy.getData("leash")) {
|
||||
if (homeDistance > 8) {
|
||||
this.physics.moveTo(enemy, enemy.getData("originX"), enemy.getData("originY"), spec.speed);
|
||||
} else {
|
||||
enemy.setPosition(enemy.getData("originX"), enemy.getData("originY")).setVelocity(0);
|
||||
}
|
||||
enemy.setDepth(enemy.y + 50);
|
||||
return;
|
||||
}
|
||||
if (spec.behaviour === "chase") {
|
||||
if (distance < 280) this.physics.moveToObject(enemy, this.player, spec.speed);
|
||||
else enemy.setVelocity(0);
|
||||
if (distance < enemy.getData("engage")) this.physics.moveToObject(enemy, this.player, spec.speed);
|
||||
else this.returnEnemyHome(enemy, spec.speed);
|
||||
} else if (spec.behaviour === "wander") {
|
||||
if (time > enemy.getData("nextAction")) {
|
||||
if (distance < enemy.getData("engage") && time > enemy.getData("nextAction")) {
|
||||
enemy.setData("nextAction", time + 650 + Math.random() * 800);
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
enemy.setVelocity(Math.cos(angle) * spec.speed, Math.sin(angle) * spec.speed);
|
||||
}
|
||||
} else if (distance >= enemy.getData("engage")) this.returnEnemyHome(enemy, spec.speed);
|
||||
} else if (spec.behaviour === "ranged") {
|
||||
if (distance < 340 && time > enemy.getData("nextAction")) {
|
||||
if (distance < enemy.getData("engage") && time > enemy.getData("nextAction")) {
|
||||
enemy.setData("nextAction", time + (controller.getState().settings.assist ? 1900 : 1350));
|
||||
this.fireProjectile(enemy, spec.damage);
|
||||
} else if (distance < 170) this.physics.moveToObject(enemy, this.player, -spec.speed);
|
||||
else enemy.setVelocity(0);
|
||||
else this.returnEnemyHome(enemy, spec.speed);
|
||||
} else if (spec.behaviour === "charge") {
|
||||
if (time > enemy.getData("nextAction")) {
|
||||
enemy.setTint(0xffd36e);
|
||||
@@ -403,6 +464,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
returnEnemyHome(enemy, speed) {
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
enemy.x, enemy.y, enemy.getData("originX"), enemy.getData("originY")
|
||||
);
|
||||
if (distance > 8) this.physics.moveTo(enemy, enemy.getData("originX"), enemy.getData("originY"), speed);
|
||||
else enemy.setVelocity(0);
|
||||
}
|
||||
|
||||
updateRedactor(enemy, time, distance) {
|
||||
const health = enemy.getData("health");
|
||||
const phase = health > 20 ? 1 : health > 10 ? 2 : 3;
|
||||
@@ -477,7 +546,7 @@
|
||||
}
|
||||
|
||||
damagePlayer(amount, fromX, fromY) {
|
||||
if (this.areaId === "town" && isTownSafe(this.player.x, this.player.y)) return;
|
||||
if (Systems.isCombatSafe(this.areaId, this.player.x, this.player.y)) return;
|
||||
const result = Systems.takeDamage(controller.getState(), amount, this.time.now, this.lastHitAt);
|
||||
if (!result.hit) return;
|
||||
this.lastHitAt = this.time.now;
|
||||
@@ -485,6 +554,7 @@
|
||||
controller.audio.play("damage");
|
||||
const direction = Systems.normalizedVector(this.player.x - fromX, this.player.y - fromY, 220);
|
||||
this.player.setVelocity(direction.x, direction.y).setTint(0xffb2b2);
|
||||
this.controlLockedUntil = this.time.now + 180;
|
||||
this.time.delayedCall(160, () => { if (this.player.active) this.player.clearTint(); });
|
||||
if (!controller.reducedMotion()) this.cameras.main.shake(90, 0.003);
|
||||
if (result.defeated) {
|
||||
@@ -501,8 +571,8 @@
|
||||
persistPosition(checkpoint) {
|
||||
if (!this.player) return;
|
||||
let state = StateWithSafe(controller.getState(), this.areaId, this.player.x, this.player.y, this.lastFacing);
|
||||
if (checkpoint || this.areaId === "town") {
|
||||
const safe = Systems.nearestSafeSpawn(this.areaId, this.player.x, this.player.y);
|
||||
if (checkpoint || Systems.isCombatSafe(this.areaId, this.player.x, this.player.y)) {
|
||||
const safe = Systems.nearestNamedSafeSpawn(this.areaId, this.player.x, this.player.y);
|
||||
state = root.ArchiveWorldState.withCheckpoint(state, safe.area, safe.spawn, safe.x, safe.y);
|
||||
}
|
||||
controller.setState(state, null, true);
|
||||
@@ -538,9 +608,5 @@
|
||||
return String(value).replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
function isTownSafe(x, y) {
|
||||
return y > 835 || (x > 570 && x < 875 && y > 405 && y < 690);
|
||||
}
|
||||
|
||||
root.ArchiveWorldScenes = Object.freeze({ createSceneClasses });
|
||||
}(typeof globalThis !== "undefined" ? globalThis : this));
|
||||
|
||||
Reference in New Issue
Block a user