(function () { "use strict"; const State = window.ArchiveWorldState; const LANDMARKS = Object.freeze({ gate: landmark("Town Gate", "Arrivals", 640, 830, "The old gate remembers every arrival. Begin at the dashboard or follow the newest paths.", [["Home", "/"], ["Recently updated", "/recently-updated.html"], ["Contact", "/home/contact.html"]]), library: landmark("Grand Library", "Knowledge", 260, 285, "Long-lived explanations, categories, and learning notes fill the blue-roofed library.", [["Posts", "/posts/posts-list.html"], ["Categories", "/home/categories.html"], ["Posts introduction", "/posts/posts-intro.html"]]), study: landmark("Guild Study", "Work", 640, 250, "Engineering notes, professional lessons, and active competencies cover every desk.", [["Career", "/posts/career/career-list.html"], ["Competency status", "/home/status.html"], ["Probation objectives", "/posts/career/probation-objectives.html"]]), inn: landmark("Kitchen Inn", "Daily life", 1005, 270, "Weekly reviews and ordinary days stay warm beside the inn's oven.", [["Blogs", "/blogs/blogs-list.html"], ["Weekly reviews", "/tags/review.html"], ["Blogs introduction", "/blogs/blogs-intro.html"]]), workshop: landmark("Workshop", "Living systems", 1030, 535, "Trackers, services, plans, and practical machinery keep the town moving.", [["Services", "/home/services.html"], ["Wird tracker", "/home/wird-tracker.html"], ["Countdowns", "/home/countdown.html"], ["Backlog", "/home/backlog.html"]]), playroom: landmark("Playroom", "Experiments", 1005, 765, "Games, generators, and stranger little mechanisms glow behind the bright windows.", [["Play hub", "/play/play.html"], ["The Rain Index", "/play/the-rain-index.html"], ["House of Pages", "/play/house.html"]]), museum: landmark("Lima Museum", "Keepsakes", 275, 765, "Personal fragments, older writing, and small memories rest beneath the glass roof.", [["Lima archive", "/lima/index.html"], ["Older writing", "/blogs/2025/2025-list.html"], ["Memory Cabinet", "/play/memory.html"]]), garden: landmark("Notes Garden", "Connections", 220, 515, "Loose notes, paths between topics, and recently tended pages grow beyond the trellis.", [["Notes wall", "/home/notes.html"], ["Categories", "/home/categories.html"], ["Recently updated", "/recently-updated.html"], ["Sitemap", "/sitemap.html"]]) }); const PALETTE_TINTS = Object.freeze({ brass: 0xf1c46f, moss: 0x91c788, berry: 0xd894ad }); const controller = { state: null, scene: null, lock: false, move: { up: false, down: false, left: false, right: false }, reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches, saveTimer: 0 }; function landmark(title, eyebrow, x, y, description, links) { return Object.freeze({ title, eyebrow, x, y, description, links: Object.freeze(links) }); } document.addEventListener("DOMContentLoaded", init); function init() { const root = document.querySelector('.archive-world[data-play-page="archive-world"]'); if (!root || !State) return; const stored = State.parse(localStorage.getItem(State.STORAGE_KEY)); controller.state = stored; bindInterface(root); if (!window.Phaser) { setStatus(root, "The world engine could not load. Every destination remains available in the plain directory."); return; } if (stored) { startGame(root); } else { openSetup(root); } } function bindInterface(root) { const setup = root.querySelector("[data-world-setup]"); const setupForm = root.querySelector("[data-world-setup-form]"); const preview = root.querySelector("[data-world-preview]"); const journal = root.querySelector("[data-world-journal]"); setup.addEventListener("cancel", (event) => event.preventDefault()); setupForm.addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(setupForm); const name = State.validateName(data.get("name")); const palette = State.validatePalette(data.get("palette")); const error = 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); persist(); setup.close(); startGame(root); }); root.querySelector("[data-world-preview-close]").addEventListener("click", () => preview.close()); preview.addEventListener("close", () => { controller.lock = false; root.querySelector("#archive-world-game").focus(); }); root.querySelector("[data-world-journal-open]").addEventListener("click", () => { renderJournal(root); controller.lock = true; journal.showModal(); }); root.querySelector("[data-world-journal-close]").addEventListener("click", () => journal.close()); journal.addEventListener("close", () => { controller.lock = false; root.querySelector("[data-world-journal-open]").focus(); }); root.querySelector("[data-world-sound]").addEventListener("click", () => toggleSound(root)); root.querySelector("[data-world-reset]").addEventListener("click", () => { if (!window.confirm("Reset your traveler, discoveries, and saved position?")) return; localStorage.removeItem(State.STORAGE_KEY); window.location.reload(); }); root.querySelectorAll("[data-world-move]").forEach((button) => { const direction = button.dataset.worldMove; const down = (event) => { event.preventDefault(); controller.move[direction] = true; }; const up = (event) => { event.preventDefault(); controller.move[direction] = false; }; button.addEventListener("pointerdown", down); button.addEventListener("pointerup", up); button.addEventListener("pointercancel", up); button.addEventListener("pointerleave", up); }); root.querySelector("[data-world-interact]").addEventListener("click", () => { if (controller.scene) controller.scene.interact(); }); window.addEventListener("pagehide", savePosition); document.addEventListener("visibilitychange", () => { if (document.hidden) savePosition(); }); } function openSetup(root) { const dialog = root.querySelector("[data-world-setup]"); window.setTimeout(() => { dialog.showModal(); dialog.querySelector("input[name=name]").focus(); }, 0); } function startGame(root) { renderHud(root); const game = new Phaser.Game({ type: Phaser.AUTO, parent: "archive-world-game", width: 960, height: 640, backgroundColor: "#17251e", pixelArt: true, roundPixels: true, physics: { default: "arcade", arcade: { debug: false } }, scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, scene: ArchiveTownScene, input: { keyboard: true, mouse: true, touch: true } }); root.dataset.gameReady = "true"; root.querySelector("#archive-world-game").focus(); window.archiveWorldGame = game; } class ArchiveTownScene extends Phaser.Scene { constructor() { super("ArchiveTown"); this.nearest = null; this.lastStepAt = 0; this.wasMoving = false; } preload() { this.load.image("archive-town", "/assets/images/play/archive-world/archive-town.webp"); this.load.image("traveler", "/assets/images/play/archive-world/traveler.png"); this.load.audio("ambient", "/assets/audio/archive-world/ambient.ogg"); this.load.audio("step", "/assets/audio/archive-world/step.wav"); this.load.audio("discover", "/assets/audio/archive-world/discover.wav"); this.load.audio("portal", "/assets/audio/archive-world/portal.wav"); } create() { controller.scene = this; this.add.image(0, 0, "archive-town").setOrigin(0).setDepth(0); this.physics.world.setBounds(0, 0, 1280, 960); this.markers = {}; Object.entries(LANDMARKS).forEach(([id, item]) => { const marker = this.add.circle(item.x, item.y, 22, 0xf4d98a, 0.16) .setStrokeStyle(3, 0xf8e6a7, 0.9) .setDepth(2); marker.setData("landmarkId", id); this.markers[id] = marker; if (!controller.reducedMotion) { this.tweens.add({ targets: marker, scale: 1.18, alpha: 0.58, duration: 900, yoyo: true, repeat: -1, delay: Object.keys(LANDMARKS).indexOf(id) * 90 }); } }); this.player = this.physics.add.sprite( controller.state.position.x, controller.state.position.y, "traveler" ).setDepth(4).setTint(PALETTE_TINTS[controller.state.palette]); this.player.setCollideWorldBounds(true); this.player.body.setSize(28, 34).setOffset(8, 58); this.cameras.main.setBounds(0, 0, 1280, 960); this.cameras.main.startFollow(this.player, true, controller.reducedMotion ? 1 : 0.12, controller.reducedMotion ? 1 : 0.12); 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 }); this.ambient = this.sound.add("ambient", { loop: true, volume: 0.24 }); this.stepSound = this.sound.add("step", { volume: 0.22 }); this.discoverSound = this.sound.add("discover", { volume: 0.35 }); this.portalSound = this.sound.add("portal", { volume: 0.3 }); if (controller.state.soundEnabled) this.enableSound(); setStatus(document.querySelector(".archive-world"), `Welcome, ${controller.state.name}. Walk near a glowing landmark and press E, Space, or Enter.`); this.updateNearest(); } update(time) { if (!this.player) return; let dx = 0; let dy = 0; if (!controller.lock) { 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; if (moving) { const vector = new Phaser.Math.Vector2(dx, dy).normalize().scale(175); this.player.setVelocity(vector.x, vector.y); if (dx) this.player.setFlipX(dx < 0); if (!controller.reducedMotion) { this.player.rotation = Math.sin(time / 80) * 0.025; this.player.setScale(1, 0.98 + Math.abs(Math.sin(time / 95)) * 0.04); } if (controller.state.soundEnabled && time - this.lastStepAt > 340) { this.stepSound.play(); this.lastStepAt = time; } if (time - controller.saveTimer > 700) { controller.saveTimer = time; savePosition(); } } else { this.player.setVelocity(0, 0); this.player.rotation = 0; this.player.setScale(1); } this.wasMoving = moving; this.updateNearest(); if (!controller.lock && ( Phaser.Input.Keyboard.JustDown(this.keys.interact) || Phaser.Input.Keyboard.JustDown(this.keys.enter) || Phaser.Input.Keyboard.JustDown(this.cursors.space) )) this.interact(); } updateNearest() { let nearest = null; let distance = Infinity; Object.entries(LANDMARKS).forEach(([id, item]) => { const nextDistance = Phaser.Math.Distance.Between(this.player.x, this.player.y, item.x, item.y); if (nextDistance < distance) { nearest = id; distance = nextDistance; } this.markers[id].setStrokeStyle(nextDistance < 95 ? 5 : 3, nextDistance < 95 ? 0xffffff : 0xf8e6a7, 0.95); }); const next = distance < 95 ? nearest : null; if (next !== this.nearest) { this.nearest = next; const root = document.querySelector(".archive-world"); setStatus(root, next ? `${LANDMARKS[next].title} is nearby. Press E, Space, Enter, or Explore.` : "Follow the stone paths toward a glowing landmark."); } } interact() { if (controller.lock) return; if (!this.nearest) { setStatus(document.querySelector(".archive-world"), "Move closer to one of the glowing landmark circles."); return; } openPreview(document.querySelector(".archive-world"), this.nearest); } enableSound() { if (this.sound.locked && this.sound.unlock) this.sound.unlock(); this.sound.mute = false; if (!this.ambient.isPlaying) this.ambient.play(); } disableSound() { this.sound.mute = true; if (this.ambient.isPlaying) this.ambient.pause(); } } function openPreview(root, id) { const item = LANDMARKS[id]; if (!item) return; const wasDiscovered = controller.state.discovered.includes(id); controller.state = State.discover(controller.state, id); persist(); controller.lock = true; root.querySelector("[data-world-preview-eyebrow]").textContent = item.eyebrow; root.querySelector("[data-world-preview-title]").textContent = item.title; root.querySelector("[data-world-preview-description]").textContent = item.description; const links = root.querySelector("[data-world-preview-links]"); links.replaceChildren(); item.links.forEach(([label, href]) => { const anchor = document.createElement("a"); anchor.href = href; anchor.textContent = label; anchor.addEventListener("click", () => { if (controller.scene && controller.state.soundEnabled) controller.scene.portalSound.play(); savePosition(); }); links.appendChild(anchor); }); renderHud(root); if (!wasDiscovered && controller.scene && controller.state.soundEnabled) controller.scene.discoverSound.play(); root.querySelector("[data-world-preview]").showModal(); setStatus(root, wasDiscovered ? `${item.title} is already recorded in your journal.` : `${item.title} was added to your discovery journal.`); } function toggleSound(root) { if (!controller.state || !controller.scene) return; controller.state.soundEnabled = !controller.state.soundEnabled; if (controller.state.soundEnabled) controller.scene.enableSound(); else controller.scene.disableSound(); persist(); renderHud(root); setStatus(root, controller.state.soundEnabled ? "Ambient sound is on." : "Sound is muted."); } function savePosition() { if (!controller.state || !controller.scene || !controller.scene.player) return; controller.state = State.withPosition(controller.state, controller.scene.player.x, controller.scene.player.y); persist(); } function persist() { if (controller.state) localStorage.setItem(State.STORAGE_KEY, JSON.stringify(controller.state)); } function renderHud(root) { if (!controller.state) return; root.querySelector("[data-world-player]").textContent = controller.state.name; root.querySelector("[data-world-discovery-count]").textContent = `${controller.state.discovered.length} / ${State.LANDMARK_IDS.length}`; root.querySelector("[data-world-sound]").textContent = controller.state.soundEnabled ? "Sound: on" : "Sound: muted"; const badge = root.querySelector("[data-world-badge]"); badge.hidden = !controller.state.complete; renderJournal(root); } function renderJournal(root) { if (!controller.state) return; const list = root.querySelector("[data-world-journal-list]"); list.replaceChildren(); State.LANDMARK_IDS.forEach((id) => { const row = document.createElement("li"); const found = controller.state.discovered.includes(id); row.className = found ? "is-discovered" : ""; row.textContent = `${found ? "Discovered" : "Unexplored"} — ${LANDMARKS[id].title}`; list.appendChild(row); }); } function setStatus(root, message) { const status = root && root.querySelector("[data-world-status]"); if (status) status.textContent = message; } }());