(function () { "use strict"; const Data = window.ArchiveWorldData; const State = window.ArchiveWorldState; const Systems = window.ArchiveWorldSystems; const UI = window.ArchiveWorldUI; const Scenes = window.ArchiveWorldScenes; const Audio = window.ArchiveWorldAudio; const controller = { root: null, state: null, game: null, scene: null, ui: null, audio: null, lock: false, move: { up: false, down: false, left: false, right: false }, prompt: "", lastSaveAt: 0, systemReducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches, getState: () => controller.state, locked: () => controller.lock, reducedMotion: () => controller.state && controller.state.settings.reducedMotion !== null ? controller.state.settings.reducedMotion : controller.systemReducedMotion }; document.addEventListener("DOMContentLoaded", init); function init() { const root = document.querySelector('.archive-world[data-play-page="archive-world"]'); if (!root || !Data || !State || !Systems || !UI || !Scenes || !Audio) return; controller.root = root; controller.audio = Audio.create(() => controller.state); bindSetup(); bindControls(); bindSettings(); controller.ui = UI.create(root, { getState: () => controller.state, onLock: (locked) => { controller.lock = locked; }, onStartQuest: startQuest, onCompleteQuest: completeQuest, onTravel: travel, onUseItem: useItem, onChoice: completeChoice, onRespawn: respawn, onFullscreen: toggleFullscreen, onReset: resetAll }); const loaded = loadState(); if (loaded.state) { controller.state = developmentAreaOverride(loaded.state); renderHud(); startGame(); if (loaded.message) status(loaded.message); } else { openSetup(loaded.message); } } function developmentAreaOverride(state) { const local = ["localhost", "127.0.0.1"].includes(window.location.hostname); const params = new URLSearchParams(window.location.search); const areaId = params.get("collisionDebug") === "1" ? params.get("area") : null; if (!local || !Data.AREAS[areaId]) return state; const spawn = Data.AREAS[areaId].spawn; return State.withPosition(state, areaId, spawn.x, spawn.y, "north", "entrance"); } function loadState() { try { const rawV2 = localStorage.getItem(State.STORAGE_KEY); if (rawV2) { const parsed = State.parse(rawV2); return parsed ? { state: ensureSafePosition(parsed), message: null } : { state: null, message: "The saved Archive World data was malformed, so a clean traveler setup is ready." }; } const rawV1 = localStorage.getItem(State.LEGACY_KEY); if (rawV1) { const migrated = State.parse(rawV1); if (migrated) { persistState(migrated); return { state: ensureSafePosition(migrated), message: "Your original Archive World traveler was safely migrated to the new adventure." }; } return { state: null, message: "The older save could not be restored, so a clean traveler setup is ready." }; } } catch (_error) { return { state: null, message: "Local saving is unavailable. You can still play during this visit." }; } return { state: null, message: null }; } function ensureSafePosition(state) { if (state.health <= 0) state = Systems.respawn(state); const safe = Systems.nearestSafeSpawn(state.position.area, state.position.x, state.position.y); return State.withPosition(state, safe.area, safe.x, safe.y, state.position.facing, safe.spawn); } function bindSetup() { const dialog = controller.root.querySelector("[data-world-setup]"); const form = controller.root.querySelector("[data-world-setup-form]"); if (!dialog || !form) return; dialog.addEventListener("cancel", (event) => event.preventDefault()); form.addEventListener("submit", (event) => { event.preventDefault(); const formData = new FormData(form); const name = State.validateName(formData.get("name")); const palette = State.validatePalette(formData.get("palette")); const error = controller.root.querySelector("[data-world-setup-error]"); if (!name || !palette) { error.textContent = "Choose a palette and enter a name between 1 and 20 characters."; return; } controller.state = State.fresh(name, palette); persistState(controller.state); dialog.close(); renderHud(); startGame(); }); } function openSetup(message) { const dialog = controller.root.querySelector("[data-world-setup]"); if (!dialog) { status(message || "Traveler setup could not open. Reload the page to try again; the plain directory remains available."); return; } if (message) { controller.root.querySelector("[data-world-setup-error]").textContent = message; status(message); } window.setTimeout(() => { dialog.showModal(); dialog.querySelector("input[name=name]").focus(); }, 0); } function bindControls() { controller.root.querySelectorAll("[data-world-move]").forEach((button) => { const direction = button.dataset.worldMove; const down = (event) => { event.preventDefault(); controller.move[direction] = true; controller.root.dataset.touching = "true"; }; const up = (event) => { event.preventDefault(); controller.move[direction] = false; controller.root.dataset.touching = "false"; }; button.addEventListener("pointerdown", down); ["pointerup", "pointercancel", "pointerleave"].forEach((name) => button.addEventListener(name, up)); }); bindClick("[data-world-interact]", () => { if (controller.scene) controller.scene.interact(); }); bindClick("[data-world-attack]", () => { if (controller.scene) controller.scene.attack(controller.scene.time.now); }); bindClick("[data-world-item]", useSelectedItem); bindClick("[data-world-menu]", openMenu); bindClick("[data-world-quests]", () => controller.ui.openMenu(controller.state, "quests")); bindClick("[data-world-inventory]", () => controller.ui.openMenu(controller.state, "inventory")); bindClick("[data-world-journal-open]", () => controller.ui.openMenu(controller.state, "journal")); bindClick("[data-world-sound]", toggleSound); bindClick("[data-world-fullscreen]", toggleFullscreen); bindClick("[data-world-reset]", () => controller.ui.openMenu(controller.state, "settings")); document.addEventListener("fullscreenchange", updateFullscreen); document.addEventListener("visibilitychange", () => { if (document.hidden && controller.scene) controller.scene.persistPosition(false); }); window.addEventListener("pagehide", () => { if (controller.scene) controller.scene.persistPosition(false); }); if (!document.fullscreenEnabled) controller.root.querySelectorAll("[data-world-fullscreen]").forEach((button) => { button.hidden = true; }); } function bindSettings() { controller.root.addEventListener("change", (event) => { if (!controller.state) return; if (event.target.matches("[data-setting='assist']")) { controller.state.settings.assist = event.target.checked; setState(controller.state, event.target.checked ? "Assist mode enabled." : "Assist mode disabled."); } else if (event.target.matches("[data-volume]")) { controller.state.settings[event.target.dataset.volume] = Number(event.target.value); setState(controller.state, null, true); controller.audio.apply(); } }); } function startGame() { if (!window.Phaser) { status("The world engine could not load. Every destination remains available in the plain directory."); return; } const sceneClasses = Scenes.createSceneClasses(controller); controller.game = new Phaser.Game({ type: Phaser.CANVAS, parent: "archive-world-game", width: 960, height: 640, backgroundColor: "#101b18", pixelArt: true, roundPixels: true, physics: { default: "arcade", arcade: { debug: false } }, scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, scene: sceneClasses, input: { keyboard: true, mouse: true, touch: true }, render: { antialias: false, pixelArt: true, roundPixels: true } }); controller.root.dataset.gameReady = "true"; controller.root.querySelector("#archive-world-game").focus(); window.archiveWorldGame = controller.game; } function startQuest(id) { setState(Systems.startQuest(controller.state, id), `${Data.QUESTS[id].title} added to the quest log.`); controller.audio.play("quest"); } function completeQuest(id) { let next = Systems.completeQuest(controller.state, id); if (id === "museum" && next.sigils.length >= 4) { next.story.midpointSeen = true; next = Systems.updateStory(next); status("Midpoint revelation: the Redactor was built to preserve the archive, but learned to mistake emptiness for safety."); } setState(next, `${Data.QUESTS[id].title} complete. The ${Data.LANDMARKS[Data.QUESTS[id].landmark].title} Sigil is restored.`); controller.audio.play("quest"); } function completeChoice(choice) { let next = Systems.startQuest(controller.state, "lost_letter"); next.npcFlags.nellChoice = choice; next = Systems.completeQuest(next, "lost_letter"); setState(next, "Nell remembers that choosing what to preserve is part of the story."); controller.audio.play("dialogue"); } function openLandmark(id) { const wasNew = !controller.state.discovered.includes(id); setState(Systems.discover(controller.state, id), wasNew ? `${Data.LANDMARKS[id].title} added to the discovery journal.` : null); if (wasNew) controller.audio.play("quest", { volume: 0.7 }); controller.ui.openLandmark(id, controller.state); } function openNpc(id) { controller.audio.play("dialogue", { volume: 0.45 }); controller.ui.openNpc(id, controller.state); } function openChallenge(id) { if (controller.state.quests[id].status === "locked") controller.state = Systems.startQuest(controller.state, id); controller.ui.openChallenge(id, controller.state); } function openMenu() { if (!controller.state || controller.ui.isOpen()) return; controller.ui.openMenu(controller.state, "quests"); } function travel(area) { if (!Data.AREAS[area]) return; const spawn = Data.AREAS[area].spawn; controller.state = State.withPosition(controller.state, area, spawn.x, spawn.y, "north", "entrance"); controller.state = State.withCheckpoint(controller.state, area, "entrance", spawn.x, spawn.y); persistState(controller.state); if (controller.scene) controller.scene.scene.restart({ area }); } function useSelectedItem() { useItem(controller.state.selectedItem); } function useItem(id) { const result = Systems.consumeItem(controller.state, id); if (!result.used) { status(controller.state.health >= controller.state.maxHealth ? "Health is already full." : "That item cannot be used now."); return; } setState(result.state, `${Data.ITEMS[id].name} restored ${result.amount} health.`); controller.audio.play("item"); } function cycleItem() { const consumables = controller.state.inventory.filter((entry) => Data.ITEMS[entry.id].type === "consumable"); if (!consumables.length) return status("No usable items are in the inventory."); const index = consumables.findIndex((entry) => entry.id === controller.state.selectedItem); controller.state.selectedItem = consumables[(index + 1) % consumables.length].id; setState(controller.state, `${Data.ITEMS[controller.state.selectedItem].name} selected.`); } function respawn() { const next = Systems.respawn(controller.state); controller.state = next; persistState(next); travel(next.position.area); status("The Living Bookmark returns you to a safe place. Story and items were preserved."); } function toggleSound() { if (!controller.state) return; controller.state.settings.soundEnabled = !controller.state.settings.soundEnabled; if (controller.state.settings.soundEnabled) controller.audio.unlock(); controller.audio.apply(); setState(controller.state, controller.state.settings.soundEnabled ? "Archive World audio enabled." : "Audio muted."); } async function toggleFullscreen() { if (!document.fullscreenEnabled) return; try { if (document.fullscreenElement) await document.exitFullscreen(); else await controller.root.querySelector("[data-world-shell]").requestFullscreen(); } catch (_error) { status("Fullscreen is unavailable in this browser."); } } function updateFullscreen() { const active = Boolean(document.fullscreenElement); controller.root.querySelectorAll("[data-world-fullscreen]").forEach((button) => { button.textContent = active ? "Exit fullscreen" : "Fullscreen"; button.setAttribute("aria-pressed", String(active)); }); if (controller.game) controller.game.scale.refresh(); if (!active) controller.root.querySelector("#archive-world-game").focus(); } function resetAll() { try { localStorage.removeItem(State.STORAGE_KEY); localStorage.removeItem(State.LEGACY_KEY); } catch (_error) { // Reload still provides a fresh in-memory state. } window.location.reload(); } function setState(next, message, quiet) { controller.state = State.normalize(next) || controller.state; if (!quiet) renderHud(); persistState(controller.state); if (message) status(message); } function persistState(state) { try { localStorage.setItem(State.STORAGE_KEY, JSON.stringify(State.normalize(state))); } catch (_error) { status("Progress could not be saved on this device."); } } function renderHud() { if (!controller.state) return; const state = controller.state; text("[data-world-player]", state.name); text("[data-world-level]", `Lv ${state.level}`); text("[data-world-health-text]", `${Math.ceil(state.health)} / ${state.maxHealth}`); const healthBar = controller.root.querySelector("[data-world-health-bar]"); if (healthBar) healthBar.style.width = `${(state.health / state.maxHealth) * 100}%`; text("[data-world-objective]", Systems.currentObjective(state)); text("[data-world-discovery-count]", `${state.discovered.length} / 8`); text("[data-world-sigil-count]", `${state.sigils.length} / 8`); text("[data-world-sound]", state.settings.soundEnabled ? "Sound: on" : "Sound: muted"); const selected = Data.ITEMS[state.selectedItem]; text("[data-world-selected-item]", selected ? `${selected.name} ×${Systems.itemQuantity(state, state.selectedItem)}` : "None"); const badge = controller.root.querySelector("[data-world-badge]"); if (badge) badge.hidden = !state.complete; controller.root.classList.toggle("is-restored", state.story.ending); } function setPrompt(message) { if (message === controller.prompt) return; controller.prompt = message; text("[data-world-prompt]", message || "Follow the paths, signs, and warm window light."); } function status(message) { text("[data-world-status]", message); } function showBoss(name, health, maximum) { const boss = controller.root.querySelector("[data-world-boss]"); boss.hidden = false; text("[data-world-boss-name]", name); boss.querySelector("[data-world-boss-bar]").style.width = `${Math.max(0, health / maximum) * 100}%`; } function hideBoss() { controller.root.querySelector("[data-world-boss]").hidden = true; } function openDefeat() { controller.lock = true; controller.ui.openDefeat(); } function openEnding() { controller.ui.openEnding(controller.state); } function text(selector, value) { const node = controller.root.querySelector(selector); if (node) node.textContent = value; } function bindClick(selector, handler) { const node = controller.root.querySelector(selector); if (node) node.addEventListener("click", handler); } Object.assign(controller, { setState, renderHud, status, setPrompt, showBoss, hideBoss, openDefeat, openEnding, openLandmark, openNpc, openChallenge, openMenu, travel, useSelectedItem, cycleItem }); }());