(function (root) { "use strict"; const Data = root.ArchiveWorldData; const Systems = root.ArchiveWorldSystems; function createSceneClasses(controller) { class BootScene extends Phaser.Scene { constructor() { super("ArchiveBoot"); } preload() { const base = "/assets/images/play/archive-world/v2/"; this.load.image("town", `${base}archive-town.webp`); this.load.spritesheet("areas", `${base}areas.webp`, { frameWidth: 724, frameHeight: 543 }); ["brass", "moss", "berry"].forEach((palette) => { this.load.spritesheet(`traveler-${palette}`, `${base}traveler-${palette}.png`, { frameWidth: 256, frameHeight: 256 }); }); this.load.spritesheet("cast", `${base}cast-atlas.png`, { frameWidth: 313, frameHeight: 313 }); this.load.spritesheet("items", `${base}item-atlas.png`, { frameWidth: 313, frameHeight: 313 }); const audio = "/assets/audio/archive-world/v2/"; [ ["ambient-v2", "town-ambient.ogg"], ["vault-ambient", "vault-ambient.ogg"], ["step-v2", "step.wav"], ["attack", "attack.wav"], ["damage", "damage.wav"], ["enemy-defeat", "enemy-defeat.wav"], ["item", "item.wav"], ["quest", "quest.wav"], ["dialogue", "dialogue.wav"], ["puzzle", "puzzle.wav"], ["boss-attack", "boss-attack.wav"], ["victory", "victory.wav"], ["portal-v2", "portal.wav"] ].forEach(([key, file]) => this.load.audio(key, `${audio}${file}`)); } create() { this.scene.start("ArchiveWorld", { area: controller.getState().position.area || "town" }); } } class WorldScene extends Phaser.Scene { constructor() { super("ArchiveWorld"); this.areaId = "town"; this.nearest = null; this.lastFacing = "north"; this.lastStepAt = 0; this.lastAttackAt = 0; this.lastHitAt = 0; this.enemySerial = 0; this.bossPuzzle = []; this.bossPuzzleSolved = false; this.controlLockedUntil = 0; this.stepDistance = 0; this.previousPlayerPosition = null; } init(data) { this.areaId = Data.AREAS[data && data.area] ? data.area : "town"; } create() { controller.scene = this; 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); this.collisions = this.physics.add.staticGroup(); const debug = new URLSearchParams(window.location.search).get("collisionDebug") === "1" && ["localhost", "127.0.0.1"].includes(window.location.hostname); area.collisions.forEach((item) => { const block = this.add.rectangle(item.x + item.width / 2, item.y + item.height / 2, item.width, item.height, 0xff3b5c, debug ? 0.25 : 0) .setDepth(debug ? 50 : item.depth || 2); 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.npcCollisions = this.physics.add.staticGroup(); const state = controller.getState(); const safe = Systems.nearestSafeSpawn(this.areaId, state.position.area === this.areaId ? state.position.x : area.spawn.x, state.position.area === this.areaId ? state.position.y : area.spawn.y); this.lastFacing = state.position.facing || "north"; 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(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 = []; this.markers = []; this.createWorldObjects(); this.physics.add.collider(this.player, this.npcCollisions); this.createEnemies(); this.createInput(); this.projectiles = this.physics.add.group(); this.physics.add.collider(this.projectiles, this.collisions, (projectile) => projectile.destroy()); this.physics.add.overlap(this.player, this.projectiles, (_player, projectile) => { projectile.destroy(); this.damagePlayer(projectile.getData("damage") || 10, projectile.x, projectile.y); }); this.physics.add.overlap(this.player, this.enemies, (_player, enemy) => { this.damagePlayer(enemy.getData("spec").damage, enemy.x, enemy.y); }); 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.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" ? "Archive Town is listening. Speak with Gatekeeper Orin or explore any landmark." : `${pretty(this.areaId)} entered. The doorway behind you returns to Archive Town.`); this.persistPosition(true); } createAnimations() { const palette = controller.getState().palette; const texture = `traveler-${palette}`; ["south", "west", "east", "north"].forEach((face, row) => { const walkKey = `${texture}-walk-${face}`; if (!this.anims.exists(walkKey)) { this.anims.create({ key: walkKey, frames: this.anims.generateFrameNumbers(texture, { start: row * 6, end: row * 6 + 5 }), frameRate: controller.reducedMotion() ? 6 : 9, repeat: -1 }); } }); } createInput() { this.cursors = this.input.keyboard.createCursorKeys(); this.keys = this.input.keyboard.addKeys({ up: Phaser.Input.Keyboard.KeyCodes.W, down: Phaser.Input.Keyboard.KeyCodes.S, left: Phaser.Input.Keyboard.KeyCodes.A, right: Phaser.Input.Keyboard.KeyCodes.D, interact: Phaser.Input.Keyboard.KeyCodes.E, enter: Phaser.Input.Keyboard.KeyCodes.ENTER, use: Phaser.Input.Keyboard.KeyCodes.Q, menu: Phaser.Input.Keyboard.KeyCodes.ESC, cycle: Phaser.Input.Keyboard.KeyCodes.TAB }); } createWorldObjects() { if (this.areaId === "town") { Object.values(Data.LANDMARKS).forEach((place) => { const marker = this.add.circle(place.x, place.y, 24, 0xf1c46f, 0.18) .setStrokeStyle(3, 0xffedaa, 0.9).setDepth(6); marker.setData("interaction", { type: "landmark", id: place.id, x: place.x, y: place.y }); this.markers.push(marker); this.interactables.push(marker.getData("interaction")); if (!controller.reducedMotion()) this.tweens.add({ targets: marker, alpha: 0.45, scale: 1.14, yoyo: true, repeat: -1, duration: 1100 }); }); this.area.npcs.forEach(([id, x, y]) => { const npc = this.npcCollisions.create(x, y, "cast", Data.NPCS[id].frame) .setScale(0.23).setDepth(y + 60); npc.refreshBody(); npc.body.setSize(82, 44).setOffset(115, 214); npc.setData("interaction", { type: "npc", id, x, y }); this.interactables.push(npc.getData("interaction")); }); 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, 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: 255 }); this.add.circle(this.area.width / 2, 255, 22, 0xf8d879, 0.45).setDepth(5); } if (this.areaId === "boss") this.createBossPedestals(); } } addPortal(area, x, y, label) { const portal = this.add.circle(x, y, 28, 0x87d4c0, 0.24).setStrokeStyle(3, 0xbff8e9, 0.8).setDepth(5); const interaction = { type: "portal", id: area, x, y, label }; portal.setData("interaction", interaction); this.interactables.push(interaction); } addChest(id, x, y, item, quantity) { if (controller.getState().openedChests.includes(id)) return; const chest = this.add.sprite(x, y, "items", 11).setScale(0.2).setDepth(y + 10); const interaction = { type: "chest", id, x, y, item, quantity }; chest.setData("interaction", interaction); chest.setData("sprite", chest); this.interactables.push(interaction); } createBossPedestals() { const labels = ["self", "neighbour", "town", "archive"]; const positions = [[220, 180], [505, 180], [505, 350], [220, 350]]; labels.forEach((value, index) => { const [x, y] = positions[index]; const pedestal = this.add.circle(x, y, 20, 0x34506b, 0.7).setStrokeStyle(3, 0xe3c16f).setDepth(4); this.interactables.push({ type: "boss-memory", id: value, x, y, sprite: pedestal }); }); } createEnemies() { this.enemies = this.physics.add.group(); this.physics.add.collider(this.enemies, this.collisions); this.physics.add.collider(this.enemies, this.npcCollisions); 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, spawn); }); } 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, 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; } update(time, delta) { if (!this.player) return; if (controller.locked()) { 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; 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; const moving = dx !== 0 || dy !== 0; const state = controller.getState(); 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); 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.anims.stop(); this.player.setFrame(frameFor(this.lastFacing)); if (!moving) this.stepDistance = 0; } this.player.setDepth(this.player.y + 60); this.updateEnemies(time, delta); this.updateNearest(); this.handleKeys(time); if (moving && time - controller.lastSaveAt > 700) { controller.lastSaveAt = time; this.persistPosition(false); } } handleKeys(time) { 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.use)) controller.useSelectedItem(); if (Phaser.Input.Keyboard.JustDown(this.keys.cycle)) { this.keys.cycle.reset(); controller.cycleItem(); } if (Phaser.Input.Keyboard.JustDown(this.keys.menu)) controller.openMenu(); } updateNearest() { this.nearest = Systems.nearestInteraction(this.interactables, this.player.x, this.player.y); controller.setPrompt(this.nearest ? promptFor(this.nearest) : ""); } interact() { if (controller.locked()) return; const item = this.nearest; if (!item) return controller.status("Nothing nearby answers the Living Bookmark."); if (item.type === "landmark") controller.openLandmark(item.id); else if (item.type === "npc") controller.openNpc(item.id); else if (item.type === "portal") { if (item.id === "boss" && controller.getState().sigils.length < 8) { controller.status("All eight Archive Sigils are needed to reveal the vault core."); } else this.travel(item.id); } else if (item.type === "challenge") controller.openChallenge(item.id); else if (item.type === "chest") this.openChest(item); else if (item.type === "boss-memory") this.activateBossMemory(item); } openChest(item) { let state = controller.getState(); if (state.openedChests.includes(item.id)) return; state.openedChests.push(item.id); state = Systems.addItem(state, item.item, item.quantity); controller.setState(state, `${Data.ITEMS[item.item].name} collected.`); controller.audio.play("item"); this.interactables = this.interactables.filter((entry) => entry !== item); const sprite = this.children.list.find((child) => child.getData && child.getData("interaction") === item); if (sprite) sprite.destroy(); } travel(area) { controller.audio.play("portal"); this.persistPosition(false); controller.travel(area); } attack(time) { if (time - this.lastAttackAt < 330 || controller.locked()) return; this.lastAttackAt = time; controller.audio.play("attack"); const direction = faceVector(this.lastFacing); const range = controller.getState().attack > 1 ? 74 : 62; const hit = this.add.rectangle(this.player.x + direction.x * 48, this.player.y + direction.y * 38, range, range, 0xf8df83, 0.32).setDepth(100); this.physics.add.existing(hit); const struck = new Set(); this.physics.overlap(hit, this.enemies, (_zone, enemy) => { if (struck.has(enemy)) return; struck.add(enemy); this.hitEnemy(enemy, controller.getState().attack, direction); }); this.time.delayedCall(controller.reducedMotion() ? 80 : 130, () => hit.destroy()); } hitEnemy(enemy, damage, direction) { if (!enemy.active) return; if (enemy.getData("type") === "redactor" && enemy.getData("phase") >= 3 && !this.bossPuzzleSolved) { controller.status("The Redactor's weak point is hidden. Restore the four memory pedestals."); return; } enemy.setData("health", enemy.getData("health") - damage); enemy.setVelocity(direction.x * 170, direction.y * 170); enemy.setTint(0xffd6d6); this.time.delayedCall(90, () => { if (enemy.active) enemy.clearTint(); }); if (enemy.getData("health") <= 0) this.defeatEnemy(enemy); this.updateBossHud(enemy); } defeatEnemy(enemy) { const type = enemy.getData("type"); const spec = enemy.getData("spec"); const x = enemy.x; const y = enemy.y; enemy.destroy(); 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 (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"); } if (Math.random() < 0.55 && !spec.boss) state = Systems.addItem(state, "ink_vial", 1); controller.setState(state, `${spec.name} restored into harmless ink. +${spec.xp} Memory.`); this.add.circle(x, y, 10, 0xbde4d6, 0.7).setDepth(30); if (type === "redactor") { controller.hideBoss(); this.time.delayedCall(500, () => controller.openEnding()); } } updateEnemies(time) { 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 < enemy.getData("engage")) this.physics.moveToObject(enemy, this.player, spec.speed); else this.returnEnemyHome(enemy, spec.speed); } else if (spec.behaviour === "wander") { 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 < 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 this.returnEnemyHome(enemy, spec.speed); } else if (spec.behaviour === "charge") { if (time > enemy.getData("nextAction")) { enemy.setTint(0xffd36e); enemy.setVelocity(0); enemy.setData("nextAction", time + 1800); this.time.delayedCall(controller.getState().settings.assist ? 850 : 550, () => { if (!enemy.active) return; enemy.clearTint(); this.physics.moveToObject(enemy, this.player, spec.speed * 2.1); controller.audio.play("boss"); }); } } else if (spec.behaviour === "redactor") { this.updateRedactor(enemy, time, distance); } enemy.setDepth(enemy.y + 50); }); } 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; if (phase !== enemy.getData("phase")) { enemy.setData("phase", phase); if (phase === 2) { controller.status("The Redactor tears open lost footnotes. Keep to the clear floor."); this.spawnEnemy("ink_blot", 150, 250); this.spawnEnemy("lost_footnote", 575, 250); } else { controller.status("Final phase: activate Self, Neighbour, Town, then Archive to reveal the weak point."); enemy.setFrame(15).setTint(0x9e7cc1); } } if (time > enemy.getData("nextAction")) { enemy.setData("nextAction", time + (controller.getState().settings.assist ? 2100 : 1450)); if (phase === 1) this.fireProjectile(enemy, 14); else if (phase === 2) { this.fireRadial(enemy, 8, 12); controller.audio.play("boss"); } else if (!this.bossPuzzleSolved) { this.fireRadial(enemy, 5, 10); } else if (distance < 320) this.physics.moveToObject(enemy, this.player, 65); } } fireProjectile(enemy, damage) { enemy.setTint(0xead58a); this.time.delayedCall(controller.getState().settings.assist ? 650 : 380, () => { if (!enemy.active) return; enemy.clearTint(); const shot = this.add.circle(enemy.x, enemy.y, 9, 0x16121d, 0.95).setStrokeStyle(2, 0xf0bfd0).setDepth(40); this.physics.add.existing(shot); shot.setData("damage", damage); this.projectiles.add(shot); this.physics.moveToObject(shot, this.player, 175); this.time.delayedCall(3600, () => { if (shot.active) shot.destroy(); }); controller.audio.play("boss", { volume: 0.6 }); }); } fireRadial(enemy, count, damage) { for (let index = 0; index < count; index += 1) { const angle = (Math.PI * 2 * index) / count; const shot = this.add.circle(enemy.x, enemy.y, 8, 0x151018, 0.95).setStrokeStyle(2, 0xd991aa).setDepth(40); this.physics.add.existing(shot); shot.setData("damage", damage); shot.body.setVelocity(Math.cos(angle) * 135, Math.sin(angle) * 135); this.projectiles.add(shot); this.time.delayedCall(4000, () => { if (shot.active) shot.destroy(); }); } } activateBossMemory(item) { if (this.bossPuzzleSolved) return; const order = ["self", "neighbour", "town", "archive"]; this.bossPuzzle.push(item.id); const valid = this.bossPuzzle.every((value, index) => value === order[index]); if (!valid) { this.bossPuzzle = []; controller.status("The memory chain breaks, but the sigils hold. Begin with Self."); return; } item.sprite.setFillStyle(0xf3d67d, 0.9); if (this.bossPuzzle.length === order.length) { this.bossPuzzleSolved = true; controller.status("Context restored: The Redactor's weak point is revealed."); controller.audio.play("puzzle"); const boss = this.enemies.getChildren().find((enemy) => enemy.getData("type") === "redactor"); if (boss) boss.clearTint().setFrame(15); } else controller.status(`${pretty(item.id)} restored. ${this.bossPuzzle.length} / 4 memories connected.`); } damagePlayer(amount, fromX, fromY) { 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; controller.setState(result.state, `The ink struck for ${Math.round(amount * (result.state.settings.assist ? 0.5 : 1))} damage.`); 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) { this.player.setVelocity(0); controller.openDefeat(); } } updateBossHud(enemy) { if (!enemy || !enemy.getData("spec").boss) return; controller.showBoss(enemy.getData("spec").name, enemy.getData("health"), enemy.getData("spec").health); } persistPosition(checkpoint) { if (!this.player) return; let state = StateWithSafe(controller.getState(), this.areaId, this.player.x, this.player.y, this.lastFacing); 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); } } return [BootScene, WorldScene]; } function StateWithSafe(state, area, x, y, facing) { const safe = Systems.nearestSafeSpawn(area, x, y); return root.ArchiveWorldState.withPosition(state, safe.area, safe.x, safe.y, facing, safe.spawn); } function frameFor(face) { return { south: 0, west: 6, east: 12, north: 18 }[face] || 18; } function faceVector(face) { return { north: { x: 0, y: -1 }, south: { x: 0, y: 1 }, west: { x: -1, y: 0 }, east: { x: 1, y: 0 } }[face]; } function promptFor(item) { if (item.type === "portal") return `${item.label}. Press E or Explore.`; if (item.type === "chest") return "A sealed archive chest is nearby. Press E or Explore."; if (item.type === "boss-memory") return `${pretty(item.id)} pedestal. Press E or Explore.`; if (item.type === "challenge") return "The lantern console is nearby. Press E or Explore."; const title = item.type === "npc" ? Data.NPCS[item.id].name : Data.LANDMARKS[item.id].title; return `${title} is nearby. Press E or Explore.`; } function pretty(value) { return String(value).replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); } root.ArchiveWorldScenes = Object.freeze({ createSceneClasses }); }(typeof globalThis !== "undefined" ? globalThis : this));