Refactor org web content and simplify shared UI
All checks were successful
Build Org Website / build (push) Successful in 44s
All checks were successful
Build Org Website / build (push) Successful in 44s
This commit is contained in:
BIN
assets/audio/archive-world/ambient.ogg
Normal file
BIN
assets/audio/archive-world/ambient.ogg
Normal file
Binary file not shown.
BIN
assets/audio/archive-world/discover.wav
Normal file
BIN
assets/audio/archive-world/discover.wav
Normal file
Binary file not shown.
BIN
assets/audio/archive-world/portal.wav
Normal file
BIN
assets/audio/archive-world/portal.wav
Normal file
Binary file not shown.
BIN
assets/audio/archive-world/step.wav
Normal file
BIN
assets/audio/archive-world/step.wav
Normal file
Binary file not shown.
BIN
assets/images/play/archive-world/archive-town.webp
Normal file
BIN
assets/images/play/archive-world/archive-town.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 563 KiB |
BIN
assets/images/play/archive-world/traveler.png
Normal file
BIN
assets/images/play/archive-world/traveler.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -1,7 +0,0 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
var script = document.createElement("script");
|
||||
script.src = "/assets/scripts/pages/ash-below-lake.js";
|
||||
script.defer = true;
|
||||
document.head.appendChild(script);
|
||||
}());
|
||||
114
assets/scripts/pages/archive-world-state.js
Normal file
114
assets/scripts/pages/archive-world-state.js
Normal file
@@ -0,0 +1,114 @@
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.ArchiveWorldState = api;
|
||||
}(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
const VERSION = 1;
|
||||
const STORAGE_KEY = "zxh_archive_world_v1";
|
||||
const PALETTES = Object.freeze(["brass", "moss", "berry"]);
|
||||
const LANDMARK_IDS = Object.freeze([
|
||||
"gate",
|
||||
"library",
|
||||
"study",
|
||||
"inn",
|
||||
"workshop",
|
||||
"playroom",
|
||||
"museum",
|
||||
"garden"
|
||||
]);
|
||||
const SPAWN = Object.freeze({ x: 640, y: 830 });
|
||||
|
||||
function validateName(value) {
|
||||
const name = String(value || "").trim().replace(/\s+/g, " ");
|
||||
return name.length >= 1 && name.length <= 20 ? name : null;
|
||||
}
|
||||
|
||||
function validatePalette(value) {
|
||||
return PALETTES.includes(value) ? value : null;
|
||||
}
|
||||
|
||||
function fresh(name, palette) {
|
||||
return {
|
||||
version: VERSION,
|
||||
name: validateName(name) || "",
|
||||
palette: validatePalette(palette) || "brass",
|
||||
discovered: [],
|
||||
complete: false,
|
||||
position: { x: SPAWN.x, y: SPAWN.y },
|
||||
soundEnabled: false
|
||||
};
|
||||
}
|
||||
|
||||
function clamp(value, min, max, fallback) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback;
|
||||
}
|
||||
|
||||
function normalize(candidate) {
|
||||
if (!candidate || candidate.version !== VERSION) return null;
|
||||
const name = validateName(candidate.name);
|
||||
const palette = validatePalette(candidate.palette);
|
||||
if (!name || !palette) return null;
|
||||
|
||||
const discovered = Array.isArray(candidate.discovered)
|
||||
? Array.from(new Set(candidate.discovered.filter((id) => LANDMARK_IDS.includes(id))))
|
||||
: [];
|
||||
|
||||
return {
|
||||
version: VERSION,
|
||||
name,
|
||||
palette,
|
||||
discovered,
|
||||
complete: LANDMARK_IDS.every((id) => discovered.includes(id)),
|
||||
position: {
|
||||
x: clamp(candidate.position && candidate.position.x, 36, 1244, SPAWN.x),
|
||||
y: clamp(candidate.position && candidate.position.y, 36, 924, SPAWN.y)
|
||||
},
|
||||
soundEnabled: candidate.soundEnabled === true
|
||||
};
|
||||
}
|
||||
|
||||
function parse(raw) {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return normalize(JSON.parse(raw));
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function discover(state, landmarkId) {
|
||||
const current = normalize(state);
|
||||
if (!current || !LANDMARK_IDS.includes(landmarkId)) return current;
|
||||
if (!current.discovered.includes(landmarkId)) current.discovered.push(landmarkId);
|
||||
current.complete = LANDMARK_IDS.every((id) => current.discovered.includes(id));
|
||||
return current;
|
||||
}
|
||||
|
||||
function withPosition(state, x, y) {
|
||||
const current = normalize(state);
|
||||
if (!current) return null;
|
||||
current.position.x = clamp(x, 36, 1244, SPAWN.x);
|
||||
current.position.y = clamp(y, 36, 924, SPAWN.y);
|
||||
return current;
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
VERSION,
|
||||
STORAGE_KEY,
|
||||
PALETTES,
|
||||
LANDMARK_IDS,
|
||||
SPAWN,
|
||||
validateName,
|
||||
validatePalette,
|
||||
fresh,
|
||||
normalize,
|
||||
parse,
|
||||
discover,
|
||||
withPosition
|
||||
});
|
||||
}));
|
||||
420
assets/scripts/pages/archive-world.js
Normal file
420
assets/scripts/pages/archive-world.js
Normal file
@@ -0,0 +1,420 @@
|
||||
(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;
|
||||
}
|
||||
}());
|
||||
File diff suppressed because it is too large
Load Diff
@@ -236,5 +236,7 @@ async function loadBoard() {
|
||||
populateMobileControls(items);
|
||||
}
|
||||
|
||||
renderLevelTabs();
|
||||
loadBoard();
|
||||
if (document.getElementById("kanban-board")) {
|
||||
renderLevelTabs();
|
||||
loadBoard();
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
caption: "A wooden moon rolls beneath the cabinet whenever nobody is looking.",
|
||||
curated: [
|
||||
["Play hub", "/play/play.html"],
|
||||
["Ash Below the Lake", "/play/rpg.html"],
|
||||
["The Archive World", "/play/rpg.html"],
|
||||
["The Rain Index", "/play/the-rain-index.html"]
|
||||
],
|
||||
include: ["/play/"],
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
const root = $(".play-root[data-play-page]");
|
||||
if (!root) return;
|
||||
const page = root.dataset.playPage;
|
||||
const inits = { hub, memory, constellation, poem, bookshelf, recipe, timeline, ink, terminal, study, sigil, rpg };
|
||||
const inits = { hub, memory, constellation, poem, bookshelf, ink, terminal, study, sigil };
|
||||
if (inits[page]) inits[page](root);
|
||||
});
|
||||
|
||||
@@ -256,63 +256,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function recipe(root) {
|
||||
const wheel = $("[data-recipe-spin]", root);
|
||||
const result = $("[data-recipe-result]", root);
|
||||
const groups = [
|
||||
["Rice", "Flatbread", "Roast potatoes", "Noodles"],
|
||||
["Lemon chicken", "Spiced lentils", "Tomato eggs", "Pepper stew"],
|
||||
["Cucumber salad", "Mint yoghurt", "Pickled onions", "Charred greens"],
|
||||
["Serve with stories", "Eat outside", "Use the blue plates", "Make extra tea"]
|
||||
];
|
||||
let spin = 0;
|
||||
let feast = 0;
|
||||
wheel.addEventListener("click", () => {
|
||||
spin += 540 + Math.floor(Math.random() * 540);
|
||||
feast += 1;
|
||||
wheel.style.transform = `rotate(${spin}deg)`;
|
||||
result.innerHTML = groups.map((group) => `<li>${pick(group)}</li>`).join("") + (feast % 4 === 0 ? "<li>Bonus: someone gets the last crispy bit.</li>" : "");
|
||||
});
|
||||
wheel.click();
|
||||
}
|
||||
|
||||
function timeline(root) {
|
||||
const items = [
|
||||
{ year: "2022", text: "The personal web habit begins." },
|
||||
{ year: "2025", text: "Org publishing becomes the main site engine." },
|
||||
{ year: "2025", text: "Weekly reviews and career notes grow into a library." },
|
||||
{ year: "2026", text: "Dashboards, services, and authoring tools join the site." },
|
||||
{ year: "2026", text: "The play wing opens." }
|
||||
];
|
||||
const list = $("[data-timeline-list]", root);
|
||||
const status = $("[data-timeline-status]", root);
|
||||
const paradox = document.createElement("button");
|
||||
paradox.type = "button";
|
||||
paradox.textContent = "Paradox";
|
||||
$("[data-timeline-shuffle]", root).insertAdjacentElement("afterend", paradox);
|
||||
setupSortable(list, render, check);
|
||||
$("[data-timeline-shuffle]", root).addEventListener("click", () => render(shuffle(items)));
|
||||
paradox.addEventListener("click", () => render([...items].reverse()));
|
||||
render(shuffle(items));
|
||||
|
||||
function render(source) {
|
||||
list.innerHTML = "";
|
||||
source.forEach((item, index) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "timeline-card";
|
||||
card.draggable = true;
|
||||
card.dataset.value = String(items.indexOf(item));
|
||||
card.innerHTML = `<span>${item.year}</span><strong>${item.text}</strong>`;
|
||||
list.appendChild(card);
|
||||
});
|
||||
check();
|
||||
}
|
||||
function check() {
|
||||
const current = $$(".timeline-card", list).map((card) => Number(card.dataset.value));
|
||||
status.textContent = current.every((value, index) => value === index) ? "Timeline restored" : "Arrange the cards";
|
||||
}
|
||||
}
|
||||
|
||||
function ink(root) {
|
||||
const canvas = $("[data-ink-canvas]", root);
|
||||
const ctx = canvas.getContext("2d");
|
||||
@@ -492,353 +435,6 @@
|
||||
draw();
|
||||
}
|
||||
|
||||
function rpg(root) {
|
||||
if (window.AshBelowLakeRpg) return window.AshBelowLakeRpg(root);
|
||||
const canvas = $("[data-rpg-canvas]", root);
|
||||
const ctx = canvas.getContext("2d");
|
||||
const els = {
|
||||
speaker: $("[data-rpg-speaker]", root),
|
||||
line: $("[data-rpg-line]", root),
|
||||
room: $("[data-rpg-room]", root),
|
||||
route: $("[data-rpg-route]", root),
|
||||
hearts: $("[data-rpg-hearts]", root),
|
||||
quests: $("[data-rpg-quests]", root),
|
||||
inventory: $("[data-rpg-inventory]", root),
|
||||
status: $("[data-rpg-status]", root)
|
||||
};
|
||||
const tile = 32;
|
||||
const saveSlot = "hollow-archive";
|
||||
const fallbackKey = "play:rpg:hollow-archive";
|
||||
const items = {
|
||||
lamp: { label: "Desk Lamp", room: "entrance", x: 10, y: 8, color: "#f2c94c" },
|
||||
page: { label: "Loose Page", room: "stacks", x: 18, y: 5, color: "#f7efe0" },
|
||||
key: { label: "Basement Key", room: "garden", x: 4, y: 11, color: "#d59b45" }
|
||||
};
|
||||
const npcs = {
|
||||
archivist: { name: "Archivist", room: "entrance", x: 6, y: 6, color: "#b98bff" },
|
||||
shade: { name: "Shy Shade", room: "stacks", x: 17, y: 9, color: "#6ed0d4" },
|
||||
gate: { name: "Iron Door", room: "garden", x: 19, y: 10, color: "#8a8f98" }
|
||||
};
|
||||
const rooms = {
|
||||
entrance: {
|
||||
name: "Entrance",
|
||||
floor: "#353029",
|
||||
exits: [{ x: 22, y: 7, to: "stacks", px: 1, py: 7 }],
|
||||
walls: rects([[0, 0, 24, 1], [0, 15, 24, 1], [0, 0, 1, 16], [23, 0, 1, 6], [23, 9, 1, 7], [8, 3, 1, 8], [14, 7, 5, 1]])
|
||||
},
|
||||
stacks: {
|
||||
name: "Stacks",
|
||||
floor: "#242d35",
|
||||
exits: [{ x: 0, y: 7, to: "entrance", px: 22, py: 7 }, { x: 23, y: 12, to: "garden", px: 1, py: 12 }],
|
||||
walls: rects([[0, 0, 24, 1], [0, 15, 24, 1], [0, 0, 1, 6], [0, 9, 1, 7], [23, 0, 1, 11], [23, 14, 1, 2], [4, 3, 2, 10], [10, 2, 2, 11], [16, 3, 2, 5]])
|
||||
},
|
||||
garden: {
|
||||
name: "Moon Garden",
|
||||
floor: "#21362e",
|
||||
exits: [{ x: 0, y: 12, to: "stacks", px: 22, py: 12 }],
|
||||
walls: rects([[0, 0, 24, 1], [0, 15, 24, 1], [0, 0, 1, 11], [0, 14, 1, 2], [23, 0, 1, 16], [7, 4, 10, 1], [7, 10, 1, 4], [13, 10, 1, 4]])
|
||||
}
|
||||
};
|
||||
let state = freshState();
|
||||
let running = false;
|
||||
|
||||
function freshState() {
|
||||
return {
|
||||
room: "entrance",
|
||||
player: { x: 3, y: 7, facing: "down" },
|
||||
inventory: [],
|
||||
flags: {},
|
||||
route: "Undecided",
|
||||
hearts: 3,
|
||||
ending: null,
|
||||
message: { speaker: "Archivist", line: "The archive waits. Find the lamp, help the shade, then decide what to do with the locked door." }
|
||||
};
|
||||
}
|
||||
|
||||
function rects(sources) {
|
||||
const set = new Set();
|
||||
sources.forEach(([x, y, w, h]) => {
|
||||
for (let yy = y; yy < y + h; yy += 1) for (let xx = x; xx < x + w; xx += 1) set.add(`${xx},${yy}`);
|
||||
});
|
||||
return set;
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const room = rooms[state.room];
|
||||
ctx.fillStyle = "#14110e";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
for (let y = 0; y < 16; y += 1) {
|
||||
for (let x = 0; x < 24; x += 1) {
|
||||
ctx.fillStyle = room.walls.has(`${x},${y}`) ? "#181716" : room.floor;
|
||||
ctx.fillRect(x * tile, y * tile, tile, tile);
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.035)";
|
||||
ctx.strokeRect(x * tile, y * tile, tile, tile);
|
||||
}
|
||||
}
|
||||
room.exits.forEach((exit) => drawGlyph(exit.x, exit.y, "#c48a41", "door"));
|
||||
Object.entries(items).forEach(([id, item]) => {
|
||||
if (item.room === state.room && !state.inventory.includes(id)) drawGlyph(item.x, item.y, item.color, "item");
|
||||
});
|
||||
Object.values(npcs).forEach((npc) => {
|
||||
if (npc.room === state.room) drawGlyph(npc.x, npc.y, npc.color, npc.name === "Iron Door" ? "doorNpc" : "npc");
|
||||
});
|
||||
drawPlayer();
|
||||
renderHud();
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
function drawGlyph(x, y, color, kind) {
|
||||
const px = x * tile;
|
||||
const py = y * tile;
|
||||
ctx.fillStyle = color;
|
||||
if (kind === "item") {
|
||||
ctx.fillRect(px + 10, py + 10, 12, 12);
|
||||
ctx.fillStyle = "rgba(255,255,255,0.45)";
|
||||
ctx.fillRect(px + 13, py + 7, 6, 6);
|
||||
} else if (kind === "door" || kind === "doorNpc") {
|
||||
ctx.fillRect(px + 7, py + 4, 18, 25);
|
||||
ctx.fillStyle = "#211812";
|
||||
ctx.fillRect(px + 20, py + 16, 3, 3);
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.arc(px + 16, py + 12, 9, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillRect(px + 8, py + 20, 16, 8);
|
||||
}
|
||||
}
|
||||
|
||||
function drawPlayer() {
|
||||
const px = state.player.x * tile;
|
||||
const py = state.player.y * tile;
|
||||
ctx.fillStyle = "#f35f5f";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px + 16, py + 7);
|
||||
ctx.bezierCurveTo(px + 2, py + 2, px + 1, py + 22, px + 16, py + 28);
|
||||
ctx.bezierCurveTo(px + 31, py + 22, px + 30, py + 2, px + 16, py + 7);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#fff8e8";
|
||||
ctx.fillRect(px + 11, py + 13, 4, 4);
|
||||
ctx.fillRect(px + 18, py + 13, 4, 4);
|
||||
}
|
||||
|
||||
function renderHud() {
|
||||
els.speaker.textContent = state.message.speaker;
|
||||
els.line.textContent = state.ending ? endingLine() : state.message.line;
|
||||
els.room.textContent = rooms[state.room].name;
|
||||
els.route.textContent = state.route;
|
||||
els.hearts.textContent = String(state.hearts);
|
||||
els.inventory.innerHTML = state.inventory.length
|
||||
? state.inventory.map((id) => `<li>${items[id].label}</li>`).join("")
|
||||
: "<li>Empty</li>";
|
||||
const questRows = [
|
||||
["Find a light", state.inventory.includes("lamp")],
|
||||
["Return the loose page to the shade", state.flags.shadeHelped],
|
||||
["Open, force, or leave the iron door", Boolean(state.ending)]
|
||||
];
|
||||
els.quests.innerHTML = questRows.map(([text, done]) => `<li class="${done ? "is-done" : ""}">${done ? "Done: " : ""}${text}</li>`).join("");
|
||||
}
|
||||
|
||||
function move(dx, dy, facing) {
|
||||
if (!running || state.ending) return;
|
||||
state.player.facing = facing;
|
||||
const nx = state.player.x + dx;
|
||||
const ny = state.player.y + dy;
|
||||
const room = rooms[state.room];
|
||||
const exit = room.exits.find((candidate) => candidate.x === nx && candidate.y === ny);
|
||||
if (exit) {
|
||||
state.room = exit.to;
|
||||
state.player.x = exit.px;
|
||||
state.player.y = exit.py;
|
||||
say("Narrator", `You enter ${rooms[state.room].name}.`);
|
||||
autosave();
|
||||
return;
|
||||
}
|
||||
if (nx < 0 || ny < 0 || nx > 23 || ny > 15 || room.walls.has(`${nx},${ny}`) || npcAt(nx, ny)) return;
|
||||
state.player.x = nx;
|
||||
state.player.y = ny;
|
||||
const item = itemAt(nx, ny);
|
||||
if (item) take(item);
|
||||
}
|
||||
|
||||
function act() {
|
||||
if (!running) return start();
|
||||
if (state.ending) return;
|
||||
const front = inFront();
|
||||
const npc = npcAt(front.x, front.y);
|
||||
if (npc) talk(npc);
|
||||
else {
|
||||
const here = itemAt(state.player.x, state.player.y);
|
||||
if (here) take(here);
|
||||
else say("Narrator", "Dust moves in the light. Nothing asks to be changed here.");
|
||||
}
|
||||
autosave();
|
||||
}
|
||||
|
||||
function inFront() {
|
||||
const delta = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] }[state.player.facing] || [0, 1];
|
||||
return { x: state.player.x + delta[0], y: state.player.y + delta[1] };
|
||||
}
|
||||
|
||||
function itemAt(x, y) {
|
||||
return Object.keys(items).find((id) => {
|
||||
const item = items[id];
|
||||
return item.room === state.room && item.x === x && item.y === y && !state.inventory.includes(id);
|
||||
});
|
||||
}
|
||||
|
||||
function npcAt(x, y) {
|
||||
return Object.keys(npcs).find((id) => {
|
||||
const npc = npcs[id];
|
||||
return npc.room === state.room && npc.x === x && npc.y === y;
|
||||
});
|
||||
}
|
||||
|
||||
function take(id) {
|
||||
state.inventory.push(id);
|
||||
if (id === "lamp") state.route = "Gentle";
|
||||
if (id === "key" && !state.flags.shadeHelped) state.route = "Power";
|
||||
say("Found", `${items[id].label} joined your inventory.`);
|
||||
}
|
||||
|
||||
function talk(id) {
|
||||
if (id === "archivist") {
|
||||
if (!state.inventory.includes("lamp")) say("Archivist", "Take the lamp from the lower desk. The stacks dislike being crossed in the dark.");
|
||||
else if (!state.flags.shadeHelped) say("Archivist", "A loose page has gone missing. The quiet reader in the stacks knows where it belongs.");
|
||||
else say("Archivist", "You have been kind to a forgotten page. The garden door will remember that.");
|
||||
}
|
||||
if (id === "shade") {
|
||||
if (!state.inventory.includes("page")) say("Shy Shade", "I lost the page with my name on it. It fell somewhere nearby.");
|
||||
else {
|
||||
state.flags.shadeHelped = true;
|
||||
state.inventory = state.inventory.filter((item) => item !== "page");
|
||||
state.route = "Mercy";
|
||||
say("Shy Shade", "You returned my page instead of keeping it. Take the honest route through the garden.");
|
||||
}
|
||||
}
|
||||
if (id === "gate") {
|
||||
if (state.flags.shadeHelped) end("mercy");
|
||||
else if (state.inventory.includes("key")) end("power");
|
||||
else end("quiet");
|
||||
}
|
||||
}
|
||||
|
||||
function end(kind) {
|
||||
state.ending = kind;
|
||||
state.route = kind === "mercy" ? "Mercy" : kind === "power" ? "Power" : "Quiet";
|
||||
say("Ending", endingLine());
|
||||
save();
|
||||
}
|
||||
|
||||
function endingLine() {
|
||||
if (state.ending === "mercy") return "Mercy ending: the iron door opens without a sound, and every returned page remembers your name.";
|
||||
if (state.ending === "power") return "Power ending: the key turns, but the archive grows colder around the missing page.";
|
||||
if (state.ending === "quiet") return "Quiet ending: you leave the locked door alone. Some mysteries stay intact.";
|
||||
return state.message.line;
|
||||
}
|
||||
|
||||
function say(speaker, line) {
|
||||
state.message = { speaker, line };
|
||||
}
|
||||
|
||||
function start() {
|
||||
running = true;
|
||||
say("Archivist", "Walk the archive. Speak gently, or take what you need. The route will notice.");
|
||||
els.status.textContent = "Started. Progress autosaves after room changes and actions.";
|
||||
}
|
||||
|
||||
async function save() {
|
||||
localStorage.setItem(fallbackKey, JSON.stringify(state));
|
||||
try {
|
||||
const response = await fetch(`/api/play/rpg/save/${saveSlot}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ payload: state })
|
||||
});
|
||||
if (!response.ok) throw new Error(`Save failed: ${response.status}`);
|
||||
els.status.textContent = "Saved to backend.";
|
||||
} catch (_error) {
|
||||
els.status.textContent = "Saved locally. Backend save API was not reachable.";
|
||||
}
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const response = await fetch(`/api/play/rpg/save/${saveSlot}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
state = normalizeState(data.payload);
|
||||
running = true;
|
||||
els.status.textContent = "Loaded from backend.";
|
||||
return;
|
||||
}
|
||||
} catch (_error) {
|
||||
/* Fall through to local save. */
|
||||
}
|
||||
const local = localStorage.getItem(fallbackKey);
|
||||
if (local) {
|
||||
state = normalizeState(JSON.parse(local));
|
||||
running = true;
|
||||
els.status.textContent = "Loaded local save.";
|
||||
} else {
|
||||
els.status.textContent = "No save found.";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeState(candidate) {
|
||||
return Object.assign(freshState(), candidate || {}, {
|
||||
player: Object.assign(freshState().player, (candidate && candidate.player) || {}),
|
||||
inventory: Array.isArray(candidate && candidate.inventory) ? candidate.inventory.filter((id) => items[id]) : [],
|
||||
flags: Object.assign({}, (candidate && candidate.flags) || {}),
|
||||
message: Object.assign(freshState().message, (candidate && candidate.message) || {})
|
||||
});
|
||||
}
|
||||
|
||||
function autosave() {
|
||||
save();
|
||||
}
|
||||
|
||||
$("[data-rpg-start]", root).addEventListener("click", start);
|
||||
$("[data-rpg-save]", root).addEventListener("click", save);
|
||||
$("[data-rpg-load]", root).addEventListener("click", load);
|
||||
$("[data-rpg-reset]", root).addEventListener("click", async () => {
|
||||
state = freshState();
|
||||
running = false;
|
||||
localStorage.removeItem(fallbackKey);
|
||||
try {
|
||||
await fetch(`/api/play/rpg/save/${saveSlot}`, { method: "DELETE" });
|
||||
els.status.textContent = "Reset and cleared backend save.";
|
||||
} catch (_error) {
|
||||
els.status.textContent = "Reset locally. Backend save API was not reachable.";
|
||||
}
|
||||
});
|
||||
$("[data-rpg-act]", root).addEventListener("click", act);
|
||||
$$("[data-rpg-move]", root).forEach((button) => {
|
||||
const moves = { up: [0, -1, "up"], down: [0, 1, "down"], left: [-1, 0, "left"], right: [1, 0, "right"] };
|
||||
button.addEventListener("click", () => move(...moves[button.dataset.rpgMove]));
|
||||
});
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (!root.isConnected) return;
|
||||
const tag = event.target.tagName;
|
||||
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
||||
const keys = {
|
||||
ArrowUp: [0, -1, "up"], w: [0, -1, "up"],
|
||||
ArrowDown: [0, 1, "down"], s: [0, 1, "down"],
|
||||
ArrowLeft: [-1, 0, "left"], a: [-1, 0, "left"],
|
||||
ArrowRight: [1, 0, "right"], d: [1, 0, "right"]
|
||||
};
|
||||
if (keys[event.key]) {
|
||||
event.preventDefault();
|
||||
move(...keys[event.key]);
|
||||
}
|
||||
if (event.key === " " || event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
act();
|
||||
}
|
||||
});
|
||||
draw();
|
||||
load();
|
||||
}
|
||||
|
||||
function setupSortable(container, _render, after) {
|
||||
let dragged = null;
|
||||
container.addEventListener("dragstart", (event) => {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
if (!document.getElementById("wird-app")) return;
|
||||
|
||||
const API = "/api/wird";
|
||||
|
||||
// type: "count" | "juz" | "min" | "rating" | "nafl" | "khatm"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* Event listener for scrolling and changing the active label on the TOC */
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const toc = document.querySelector("#text-table-of-contents");
|
||||
if (!toc) { console.warn("No #text-table-of-contents found"); return; }
|
||||
if (!toc) return;
|
||||
|
||||
const links = toc.querySelectorAll('a[href^="#"]'); // '^=' is a starts with operator.
|
||||
// <a href="#introduction">Intro</a> matches
|
||||
if (!links.length) { console.warn("No ToC links found"); return; }
|
||||
if (!links.length) return;
|
||||
|
||||
// Map: id -> link
|
||||
const linkById = new Map();
|
||||
@@ -14,7 +14,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) linkById.set(id, a);
|
||||
});
|
||||
if (!linkById.size) { console.warn("No matching headings with IDs"); return; }
|
||||
if (!linkById.size) return;
|
||||
|
||||
// Headings to observe (h2–h4 usually)
|
||||
const headings = [...document.querySelectorAll("h2[id], h3[id], h4[id]")]
|
||||
|
||||
1
assets/scripts/vendor/phaser-4.1.0.min.js
vendored
Normal file
1
assets/scripts/vendor/phaser-4.1.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -307,16 +307,14 @@
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
|
||||
.bookshelf,
|
||||
.timeline-list {
|
||||
.bookshelf {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.book-spine,
|
||||
.timeline-card {
|
||||
.book-spine {
|
||||
touch-action: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
@@ -350,54 +348,6 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.timeline-list {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.timeline-card {
|
||||
display: grid;
|
||||
grid-template-columns: 5rem 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.timeline-card span {
|
||||
color: var(--play-brass);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.recipe-tool {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 280px) minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recipe-wheel {
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
border-radius: 50% !important;
|
||||
background:
|
||||
conic-gradient(from 0deg, var(--play-red), var(--play-brass), var(--play-green), var(--play-blue), var(--play-red));
|
||||
color: #fff8e8 !important;
|
||||
font-weight: 900;
|
||||
transition: transform 900ms cubic-bezier(.18, .88, .2, 1.15);
|
||||
}
|
||||
|
||||
.recipe-result {
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.recipe-result li {
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.terminal-tool {
|
||||
background: #15120e;
|
||||
color: #f2e3bd;
|
||||
@@ -495,316 +445,358 @@
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.rpg-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 960px) minmax(280px, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
.archive-world {
|
||||
--world-ink: #17251e;
|
||||
--world-panel: #f3ead2;
|
||||
--world-paper: #fff9e9;
|
||||
--world-line: #8b6841;
|
||||
--world-brass: #d49a3a;
|
||||
--world-moss: #527657;
|
||||
--world-berry: #9d5268;
|
||||
max-width: 1440px;
|
||||
}
|
||||
|
||||
.ash-rpg-root {
|
||||
--ash-black: #05070c;
|
||||
--ash-ink: #0d1420;
|
||||
--ash-panel: #121a27;
|
||||
--ash-line: #344154;
|
||||
--ash-gold: #e0a451;
|
||||
--ash-cold: #8bd3ff;
|
||||
--ash-red: #db4f62;
|
||||
--ash-text: #f8efe0;
|
||||
--ash-muted: #a8b4c2;
|
||||
max-width: 1380px;
|
||||
.archive-world__heading {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.ash-rpg-root .play-page-head {
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid rgba(139, 211, 255, 0.18);
|
||||
.archive-world__shell {
|
||||
overflow: hidden;
|
||||
border: 2px solid var(--world-line);
|
||||
border-radius: 12px;
|
||||
background: var(--world-ink);
|
||||
box-shadow: 0 22px 70px rgba(23, 37, 30, 0.28);
|
||||
}
|
||||
|
||||
.ash-rpg-root .play-page-head h1 {
|
||||
color: var(--ash-text);
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
text-shadow: 0 0 18px rgba(139, 211, 255, 0.22);
|
||||
}
|
||||
|
||||
.ash-rpg-root .play-page-head p,
|
||||
.ash-rpg-root .play-back {
|
||||
color: var(--ash-muted);
|
||||
}
|
||||
|
||||
.ash-rpg-shell {
|
||||
position: relative;
|
||||
border-color: rgba(139, 211, 255, 0.22);
|
||||
.archive-world__toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 2px solid #6e5738;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(12, 20, 32, 0.96), rgba(4, 7, 12, 0.98)),
|
||||
var(--ash-black);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(224, 164, 81, 0.14) inset,
|
||||
0 28px 80px rgba(0, 0, 0, 0.42);
|
||||
linear-gradient(rgba(255, 255, 255, 0.04), transparent),
|
||||
#2d3f33;
|
||||
color: #fff6dc;
|
||||
}
|
||||
|
||||
.ash-rpg-shell::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
border-radius: 8px;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.025), rgba(255, 255, 255, 0.025) 1px, transparent 1px, transparent 4px);
|
||||
mix-blend-mode: screen;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.rpg-stage {
|
||||
.archive-world__toolbar > div {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.rpg-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
image-rendering: pixelated;
|
||||
border: 2px solid var(--ash-line);
|
||||
border-radius: 8px;
|
||||
background: var(--ash-black);
|
||||
box-shadow:
|
||||
0 0 0 4px #05070c,
|
||||
0 0 34px rgba(139, 211, 255, 0.16);
|
||||
}
|
||||
|
||||
.ash-rpg-canvas {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.rpg-dialogue,
|
||||
.rpg-panel {
|
||||
border: 1px solid var(--ash-line);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(21, 31, 46, 0.95), rgba(8, 12, 18, 0.96)),
|
||||
var(--ash-panel);
|
||||
color: var(--ash-text);
|
||||
}
|
||||
|
||||
.rpg-dialogue {
|
||||
min-height: 130px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 0 0 1px rgba(248, 239, 224, 0.05) inset;
|
||||
}
|
||||
|
||||
.rpg-dialogue strong,
|
||||
.rpg-list h2 {
|
||||
color: var(--ash-gold);
|
||||
}
|
||||
|
||||
.rpg-dialogue p,
|
||||
.rpg-status {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--ash-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.rpg-panel {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.rpg-actions,
|
||||
.rpg-mobile-pad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ash-rpg-root button {
|
||||
border-color: rgba(139, 211, 255, 0.28);
|
||||
background: linear-gradient(180deg, #1a2638, #0c121c);
|
||||
color: var(--ash-text);
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
font-weight: 800;
|
||||
.archive-world__toolbar span {
|
||||
color: #d8c9a6;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ash-rpg-root button:hover:not(:disabled),
|
||||
.ash-rpg-root button:focus-visible {
|
||||
background: linear-gradient(180deg, #31435f, #142236);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 0 18px rgba(139, 211, 255, 0.22);
|
||||
.archive-world__toolbar button,
|
||||
.archive-world__controls button,
|
||||
.archive-world__dialog button {
|
||||
border: 1px solid #d6b777;
|
||||
border-radius: 6px;
|
||||
background: #fff3d2;
|
||||
color: #2b2116;
|
||||
font: inherit;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ash-rpg-root button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.42;
|
||||
.archive-world__toolbar button {
|
||||
margin-left: auto;
|
||||
padding: 0.55rem 0.75rem;
|
||||
}
|
||||
|
||||
.rpg-mobile-pad {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
.archive-world__toolbar button + button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.rpg-mobile-pad [data-rpg-move="up"],
|
||||
.rpg-mobile-pad [data-rpg-move="down"] {
|
||||
.archive-world__toolbar button:hover,
|
||||
.archive-world__toolbar button:focus-visible,
|
||||
.archive-world__controls button:hover,
|
||||
.archive-world__controls button:focus-visible,
|
||||
.archive-world__dialog button:hover,
|
||||
.archive-world__dialog button:focus-visible {
|
||||
border-color: #fff0b5;
|
||||
background: #fffaf0;
|
||||
box-shadow: 0 0 0 3px rgba(255, 236, 173, 0.24);
|
||||
}
|
||||
|
||||
.archive-world__badge {
|
||||
margin: 0;
|
||||
padding: 0.4rem 0.65rem;
|
||||
border: 1px solid #f2d483;
|
||||
border-radius: 999px;
|
||||
background: #604b1f;
|
||||
color: #fff1b6;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.archive-world__game {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 3 / 2;
|
||||
max-height: min(72vh, 760px);
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
background: #17251e;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.archive-world__game:focus-visible {
|
||||
box-shadow: inset 0 0 0 4px #fff0a9;
|
||||
}
|
||||
|
||||
.archive-world__game canvas {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.archive-world__loading {
|
||||
position: absolute;
|
||||
inset: 45% 0 auto;
|
||||
margin: 0;
|
||||
color: #f5e4b7;
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.archive-world[data-game-ready="true"] .archive-world__loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.archive-world__controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.8rem;
|
||||
border-top: 2px solid #6e5738;
|
||||
background: #2d3f33;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.archive-world__dpad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 46px);
|
||||
grid-template-rows: repeat(2, 42px);
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.archive-world__dpad [data-world-move="up"] {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.rpg-mobile-pad [data-rpg-move="left"] {
|
||||
.archive-world__dpad [data-world-move="left"] {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.rpg-mobile-pad [data-rpg-act] {
|
||||
.archive-world__dpad [data-world-move="down"] {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.rpg-mobile-pad [data-rpg-move="right"] {
|
||||
.archive-world__dpad [data-world-move="right"] {
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
.rpg-stats {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
margin: 0;
|
||||
.archive-world__controls button {
|
||||
min-height: 42px;
|
||||
padding: 0.5rem 0.8rem;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.rpg-stats div {
|
||||
.archive-world__interact {
|
||||
min-width: 110px;
|
||||
min-height: 64px !important;
|
||||
}
|
||||
|
||||
.archive-world__status,
|
||||
.archive-world__instructions {
|
||||
margin: 0;
|
||||
padding: 0.65rem 1rem;
|
||||
color: #f5e4b7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.archive-world__status {
|
||||
border-top: 1px solid rgba(245, 228, 183, 0.16);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.archive-world__instructions {
|
||||
padding-top: 0;
|
||||
color: #cbbd9d;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.archive-world__dialog {
|
||||
width: min(92vw, 620px);
|
||||
max-height: 86vh;
|
||||
overflow: auto;
|
||||
border: 2px solid var(--world-line);
|
||||
border-radius: 10px;
|
||||
padding: 1.2rem;
|
||||
background:
|
||||
linear-gradient(rgba(255, 255, 255, 0.45), transparent 50%),
|
||||
var(--world-panel);
|
||||
color: #2b2116;
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.46);
|
||||
}
|
||||
|
||||
.archive-world__dialog::backdrop {
|
||||
background: rgba(15, 24, 19, 0.78);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.archive-world__dialog h2 {
|
||||
margin-top: 0.25rem;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
}
|
||||
|
||||
.archive-world__dialog form,
|
||||
.archive-world__dialog label,
|
||||
.archive-world__dialog fieldset {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.archive-world__dialog input[type="text"] {
|
||||
width: 100%;
|
||||
border: 1px solid var(--world-line);
|
||||
border-radius: 6px;
|
||||
padding: 0.7rem;
|
||||
background: var(--world-paper);
|
||||
color: #2b2116;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.archive-world__dialog fieldset {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
border: 1px solid #b79b70;
|
||||
}
|
||||
|
||||
.archive-world__dialog fieldset label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid rgba(139, 211, 255, 0.16);
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rpg-stats dt,
|
||||
.rpg-list h2 {
|
||||
margin: 0;
|
||||
color: var(--ash-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
.archive-world__dialog button {
|
||||
padding: 0.65rem 0.85rem;
|
||||
}
|
||||
|
||||
.rpg-stats dd {
|
||||
.palette-swatch {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #392e22;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.palette-swatch--brass { background: #f1c46f; }
|
||||
.palette-swatch--moss { background: #91c788; }
|
||||
.palette-swatch--berry { background: #d894ad; }
|
||||
|
||||
.archive-world__error {
|
||||
min-height: 1.4em;
|
||||
margin: 0;
|
||||
color: #8b2635;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.rpg-list ul {
|
||||
.archive-world__preview-links {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin: 0.45rem 0 0;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.archive-world__preview-links a {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--world-line);
|
||||
border-radius: 6px;
|
||||
background: var(--world-paper);
|
||||
color: #3a2b18;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.archive-world__preview-links a:hover,
|
||||
.archive-world__preview-links a:focus-visible {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(82, 118, 87, 0.25);
|
||||
}
|
||||
|
||||
.archive-world__journal {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.rpg-list li {
|
||||
padding: 0.55rem;
|
||||
border: 1px solid rgba(139, 211, 255, 0.16);
|
||||
border-radius: 6px;
|
||||
background: rgba(5, 7, 12, 0.62);
|
||||
color: var(--ash-text);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.rpg-list li.is-done {
|
||||
border-color: rgba(102, 199, 162, 0.42);
|
||||
background: rgba(45, 91, 77, 0.34);
|
||||
}
|
||||
|
||||
.rpg-list li.is-erased {
|
||||
border-color: rgba(219, 79, 98, 0.32);
|
||||
color: rgba(248, 239, 224, 0.42);
|
||||
background: repeating-linear-gradient(90deg, rgba(219, 79, 98, 0.08), rgba(219, 79, 98, 0.08) 6px, rgba(5, 7, 12, 0.6) 6px, rgba(5, 7, 12, 0.6) 12px);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.ash-choicebar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.ash-choicebar button {
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.ash-memory-meter {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
color: var(--ash-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ash-memory-meter::after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 10px;
|
||||
border: 1px solid rgba(139, 211, 255, 0.24);
|
||||
border-radius: 999px;
|
||||
background: #05070c;
|
||||
}
|
||||
|
||||
.ash-memory-meter i {
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
right: auto;
|
||||
bottom: 1px;
|
||||
width: 0;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, var(--ash-cold), var(--ash-red));
|
||||
transition: width 220ms ease;
|
||||
}
|
||||
|
||||
.ash-name-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.5rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.ash-name-form label {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
color: var(--ash-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ash-name-form input {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(139, 211, 255, 0.24);
|
||||
border-radius: 6px;
|
||||
.archive-world__journal li {
|
||||
padding: 0.65rem;
|
||||
background: #05070c;
|
||||
color: var(--ash-text);
|
||||
font: inherit;
|
||||
border: 1px dashed #aa977b;
|
||||
border-radius: 5px;
|
||||
color: #756956;
|
||||
}
|
||||
|
||||
.ash-rpg-root.is-lake-awake .rpg-dialogue,
|
||||
.ash-rpg-root.is-lake-awake .rpg-panel {
|
||||
border-color: rgba(219, 79, 98, 0.36);
|
||||
.archive-world__journal li.is-discovered {
|
||||
border-style: solid;
|
||||
border-color: #638568;
|
||||
background: #e5efdc;
|
||||
color: #29452e;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ash-rpg-root.is-lake-awake .ash-rpg-canvas {
|
||||
box-shadow:
|
||||
0 0 0 4px #05070c,
|
||||
0 0 38px rgba(219, 79, 98, 0.22);
|
||||
.archive-world__directory {
|
||||
margin-top: 1.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.ash-rpg-root.is-ending .rpg-panel {
|
||||
box-shadow: 0 0 34px rgba(224, 164, 81, 0.16);
|
||||
.archive-world__directory summary {
|
||||
padding: 0.9rem 1rem;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.archive-world__directory-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.8rem;
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.archive-world__directory-grid section {
|
||||
padding: 0.8rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.archive-world__directory-grid h2 {
|
||||
margin: 0 0 0.45rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.archive-world__noscript {
|
||||
padding: 0.8rem;
|
||||
border-left: 4px solid var(--world-brass);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.play-hero,
|
||||
.play-console,
|
||||
.recipe-tool,
|
||||
.sigil-tool,
|
||||
.rpg-shell {
|
||||
.sigil-tool {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -821,12 +813,48 @@
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.ash-choicebar,
|
||||
.ash-name-form {
|
||||
.archive-world__toolbar > div {
|
||||
min-width: 82px;
|
||||
}
|
||||
|
||||
.archive-world__toolbar button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.archive-world__game {
|
||||
min-height: 360px;
|
||||
max-height: 62vh;
|
||||
}
|
||||
|
||||
.archive-world__dialog fieldset,
|
||||
.archive-world__preview-links,
|
||||
.archive-world__directory-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ash-rpg-canvas {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.archive-world__game {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.archive-world__controls {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.archive-world__dpad {
|
||||
grid-template-columns: repeat(3, 42px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.archive-world *,
|
||||
.archive-world *::before,
|
||||
.archive-world *::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user