Refactor org platform navigation and content flows
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:
@@ -1,420 +1,401 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const Data = window.ArchiveWorldData;
|
||||
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 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 },
|
||||
reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
|
||||
saveTimer: 0
|
||||
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
|
||||
};
|
||||
|
||||
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;
|
||||
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 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);
|
||||
const loaded = loadState();
|
||||
if (loaded.state) {
|
||||
controller.state = loaded.state;
|
||||
renderHud();
|
||||
startGame();
|
||||
if (loaded.message) status(loaded.message);
|
||||
} else {
|
||||
openSetup(root);
|
||||
openSetup(loaded.message);
|
||||
}
|
||||
}
|
||||
|
||||
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]");
|
||||
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 };
|
||||
}
|
||||
|
||||
setup.addEventListener("cancel", (event) => event.preventDefault());
|
||||
setupForm.addEventListener("submit", (event) => {
|
||||
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]");
|
||||
dialog.addEventListener("cancel", (event) => event.preventDefault());
|
||||
form.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]");
|
||||
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);
|
||||
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();
|
||||
persistState(controller.state);
|
||||
dialog.close();
|
||||
renderHud();
|
||||
startGame();
|
||||
});
|
||||
}
|
||||
|
||||
function openSetup(root) {
|
||||
const dialog = root.querySelector("[data-world-setup]");
|
||||
function openSetup(message) {
|
||||
const dialog = controller.root.querySelector("[data-world-setup]");
|
||||
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 startGame(root) {
|
||||
renderHud(root);
|
||||
const game = new Phaser.Game({
|
||||
type: Phaser.AUTO,
|
||||
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));
|
||||
});
|
||||
controller.root.querySelector("[data-world-interact]").addEventListener("click", () => {
|
||||
if (controller.scene) controller.scene.interact();
|
||||
});
|
||||
controller.root.querySelector("[data-world-attack]").addEventListener("click", () => {
|
||||
if (controller.scene) controller.scene.attack(controller.scene.time.now);
|
||||
});
|
||||
controller.root.querySelector("[data-world-item]").addEventListener("click", useSelectedItem);
|
||||
controller.root.querySelector("[data-world-menu]").addEventListener("click", openMenu);
|
||||
controller.root.querySelector("[data-world-quests]").addEventListener("click", () => controller.ui.openMenu(controller.state, "quests"));
|
||||
controller.root.querySelector("[data-world-inventory]").addEventListener("click", () => controller.ui.openMenu(controller.state, "inventory"));
|
||||
controller.root.querySelector("[data-world-journal-open]").addEventListener("click", () => controller.ui.openMenu(controller.state, "journal"));
|
||||
controller.root.querySelector("[data-world-sound]").addEventListener("click", toggleSound);
|
||||
controller.root.querySelector("[data-world-fullscreen]").addEventListener("click", toggleFullscreen);
|
||||
controller.root.querySelector("[data-world-reset]").addEventListener("click", () => 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: "#17251e",
|
||||
backgroundColor: "#101b18",
|
||||
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
|
||||
}
|
||||
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 }
|
||||
});
|
||||
root.dataset.gameReady = "true";
|
||||
root.querySelector("#archive-world-game").focus();
|
||||
window.archiveWorldGame = game;
|
||||
controller.root.dataset.gameReady = "true";
|
||||
controller.root.querySelector("#archive-world-game").focus();
|
||||
window.archiveWorldGame = controller.game;
|
||||
}
|
||||
|
||||
class ArchiveTownScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super("ArchiveTown");
|
||||
this.nearest = null;
|
||||
this.lastStepAt = 0;
|
||||
this.wasMoving = false;
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
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");
|
||||
}
|
||||
|
||||
create() {
|
||||
controller.scene = this;
|
||||
this.add.image(0, 0, "archive-town").setOrigin(0).setDepth(0);
|
||||
this.physics.world.setBounds(0, 0, 1280, 960);
|
||||
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.`);
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
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.");
|
||||
}
|
||||
|
||||
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);
|
||||
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.");
|
||||
}
|
||||
|
||||
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();
|
||||
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 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();
|
||||
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}`);
|
||||
controller.root.querySelector("[data-world-health-bar]").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");
|
||||
controller.root.querySelector("[data-world-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;
|
||||
|
||||
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.`);
|
||||
controller.ui.openDefeat();
|
||||
}
|
||||
|
||||
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 openEnding() {
|
||||
controller.ui.openEnding(controller.state);
|
||||
}
|
||||
|
||||
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 text(selector, value) {
|
||||
const node = controller.root.querySelector(selector);
|
||||
if (node) node.textContent = value;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Object.assign(controller, {
|
||||
setState, renderHud, status, setPrompt, showBoss, hideBoss, openDefeat, openEnding,
|
||||
openLandmark, openNpc, openChallenge, openMenu, travel, useSelectedItem, cycleItem
|
||||
});
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user