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:
@@ -274,7 +274,7 @@ The published HTML is static, but some browser modules rely on same-origin APIs:
|
|||||||
| Notes board | `pages/notes.js` | `/api/notes` |
|
| Notes board | `pages/notes.js` | `/api/notes` |
|
||||||
| Wird tracker | `pages/wird-tracker.js` | `/api/wird`, `/api/wird/motalah`, `/api/calibre/books` |
|
| Wird tracker | `pages/wird-tracker.js` | `/api/wird`, `/api/wird/motalah`, `/api/calibre/books` |
|
||||||
| Competency board | `pages/competency-status-board.js` | `/api/competencies/items...` |
|
| Competency board | `pages/competency-status-board.js` | `/api/competencies/items...` |
|
||||||
| RPG saves | `pages/play.js`, `pages/ash-below-lake.js` | `/api/play/rpg/save/:slot`, with local-storage fallback |
|
| Archive World progress | `pages/archive-world.js` | Device-local `localStorage` state |
|
||||||
|
|
||||||
These APIs are not implemented here. Production routing must serve `output/` and proxy `/api/` to the appropriate backend. When changing an endpoint contract, coordinate the static client and its external service.
|
These APIs are not implemented here. Production routing must serve `output/` and proxy `/api/` to the appropriate backend. When changing an endpoint contract, coordinate the static client and its external service.
|
||||||
|
|
||||||
|
|||||||
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);
|
populateMobileControls(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLevelTabs();
|
if (document.getElementById("kanban-board")) {
|
||||||
loadBoard();
|
renderLevelTabs();
|
||||||
|
loadBoard();
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
caption: "A wooden moon rolls beneath the cabinet whenever nobody is looking.",
|
caption: "A wooden moon rolls beneath the cabinet whenever nobody is looking.",
|
||||||
curated: [
|
curated: [
|
||||||
["Play hub", "/play/play.html"],
|
["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"]
|
["The Rain Index", "/play/the-rain-index.html"]
|
||||||
],
|
],
|
||||||
include: ["/play/"],
|
include: ["/play/"],
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
const root = $(".play-root[data-play-page]");
|
const root = $(".play-root[data-play-page]");
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
const page = root.dataset.playPage;
|
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);
|
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) {
|
function ink(root) {
|
||||||
const canvas = $("[data-ink-canvas]", root);
|
const canvas = $("[data-ink-canvas]", root);
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
@@ -492,353 +435,6 @@
|
|||||||
draw();
|
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) {
|
function setupSortable(container, _render, after) {
|
||||||
let dragged = null;
|
let dragged = null;
|
||||||
container.addEventListener("dragstart", (event) => {
|
container.addEventListener("dragstart", (event) => {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
if (!document.getElementById("wird-app")) return;
|
||||||
|
|
||||||
const API = "/api/wird";
|
const API = "/api/wird";
|
||||||
|
|
||||||
// type: "count" | "juz" | "min" | "rating" | "nafl" | "khatm"
|
// type: "count" | "juz" | "min" | "rating" | "nafl" | "khatm"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* Event listener for scrolling and changing the active label on the TOC */
|
/* Event listener for scrolling and changing the active label on the TOC */
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const toc = document.querySelector("#text-table-of-contents");
|
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.
|
const links = toc.querySelectorAll('a[href^="#"]'); // '^=' is a starts with operator.
|
||||||
// <a href="#introduction">Intro</a> matches
|
// <a href="#introduction">Intro</a> matches
|
||||||
if (!links.length) { console.warn("No ToC links found"); return; }
|
if (!links.length) return;
|
||||||
|
|
||||||
// Map: id -> link
|
// Map: id -> link
|
||||||
const linkById = new Map();
|
const linkById = new Map();
|
||||||
@@ -14,7 +14,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) linkById.set(id, a);
|
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)
|
// Headings to observe (h2–h4 usually)
|
||||||
const headings = [...document.querySelectorAll("h2[id], h3[id], h4[id]")]
|
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);
|
transform: rotate(-4deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookshelf,
|
.bookshelf {
|
||||||
.timeline-list {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-spine,
|
.book-spine {
|
||||||
.timeline-card {
|
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
@@ -350,54 +348,6 @@
|
|||||||
font-weight: 700;
|
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 {
|
.terminal-tool {
|
||||||
background: #15120e;
|
background: #15120e;
|
||||||
color: #f2e3bd;
|
color: #f2e3bd;
|
||||||
@@ -495,316 +445,358 @@
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-shell {
|
.archive-world {
|
||||||
display: grid;
|
--world-ink: #17251e;
|
||||||
grid-template-columns: minmax(0, 960px) minmax(280px, 1fr);
|
--world-panel: #f3ead2;
|
||||||
gap: 1rem;
|
--world-paper: #fff9e9;
|
||||||
align-items: start;
|
--world-line: #8b6841;
|
||||||
|
--world-brass: #d49a3a;
|
||||||
|
--world-moss: #527657;
|
||||||
|
--world-berry: #9d5268;
|
||||||
|
max-width: 1440px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ash-rpg-root {
|
.archive-world__heading {
|
||||||
--ash-black: #05070c;
|
max-width: 900px;
|
||||||
--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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ash-rpg-root .play-page-head {
|
.archive-world__shell {
|
||||||
padding: 1rem 0;
|
overflow: hidden;
|
||||||
border-bottom: 1px solid rgba(139, 211, 255, 0.18);
|
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 {
|
.archive-world__toolbar {
|
||||||
color: var(--ash-text);
|
display: flex;
|
||||||
font-family: Georgia, "Times New Roman", serif;
|
flex-wrap: wrap;
|
||||||
text-shadow: 0 0 18px rgba(139, 211, 255, 0.22);
|
gap: 0.65rem;
|
||||||
}
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
.ash-rpg-root .play-page-head p,
|
border-bottom: 2px solid #6e5738;
|
||||||
.ash-rpg-root .play-back {
|
|
||||||
color: var(--ash-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ash-rpg-shell {
|
|
||||||
position: relative;
|
|
||||||
border-color: rgba(139, 211, 255, 0.22);
|
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(12, 20, 32, 0.96), rgba(4, 7, 12, 0.98)),
|
linear-gradient(rgba(255, 255, 255, 0.04), transparent),
|
||||||
var(--ash-black);
|
#2d3f33;
|
||||||
box-shadow:
|
color: #fff6dc;
|
||||||
0 0 0 1px rgba(224, 164, 81, 0.14) inset,
|
|
||||||
0 28px 80px rgba(0, 0, 0, 0.42);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ash-rpg-shell::before {
|
.archive-world__toolbar > div {
|
||||||
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 {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
min-width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-canvas {
|
.archive-world__toolbar span {
|
||||||
display: block;
|
color: #d8c9a6;
|
||||||
width: 100%;
|
font-size: 0.68rem;
|
||||||
aspect-ratio: 16 / 9;
|
font-weight: 900;
|
||||||
image-rendering: pixelated;
|
letter-spacing: 0.08em;
|
||||||
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;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ash-rpg-root button:hover:not(:disabled),
|
.archive-world__toolbar button,
|
||||||
.ash-rpg-root button:focus-visible {
|
.archive-world__controls button,
|
||||||
background: linear-gradient(180deg, #31435f, #142236);
|
.archive-world__dialog button {
|
||||||
color: #ffffff;
|
border: 1px solid #d6b777;
|
||||||
box-shadow: 0 0 18px rgba(139, 211, 255, 0.22);
|
border-radius: 6px;
|
||||||
|
background: #fff3d2;
|
||||||
|
color: #2b2116;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ash-rpg-root button:disabled {
|
.archive-world__toolbar button {
|
||||||
cursor: not-allowed;
|
margin-left: auto;
|
||||||
opacity: 0.42;
|
padding: 0.55rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-mobile-pad {
|
.archive-world__toolbar button + button {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-mobile-pad [data-rpg-move="up"],
|
.archive-world__toolbar button:hover,
|
||||||
.rpg-mobile-pad [data-rpg-move="down"] {
|
.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;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-mobile-pad [data-rpg-move="left"] {
|
.archive-world__dpad [data-world-move="left"] {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-mobile-pad [data-rpg-act] {
|
.archive-world__dpad [data-world-move="down"] {
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-mobile-pad [data-rpg-move="right"] {
|
.archive-world__dpad [data-world-move="right"] {
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-stats {
|
.archive-world__controls button {
|
||||||
display: grid;
|
min-height: 42px;
|
||||||
gap: 0.55rem;
|
padding: 0.5rem 0.8rem;
|
||||||
margin: 0;
|
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;
|
display: flex;
|
||||||
justify-content: space-between;
|
gap: 0.4rem;
|
||||||
gap: 1rem;
|
align-items: center;
|
||||||
padding-bottom: 0.45rem;
|
|
||||||
border-bottom: 1px solid rgba(139, 211, 255, 0.16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-stats dt,
|
.archive-world__dialog button {
|
||||||
.rpg-list h2 {
|
padding: 0.65rem 0.85rem;
|
||||||
margin: 0;
|
|
||||||
color: var(--ash-muted);
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 800;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
margin: 0;
|
||||||
|
color: #8b2635;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-list ul {
|
.archive-world__preview-links {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.45rem;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
margin: 0.45rem 0 0;
|
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;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-list li {
|
.archive-world__journal 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;
|
|
||||||
padding: 0.65rem;
|
padding: 0.65rem;
|
||||||
background: #05070c;
|
border: 1px dashed #aa977b;
|
||||||
color: var(--ash-text);
|
border-radius: 5px;
|
||||||
font: inherit;
|
color: #756956;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ash-rpg-root.is-lake-awake .rpg-dialogue,
|
.archive-world__journal li.is-discovered {
|
||||||
.ash-rpg-root.is-lake-awake .rpg-panel {
|
border-style: solid;
|
||||||
border-color: rgba(219, 79, 98, 0.36);
|
border-color: #638568;
|
||||||
|
background: #e5efdc;
|
||||||
|
color: #29452e;
|
||||||
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ash-rpg-root.is-lake-awake .ash-rpg-canvas {
|
.archive-world__directory {
|
||||||
box-shadow:
|
margin-top: 1.5rem;
|
||||||
0 0 0 4px #05070c,
|
border: 1px solid var(--border);
|
||||||
0 0 38px rgba(219, 79, 98, 0.22);
|
border-radius: 8px;
|
||||||
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ash-rpg-root.is-ending .rpg-panel {
|
.archive-world__directory summary {
|
||||||
box-shadow: 0 0 34px rgba(224, 164, 81, 0.16);
|
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) {
|
@media (max-width: 760px) {
|
||||||
.play-hero,
|
.play-hero,
|
||||||
.play-console,
|
.play-console,
|
||||||
.recipe-tool,
|
.sigil-tool {
|
||||||
.sigil-tool,
|
|
||||||
.rpg-shell {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,12 +813,48 @@
|
|||||||
min-height: 160px;
|
min-height: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ash-choicebar,
|
.archive-world__toolbar > div {
|
||||||
.ash-name-form {
|
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;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -701,3 +701,62 @@ at <ScriptBlock>, /home/zaine/master-folder/org-platform/org_web/gitea-build-mon
|
|||||||
2026-07-21T01:00:59.4587832+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
2026-07-21T01:00:59.4587832+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
2026-07-21T01:00:59.5350433+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
2026-07-21T01:00:59.5350433+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
2026-07-21T01:00:59.9886800+01:00 [INFO] Sent authoring server test notification.
|
2026-07-21T01:00:59.9886800+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-21T12:09:03.6175899+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-21T12:09:03.6252048+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-21T12:09:04.4499267+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
2026-07-21T12:09:04.4697653+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
|
2026-07-21T12:09:04.5483233+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
|
2026-07-21T12:09:04.8706027+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-22T01:01:24.7920497+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-22T01:01:24.8001389+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-22T01:01:26.7297805+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
2026-07-22T01:01:26.7559529+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
|
2026-07-22T01:01:26.9028910+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
|
2026-07-22T01:01:27.2365854+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-23T01:01:18.8035310+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-23T01:01:18.8120210+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-23T01:01:20.0831461+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
2026-07-23T01:01:20.1060772+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
|
2026-07-23T01:01:20.1887487+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
|
2026-07-23T01:01:20.4711305+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-24T01:01:27.2870874+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-24T01:01:27.2947406+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-24T01:01:28.1121881+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
2026-07-24T01:01:28.1314531+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
|
2026-07-24T01:01:28.2017989+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
|
2026-07-24T01:01:29.3171443+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-25T01:01:33.9981810+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-25T01:01:34.0073830+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-25T01:01:35.2771459+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
2026-07-25T01:01:35.3069751+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
|
2026-07-25T01:01:35.3949964+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
|
2026-07-25T01:01:35.7310924+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-26T01:01:30.0185478+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-26T01:01:30.0259875+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-26T01:01:31.0690727+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
2026-07-26T01:01:31.0895824+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
|
2026-07-26T01:01:31.1699179+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
|
2026-07-26T01:01:31.7304381+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-27T01:01:33.9108228+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-27T01:01:33.9181628+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-27T01:01:35.1163859+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
2026-07-27T01:01:35.1367747+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
|
2026-07-27T01:01:35.2133300+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
|
2026-07-27T01:01:35.5108202+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-28T01:01:29.1770646+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-28T01:01:29.1847179+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-28T01:01:30.7324900+01:00 [INFO] Sent build status notification with 1 embed(s).
|
||||||
|
2026-07-28T01:01:30.7520630+01:00 [INFO] Running authoring server tests with /home/zaine/master-folder/org-platform/authoring-service/.venv/bin/python.
|
||||||
|
2026-07-28T01:01:30.8305107+01:00 [INFO] Authoring server tests passed: ran=0, failures=0, errors=0, skipped=0, exit=0
|
||||||
|
2026-07-28T01:01:31.2158991+01:00 [INFO] Sent authoring server test notification.
|
||||||
|
2026-07-29T01:02:37.1096593+01:00 [INFO] Prepared current build status for zaine/org_web: severity=Info, status=success
|
||||||
|
2026-07-29T01:02:37.1171545+01:00 [INFO] Fetching recent Gitea Actions runs for zaine/org_web.
|
||||||
|
2026-07-29T01:02:53.6950255+01:00 [WARN] Discord webhook failed, attempt 1/3: Resource temporarily unavailable (discord.com:443)
|
||||||
|
2026-07-29T01:03:06.9463075+01:00 [WARN] Discord webhook failed, attempt 2/3: Resource temporarily unavailable (discord.com:443)
|
||||||
|
2026-07-29T01:03:22.3057775+01:00 [WARN] Discord webhook failed, attempt 3/3: Resource temporarily unavailable (discord.com:443)
|
||||||
|
2026-07-29T01:03:22.3313219+01:00 [ERROR] Build monitor failed: Resource temporarily unavailable (discord.com:443)
|
||||||
|
2026-07-29T01:03:22.3415198+01:00 [ERROR] Exception type: System.Net.Http.HttpRequestException
|
||||||
|
2026-07-29T01:03:22.3472157+01:00 [ERROR] Stack trace: at Invoke-DiscordWebhook, /home/zaine/master-folder/org-platform/org_web/build-logs/gitea-build-monitor.ps1: line 179
|
||||||
|
at Send-BuildStatusNotification, /home/zaine/master-folder/org-platform/org_web/build-logs/gitea-build-monitor.ps1: line 985
|
||||||
|
at Start-BuildMonitor, /home/zaine/master-folder/org-platform/org_web/build-logs/gitea-build-monitor.ps1: line 1262
|
||||||
|
at <ScriptBlock>, /home/zaine/master-folder/org-platform/org_web/build-logs/gitea-build-monitor.ps1: line 1287
|
||||||
|
|||||||
320
docs/rpg.md
320
docs/rpg.md
@@ -1,320 +0,0 @@
|
|||||||
## Title: **Ash Below the Lake**
|
|
||||||
|
|
||||||
### Premise
|
|
||||||
|
|
||||||
A quiet town sits beside a black lake that never reflects the sky.
|
|
||||||
|
|
||||||
Every year, one person disappears into the water.
|
|
||||||
|
|
||||||
Nobody talks about it.
|
|
||||||
|
|
||||||
You play as a teenager named **Ilyas**, who arrives in the isolated town of **Morrow’s End** after receiving a letter from a sibling who vanished there years ago. The letter contains only one sentence:
|
|
||||||
|
|
||||||
> “Don’t let the lake remember your name.”
|
|
||||||
|
|
||||||
The game begins normally — odd townsfolk, quirky humor, strange shops — but slowly becomes darker as the player realizes the town itself is alive in a subtle, dreamlike way.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Core Themes
|
|
||||||
|
|
||||||
* Memory vs identity
|
|
||||||
* Grief and denial
|
|
||||||
* The fear of being forgotten
|
|
||||||
* Whether pain should be erased or accepted
|
|
||||||
|
|
||||||
Like *Undertale*, the game balances:
|
|
||||||
|
|
||||||
* humor
|
|
||||||
* emotional character writing
|
|
||||||
* eerie atmosphere
|
|
||||||
* choices that genuinely matter
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# The World
|
|
||||||
|
|
||||||
## Morrow’s End
|
|
||||||
|
|
||||||
A sleepy lakeside town with:
|
|
||||||
|
|
||||||
* crooked houses
|
|
||||||
* underground tunnels
|
|
||||||
* abandoned train stations
|
|
||||||
* old flood barriers
|
|
||||||
* glowing flowers near the shore
|
|
||||||
|
|
||||||
The town is split into districts:
|
|
||||||
|
|
||||||
1. **Harbor Row** – fishermen, markets, friendly NPCs
|
|
||||||
2. **The Red Streets** – entertainment district with unsettling performers
|
|
||||||
3. **The Roots** – underground caverns beneath the town
|
|
||||||
4. **The Drowned Chapel** – sunken cathedral inside the lake
|
|
||||||
5. **The Hollow Shore** – final area where reality begins collapsing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Main Gameplay Twist
|
|
||||||
|
|
||||||
The lake “remembers” people.
|
|
||||||
|
|
||||||
If someone is forgotten by everybody, they slowly become erased from reality.
|
|
||||||
|
|
||||||
Not dead.
|
|
||||||
|
|
||||||
Erased.
|
|
||||||
|
|
||||||
Photos change.
|
|
||||||
Dialogue changes.
|
|
||||||
Rooms disappear.
|
|
||||||
Music distorts.
|
|
||||||
|
|
||||||
As the player progresses, NPCs begin forgetting characters the player met earlier.
|
|
||||||
|
|
||||||
Eventually the player questions:
|
|
||||||
|
|
||||||
* Is the town cursed?
|
|
||||||
* Or is the lake protecting people from painful memories?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Main Characters
|
|
||||||
|
|
||||||
## Ilyas (Player)
|
|
||||||
|
|
||||||
Quiet protagonist searching for their sibling.
|
|
||||||
|
|
||||||
The player can shape Ilyas:
|
|
||||||
|
|
||||||
* compassionate
|
|
||||||
* detached
|
|
||||||
* manipulative
|
|
||||||
* fearful
|
|
||||||
|
|
||||||
Choices affect dialogue, endings, and combat resolution.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mira
|
|
||||||
|
|
||||||
A sarcastic runaway girl who joins you early.
|
|
||||||
|
|
||||||
She uses humor to hide panic and becomes emotionally attached to the player.
|
|
||||||
|
|
||||||
She remembers things others forget.
|
|
||||||
|
|
||||||
That should be impossible.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The Ferryman
|
|
||||||
|
|
||||||
A masked boatman who appears throughout the game.
|
|
||||||
|
|
||||||
He speaks in riddles and always knows where the player has been.
|
|
||||||
|
|
||||||
Sometimes he appears before important choices.
|
|
||||||
|
|
||||||
He may not be human.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Brother Cinder
|
|
||||||
|
|
||||||
Leader of the Drowned Chapel.
|
|
||||||
|
|
||||||
Believes forgetting pain is mercy.
|
|
||||||
|
|
||||||
Wants to feed memories to the lake so humanity can become “peaceful.”
|
|
||||||
|
|
||||||
Not purely evil — deeply tragic.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Your Sibling
|
|
||||||
|
|
||||||
You spend the whole game searching for them.
|
|
||||||
|
|
||||||
Late-game reveal:
|
|
||||||
They willingly entered the lake.
|
|
||||||
|
|
||||||
And they may no longer fully exist.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Combat System
|
|
||||||
|
|
||||||
Similar vibe to *Undertale*:
|
|
||||||
|
|
||||||
* turn-based encounters
|
|
||||||
* bullet-hell dodging
|
|
||||||
* dialogue options during fights
|
|
||||||
|
|
||||||
But instead of MERCY/FIGHT:
|
|
||||||
|
|
||||||
## Three Main Actions
|
|
||||||
|
|
||||||
* **Confront** — direct resistance
|
|
||||||
* **Listen** — understand enemies emotionally
|
|
||||||
* **Forget** — erase enemies from memory
|
|
||||||
|
|
||||||
Using “Forget” makes encounters easier…
|
|
||||||
|
|
||||||
…but causes side effects in the world.
|
|
||||||
|
|
||||||
NPCs disappear.
|
|
||||||
Music tracks vanish.
|
|
||||||
Areas become inaccessible.
|
|
||||||
|
|
||||||
The player slowly realizes:
|
|
||||||
They are becoming part of the lake.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Major Story Beats
|
|
||||||
|
|
||||||
## Act 1 — Strange Town
|
|
||||||
|
|
||||||
* Meet Mira
|
|
||||||
* Discover townsfolk avoid speaking certain names
|
|
||||||
* Encounter first erased person
|
|
||||||
* Learn your sibling stayed at the old chapel
|
|
||||||
|
|
||||||
Tone:
|
|
||||||
cozy + unsettling
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Act 2 — The Roots
|
|
||||||
|
|
||||||
You descend beneath the town.
|
|
||||||
|
|
||||||
Find:
|
|
||||||
|
|
||||||
* memory archives
|
|
||||||
* abandoned homes nobody remembers
|
|
||||||
* creatures made from forgotten people
|
|
||||||
|
|
||||||
The player discovers:
|
|
||||||
The lake feeds on emotional suffering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Act 3 — The Drowned Chapel
|
|
||||||
|
|
||||||
Brother Cinder reveals the truth:
|
|
||||||
The lake was created long ago to contain humanity’s collective grief.
|
|
||||||
|
|
||||||
Without it:
|
|
||||||
people would drown in their pain.
|
|
||||||
|
|
||||||
But now it has grown hungry.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Final Act — The Hollow Shore
|
|
||||||
|
|
||||||
Reality breaks down:
|
|
||||||
|
|
||||||
* menus glitch
|
|
||||||
* save points speak
|
|
||||||
* NPC dialogue rewrites itself
|
|
||||||
* your own name occasionally changes
|
|
||||||
|
|
||||||
The player must choose:
|
|
||||||
|
|
||||||
### Ending 1 — “Silence”
|
|
||||||
|
|
||||||
Join the lake.
|
|
||||||
All pain disappears.
|
|
||||||
The town becomes peaceful… empty.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Ending 2 — “Carry”
|
|
||||||
|
|
||||||
Destroy the lake.
|
|
||||||
Everyone regains painful memories.
|
|
||||||
The town survives but suffers.
|
|
||||||
|
|
||||||
Hopeful but bittersweet.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Ending 3 — “Forgotten”
|
|
||||||
|
|
||||||
Use “Forget” too often.
|
|
||||||
|
|
||||||
Eventually:
|
|
||||||
the protagonist themselves is erased.
|
|
||||||
|
|
||||||
The final scene shows the game continuing without you.
|
|
||||||
|
|
||||||
No one remembers the player existed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Tone & Style
|
|
||||||
|
|
||||||
Visual Style:
|
|
||||||
|
|
||||||
* pixel art
|
|
||||||
* heavy shadows
|
|
||||||
* warm interiors vs cold outdoor areas
|
|
||||||
* subtle environmental animations
|
|
||||||
|
|
||||||
Music:
|
|
||||||
|
|
||||||
* soft piano
|
|
||||||
* detuned music-box melodies
|
|
||||||
* distorted ambient tracks late-game
|
|
||||||
|
|
||||||
Humor:
|
|
||||||
|
|
||||||
* weird NPC dialogue
|
|
||||||
* absurd side quests
|
|
||||||
* fake advertisements
|
|
||||||
* awkward interactions
|
|
||||||
|
|
||||||
The humor makes the emotional moments hit harder.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Extra Mechanics
|
|
||||||
|
|
||||||
## Memory Journal
|
|
||||||
|
|
||||||
Instead of a quest log, the player keeps memories.
|
|
||||||
|
|
||||||
Entries can:
|
|
||||||
|
|
||||||
* change
|
|
||||||
* disappear
|
|
||||||
* rewrite themselves
|
|
||||||
|
|
||||||
The player cannot always trust it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Name System
|
|
||||||
|
|
||||||
NPCs react differently depending on whether you tell them your real name.
|
|
||||||
|
|
||||||
Some characters literally gain power if they know it.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Optional Secret Route
|
|
||||||
|
|
||||||
If the player never uses “Forget” and resolves conflicts peacefully:
|
|
||||||
|
|
||||||
* hidden areas appear
|
|
||||||
* erased characters return temporarily
|
|
||||||
* Mira reveals she died years ago and is only remembered because of you
|
|
||||||
|
|
||||||
This leads to the true ending:
|
|
||||||
The lake is not destroyed.
|
|
||||||
|
|
||||||
It is forgiven.
|
|
||||||
|
|
||||||
And finally allowed to rest.
|
|
||||||
@@ -61,7 +61,6 @@
|
|||||||
"pages/home-dashboard.js"
|
"pages/home-dashboard.js"
|
||||||
"pages/play.js"
|
"pages/play.js"
|
||||||
"pages/house.js"
|
"pages/house.js"
|
||||||
"pages/ash-below-lake.js"
|
|
||||||
"features/hidden-details.js")
|
"features/hidden-details.js")
|
||||||
"\n")
|
"\n")
|
||||||
(format "<script src=\"%s\"></script>"
|
(format "<script src=\"%s\"></script>"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
("org-assets"
|
("org-assets"
|
||||||
:base-directory ,(site-path "assets/")
|
:base-directory ,(site-path "assets/")
|
||||||
:base-extension "css\\|js\\|png\\|jpg\\|jpeg\\|gif\\|webp\\|svg\\|pdf\\|mp4\\|webm\\|mov\\|woff\\|woff2\\|ttf"
|
:base-extension "css\\|js\\|png\\|jpg\\|jpeg\\|gif\\|webp\\|svg\\|pdf\\|mp4\\|webm\\|mov\\|ogg\\|wav\\|woff\\|woff2\\|ttf"
|
||||||
:publishing-directory ,(output-path "assets/")
|
:publishing-directory ,(output-path "assets/")
|
||||||
:recursive t
|
:recursive t
|
||||||
:publishing-function org-publish-attachment)
|
:publishing-function org-publish-attachment)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
<section><h2>Study</h2><a href="/posts/career/career-list.html">Career</a> · <a href="/home/status.html">Competency status</a></section>
|
<section><h2>Study</h2><a href="/posts/career/career-list.html">Career</a> · <a href="/home/status.html">Competency status</a></section>
|
||||||
<section><h2>Kitchen</h2><a href="/blogs/blogs-list.html">Blogs</a> · <a href="/tags/review.html">Weekly reviews</a></section>
|
<section><h2>Kitchen</h2><a href="/blogs/blogs-list.html">Blogs</a> · <a href="/tags/review.html">Weekly reviews</a></section>
|
||||||
<section><h2>Workshop</h2><a href="/home/services.html">Services</a> · <a href="/home/wird-tracker.html">Wird tracker</a> · <a href="/home/backlog.html">Backlog</a></section>
|
<section><h2>Workshop</h2><a href="/home/services.html">Services</a> · <a href="/home/wird-tracker.html">Wird tracker</a> · <a href="/home/backlog.html">Backlog</a></section>
|
||||||
<section><h2>Playroom</h2><a href="/play/play.html">Play</a> · <a href="/play/rpg.html">Ash Below the Lake</a></section>
|
<section><h2>Playroom</h2><a href="/play/play.html">Play</a> · <a href="/play/rpg.html">The Archive World</a></section>
|
||||||
<section><h2>Attic</h2><a href="/lima/index.html">Lima archive</a> · <a href="/blogs/blogs-list.html">Older writing</a></section>
|
<section><h2>Attic</h2><a href="/lima/index.html">Lima archive</a> · <a href="/blogs/blogs-list.html">Older writing</a></section>
|
||||||
<section><h2>Garden</h2><a href="/home/notes.html">Notes wall</a> · <a href="/home/categories.html">Categories</a> · <a href="/sitemap.html">Sitemap</a></section>
|
<section><h2>Garden</h2><a href="/home/notes.html">Notes wall</a> · <a href="/home/categories.html">Categories</a> · <a href="/sitemap.html">Sitemap</a></section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,8 +23,6 @@
|
|||||||
<button class="play-orbit__node" type="button" data-index="8">9</button>
|
<button class="play-orbit__node" type="button" data-index="8">9</button>
|
||||||
<button class="play-orbit__node" type="button" data-index="9">10</button>
|
<button class="play-orbit__node" type="button" data-index="9">10</button>
|
||||||
<button class="play-orbit__node" type="button" data-index="10">11</button>
|
<button class="play-orbit__node" type="button" data-index="10">11</button>
|
||||||
<button class="play-orbit__node" type="button" data-index="11">12</button>
|
|
||||||
<button class="play-orbit__node" type="button" data-index="12">13</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -47,15 +45,13 @@
|
|||||||
<a href="/play/constellation.html" data-kind="Sketch" data-desc="Draw a star map by connecting points, then name the constellation."><span>02</span><strong>Constellation Desk</strong></a>
|
<a href="/play/constellation.html" data-kind="Sketch" data-desc="Draw a star map by connecting points, then name the constellation."><span>02</span><strong>Constellation Desk</strong></a>
|
||||||
<a href="/play/poem.html" data-kind="Generator" data-desc="Compose tiny academic marginalia with a hand-cranked phrase machine."><span>03</span><strong>Marginalia Machine</strong></a>
|
<a href="/play/poem.html" data-kind="Generator" data-desc="Compose tiny academic marginalia with a hand-cranked phrase machine."><span>03</span><strong>Marginalia Machine</strong></a>
|
||||||
<a href="/play/bookshelf.html" data-kind="Puzzle" data-desc="Sort a shelf of invented books into a pleasing order before the bell."><span>04</span><strong>Bookshelf Sort</strong></a>
|
<a href="/play/bookshelf.html" data-kind="Puzzle" data-desc="Sort a shelf of invented books into a pleasing order before the bell."><span>04</span><strong>Bookshelf Sort</strong></a>
|
||||||
<a href="/play/recipe.html" data-kind="Spinner" data-desc="Spin a family supper wheel and collect a playful menu."><span>05</span><strong>Supper Wheel</strong></a>
|
<a href="/play/ink.html" data-kind="Canvas" data-desc="Make a living ink pond with ripples, trails, and quiet motion."><span>05</span><strong>Ink Pond</strong></a>
|
||||||
<a href="/play/timeline.html" data-kind="Puzzle" data-desc="Drag family-site milestones into chronological order."><span>06</span><strong>Timeline Tangle</strong></a>
|
<a href="/play/terminal.html" data-kind="Story" data-desc="Explore a tiny command-line adventure hidden in the archive."><span>06</span><strong>Archive Terminal</strong></a>
|
||||||
<a href="/play/ink.html" data-kind="Canvas" data-desc="Make a living ink pond with ripples, trails, and quiet motion."><span>07</span><strong>Ink Pond</strong></a>
|
<a href="/play/study.html" data-kind="Timer" data-desc="Run a focus timer that grows a little desk scene as time passes."><span>07</span><strong>Study Lamp</strong></a>
|
||||||
<a href="/play/terminal.html" data-kind="Story" data-desc="Explore a tiny command-line adventure hidden in the archive."><span>08</span><strong>Archive Terminal</strong></a>
|
<a href="/play/sigil.html" data-kind="Maker" data-desc="Generate a small personal sigil from initials, colors, and motto."><span>08</span><strong>Sigil Press</strong></a>
|
||||||
<a href="/play/study.html" data-kind="Timer" data-desc="Run a focus timer that grows a little desk scene as time passes."><span>09</span><strong>Study Lamp</strong></a>
|
<a href="/play/rpg.html" data-kind="RPG world" data-desc="Walk through the website as Archive Town, discover eight landmarks, and travel through its living sections."><span>09</span><strong>The Archive World</strong></a>
|
||||||
<a href="/play/sigil.html" data-kind="Maker" data-desc="Generate a small personal sigil from initials, colors, and motto."><span>10</span><strong>Sigil Press</strong></a>
|
<a href="/play/the-rain-index.html" data-kind="Artifact" data-desc="Handle impossible paper rooms, type forgotten words, and let a rainy archive file you back."><span>10</span><strong>The Rain Index</strong></a>
|
||||||
<a href="/play/rpg.html" data-kind="RPG" data-desc="Enter Morrow's End, survive memory battles, choose whether to confront, listen, or forget, and decide the lake's fate."><span>11</span><strong>Ash Below the Lake</strong></a>
|
<a href="/play/house.html" data-kind="Living archive" data-desc="Wander through a lived-in house where every room opens another part of the archive."><span>11</span><strong>The House of Pages</strong></a>
|
||||||
<a href="/play/the-rain-index.html" data-kind="Artifact" data-desc="Handle impossible paper rooms, type forgotten words, and let a rainy archive file you back."><span>12</span><strong>The Rain Index</strong></a>
|
|
||||||
<a href="/play/house.html" data-kind="Living archive" data-desc="Wander through a lived-in house where every room opens another part of the archive."><span>13</span><strong>The House of Pages</strong></a>
|
|
||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
#+END_EXPORT
|
#+END_EXPORT
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
#+TITLE: Supper Wheel
|
|
||||||
#+OPTIONS: num:nil title:nil toc:nil
|
|
||||||
#+NO_SIDENOTES: t
|
|
||||||
#+DATE: <2026-05-09 Sat>
|
|
||||||
|
|
||||||
#+BEGIN_EXPORT html
|
|
||||||
<main class="play-root play-page" data-play-page="recipe">
|
|
||||||
<a class="play-back" href="/play/play.html">Back to Play</a>
|
|
||||||
<header class="play-page-head">
|
|
||||||
<p class="play-kicker">05 / Spinner</p>
|
|
||||||
<h1>Supper Wheel</h1>
|
|
||||||
<p>Spin for a cheerful menu: base, main, side, and table note.</p>
|
|
||||||
</header>
|
|
||||||
<section class="play-tool recipe-tool">
|
|
||||||
<button class="recipe-wheel" type="button" data-recipe-spin aria-label="Spin supper wheel">Spin</button>
|
|
||||||
<ul class="recipe-result" data-recipe-result></ul>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
#+END_EXPORT
|
|
||||||
147
play/rpg.org
Executable file → Normal file
147
play/rpg.org
Executable file → Normal file
@@ -1,61 +1,116 @@
|
|||||||
#+TITLE: Ash Below the Lake
|
#+TITLE: The Archive World
|
||||||
#+OPTIONS: num:nil title:nil toc:nil
|
#+OPTIONS: num:nil title:nil toc:nil
|
||||||
#+NO_SIDENOTES: t
|
#+NO_SIDENOTES: t
|
||||||
#+DATE: <2026-05-09 Sat>
|
#+DATE: <2026-07-29 Wed>
|
||||||
|
|
||||||
#+BEGIN_EXPORT html
|
#+BEGIN_EXPORT html
|
||||||
<main class="play-root play-page rpg-root ash-rpg-root" data-play-page="rpg">
|
<main class="play-root play-page archive-world" data-play-page="archive-world">
|
||||||
<a class="play-back" href="/play/play.html">Back to Play</a>
|
<a class="play-back" href="/play/play.html">Back to Play</a>
|
||||||
<header class="play-page-head">
|
|
||||||
<p class="play-kicker">11 / Role Playing Game</p>
|
<header class="play-page-head archive-world__heading">
|
||||||
<h1>Ash Below the Lake</h1>
|
<p class="play-kicker">09 / Role Playing World</p>
|
||||||
<p>A 2D story RPG set beside a black lake that remembers names, grief, and everyone the world tries to forget.</p>
|
<h1>The Archive World</h1>
|
||||||
|
<p>Walk through the website as a living town. Every lit doorway opens another part of the archive.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="play-tool rpg-shell ash-rpg-shell" aria-label="Ash Below the Lake game">
|
<section class="archive-world__shell" aria-labelledby="archive-world-title">
|
||||||
<div class="rpg-stage">
|
<div class="archive-world__toolbar">
|
||||||
<canvas class="rpg-canvas ash-rpg-canvas" width="960" height="540" data-rpg-canvas></canvas>
|
<div>
|
||||||
<div class="rpg-dialogue" data-rpg-dialogue aria-live="polite">
|
<span>Traveler</span>
|
||||||
<strong data-rpg-speaker>The Letter</strong>
|
<strong data-world-player>New arrival</strong>
|
||||||
<p data-rpg-line>Don't let the lake remember your name.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Landmarks</span>
|
||||||
|
<strong data-world-discovery-count>0 / 8</strong>
|
||||||
|
</div>
|
||||||
|
<p class="archive-world__badge" data-world-badge hidden>Archive Cartographer</p>
|
||||||
|
<button type="button" data-world-journal-open>Discovery journal</button>
|
||||||
|
<button type="button" data-world-sound>Sound: muted</button>
|
||||||
|
<button type="button" data-world-reset>Reset traveler</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="rpg-panel" aria-label="Game controls and progress">
|
<h2 id="archive-world-title" class="visually-hidden">Archive Town game</h2>
|
||||||
<div class="rpg-actions">
|
<div
|
||||||
<button type="button" data-rpg-start>Start</button>
|
id="archive-world-game"
|
||||||
<button type="button" data-rpg-save>Save</button>
|
class="archive-world__game"
|
||||||
<button type="button" data-rpg-load>Load</button>
|
tabindex="0"
|
||||||
<button type="button" data-rpg-reset>New Game</button>
|
role="application"
|
||||||
<button type="button" data-rpg-sound>Sound</button>
|
aria-label="The Archive World. Move with arrow keys or WASD and interact with E, Space, or Enter."
|
||||||
|
>
|
||||||
|
<p class="archive-world__loading">Preparing Archive Town…</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="archive-world__controls" aria-label="Touch game controls">
|
||||||
|
<div class="archive-world__dpad">
|
||||||
|
<button type="button" data-world-move="up" aria-label="Move up">↑</button>
|
||||||
|
<button type="button" data-world-move="left" aria-label="Move left">←</button>
|
||||||
|
<button type="button" data-world-move="down" aria-label="Move down">↓</button>
|
||||||
|
<button type="button" data-world-move="right" aria-label="Move right">→</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="archive-world__interact" type="button" data-world-interact>Explore</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rpg-mobile-pad" aria-label="Movement controls">
|
<p class="archive-world__status" data-world-status aria-live="polite">
|
||||||
<button type="button" data-rpg-move="up">Up</button>
|
Choose your traveler to enter Archive Town.
|
||||||
<button type="button" data-rpg-move="left">Left</button>
|
</p>
|
||||||
<button type="button" data-rpg-act>Act</button>
|
<p class="archive-world__instructions">
|
||||||
<button type="button" data-rpg-move="right">Right</button>
|
Move with arrow keys or WASD. Press E, Space, Enter, or Explore near a glowing landmark.
|
||||||
<button type="button" data-rpg-move="down">Down</button>
|
Sound remains muted until you enable it.
|
||||||
</div>
|
</p>
|
||||||
|
|
||||||
<dl class="rpg-stats">
|
|
||||||
<div><dt>District</dt><dd data-rpg-room>Harbor Row</dd></div>
|
|
||||||
<div><dt>Route</dt><dd data-rpg-route>Undecided</dd></div>
|
|
||||||
<div><dt>Resolve</dt><dd data-rpg-hearts>20</dd></div>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<section class="rpg-list">
|
|
||||||
<h2>Memory Journal</h2>
|
|
||||||
<ul data-rpg-quests></ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="rpg-list">
|
|
||||||
<h2>Inventory</h2>
|
|
||||||
<ul data-rpg-inventory></ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<p class="rpg-status" data-rpg-status>Move with arrows or WASD. Act with Space or Enter. Sound starts after your first input.</p>
|
|
||||||
</aside>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<dialog class="archive-world__dialog archive-world__setup" data-world-setup>
|
||||||
|
<form data-world-setup-form>
|
||||||
|
<p class="play-kicker">New traveler</p>
|
||||||
|
<h2>Who has arrived?</h2>
|
||||||
|
<label>
|
||||||
|
Traveler name
|
||||||
|
<input name="name" type="text" minlength="1" maxlength="20" autocomplete="nickname" required />
|
||||||
|
</label>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Coat palette</legend>
|
||||||
|
<label><input type="radio" name="palette" value="brass" checked /> <span class="palette-swatch palette-swatch--brass"></span> Brass</label>
|
||||||
|
<label><input type="radio" name="palette" value="moss" /> <span class="palette-swatch palette-swatch--moss"></span> Moss</label>
|
||||||
|
<label><input type="radio" name="palette" value="berry" /> <span class="palette-swatch palette-swatch--berry"></span> Berry</label>
|
||||||
|
</fieldset>
|
||||||
|
<p class="archive-world__error" data-world-setup-error aria-live="polite"></p>
|
||||||
|
<button type="submit">Enter the world</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<dialog class="archive-world__dialog" data-world-preview>
|
||||||
|
<p class="play-kicker" data-world-preview-eyebrow>Landmark</p>
|
||||||
|
<h2 data-world-preview-title>Archive Town</h2>
|
||||||
|
<p data-world-preview-description></p>
|
||||||
|
<nav class="archive-world__preview-links" data-world-preview-links aria-label="Landmark destinations"></nav>
|
||||||
|
<button type="button" data-world-preview-close>Return to town</button>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<dialog class="archive-world__dialog" data-world-journal>
|
||||||
|
<p class="play-kicker">Discovery journal</p>
|
||||||
|
<h2>Archive Town landmarks</h2>
|
||||||
|
<ul class="archive-world__journal" data-world-journal-list></ul>
|
||||||
|
<button type="button" data-world-journal-close>Close journal</button>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<details class="archive-world__directory">
|
||||||
|
<summary>Plain directory of Archive Town</summary>
|
||||||
|
<div class="archive-world__directory-grid">
|
||||||
|
<section><h2>Town Gate</h2><a href="/">Home</a> · <a href="/recently-updated.html">Recently updated</a> · <a href="/home/contact.html">Contact</a></section>
|
||||||
|
<section><h2>Grand Library</h2><a href="/posts/posts-list.html">Posts</a> · <a href="/home/categories.html">Categories</a> · <a href="/posts/posts-intro.html">Introduction</a></section>
|
||||||
|
<section><h2>Guild Study</h2><a href="/posts/career/career-list.html">Career</a> · <a href="/home/status.html">Competency status</a> · <a href="/posts/career/probation-objectives.html">Objectives</a></section>
|
||||||
|
<section><h2>Kitchen Inn</h2><a href="/blogs/blogs-list.html">Blogs</a> · <a href="/tags/review.html">Weekly reviews</a> · <a href="/blogs/blogs-intro.html">Introduction</a></section>
|
||||||
|
<section><h2>Workshop</h2><a href="/home/services.html">Services</a> · <a href="/home/wird-tracker.html">Wird tracker</a> · <a href="/home/countdown.html">Countdowns</a> · <a href="/home/backlog.html">Backlog</a></section>
|
||||||
|
<section><h2>Playroom</h2><a href="/play/play.html">Play</a> · <a href="/play/the-rain-index.html">Rain Index</a> · <a href="/play/house.html">House of Pages</a></section>
|
||||||
|
<section><h2>Lima Museum</h2><a href="/lima/index.html">Lima</a> · <a href="/blogs/2025/2025-list.html">Older writing</a> · <a href="/play/memory.html">Memory Cabinet</a></section>
|
||||||
|
<section><h2>Notes Garden</h2><a href="/home/notes.html">Notes wall</a> · <a href="/home/categories.html">Categories</a> · <a href="/recently-updated.html">Recently updated</a> · <a href="/sitemap.html">Sitemap</a></section>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<noscript><p class="archive-world__noscript">The game needs JavaScript, but every destination remains available in the plain directory above.</p></noscript>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="/assets/scripts/vendor/phaser-4.1.0.min.js" defer></script>
|
||||||
|
<script src="/assets/scripts/pages/archive-world-state.js" defer></script>
|
||||||
|
<script src="/assets/scripts/pages/archive-world.js" defer></script>
|
||||||
#+END_EXPORT
|
#+END_EXPORT
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
#+TITLE: Timeline Tangle
|
|
||||||
#+OPTIONS: num:nil title:nil toc:nil
|
|
||||||
#+NO_SIDENOTES: t
|
|
||||||
#+DATE: <2026-05-09 Sat>
|
|
||||||
|
|
||||||
#+BEGIN_EXPORT html
|
|
||||||
<main class="play-root play-page" data-play-page="timeline">
|
|
||||||
<a class="play-back" href="/play/play.html">Back to Play</a>
|
|
||||||
<header class="play-page-head">
|
|
||||||
<p class="play-kicker">06 / Puzzle</p>
|
|
||||||
<h1>Timeline Tangle</h1>
|
|
||||||
<p>Drag the cards from earliest to latest.</p>
|
|
||||||
</header>
|
|
||||||
<section class="play-tool">
|
|
||||||
<div class="play-scorebar">
|
|
||||||
<span data-timeline-status>Arrange the cards</span>
|
|
||||||
<button type="button" data-timeline-shuffle>Shuffle</button>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-list" data-timeline-list></div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
#+END_EXPORT
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||||
|
|
||||||
* Posts:
|
* Posts:
|
||||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">20-07-2026 01:00</span>@@
|
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">28-07-2026 01:01</span>@@
|
||||||
- [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:<span class="post-date">01-06-2026 10:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/cross-site-scripting-xss.org][Cross-Site Scripting (XSS)]] @@html:<span class="post-date">01-06-2026 10:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
@@ -24,4 +24,4 @@ See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
|||||||
- [[file:career/lean.org][Lean]] @@html:<span class="post-date">05-11-2025 20:46</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/lean.org][Lean]] @@html:<span class="post-date">05-11-2025 20:46</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/owasp.org][OWASP Top Ten]] @@html:<span class="post-date">19-10-2025 13:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/owasp.org][OWASP Top Ten]] @@html:<span class="post-date">19-10-2025 13:21</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/solid-principles.org][SOLID Principles]] @@html:<span class="post-date">18-10-2025 19:14</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/solid-principles.org][SOLID Principles]] @@html:<span class="post-date">18-10-2025 19:14</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:posts-intro.org][Posts Introduction]] @@html:<span class="post-date">06-08-2025 00:00</span>@@ @@html:<a href="/tags/introduction.html"><span class="post-tag">introduction</span></a>@@
|
- [[file:posts-intro.org][Posts Introduction]] @@html:<span class="post-date">06-08-2025 00:00</span>@@ @@html:<a href="/tags/introduction.html"><span class="post-tag">introduction</span></a>@@
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#+OPTIONS: toc:nil num:nil
|
#+OPTIONS: toc:nil num:nil
|
||||||
|
|
||||||
* Recently Updated (top 26 files)
|
* Recently Updated (top 26 files)
|
||||||
|
- [[file:play/rpg.org][The Archive World]] @@html:<span class="post-date">2026-07-29 00:00</span>@@
|
||||||
- [[file:play/house.org][The House of Pages]] @@html:<span class="post-date">2026-07-15 00:00</span>@@
|
- [[file:play/house.org][The House of Pages]] @@html:<span class="post-date">2026-07-15 00:00</span>@@
|
||||||
- [[file:blogs/2026/07-july/12-07-week-review.org][[12-07-2026] - Weekly Review]] @@html:<span class="post-date">2026-07-12 12:00</span>@@
|
- [[file:blogs/2026/07-july/12-07-week-review.org][[12-07-2026] - Weekly Review]] @@html:<span class="post-date">2026-07-12 12:00</span>@@
|
||||||
- [[file:home/wird-tracker.org][Wird Tracker]] @@html:<span class="post-date">2026-07-08 22:36</span>@@
|
- [[file:home/wird-tracker.org][Wird Tracker]] @@html:<span class="post-date">2026-07-08 22:36</span>@@
|
||||||
@@ -27,4 +28,3 @@
|
|||||||
- [[file:blogs/2026/05-may/poppadoms-num-13-05.org][Poppadoms num]] @@html:<span class="post-date">2026-05-13 23:29</span>@@
|
- [[file:blogs/2026/05-may/poppadoms-num-13-05.org][Poppadoms num]] @@html:<span class="post-date">2026-05-13 23:29</span>@@
|
||||||
- [[file:blogs/2026/05-may/using-gifs-13-05.org][Sans spins for us!]] @@html:<span class="post-date">2026-05-13 13:43</span>@@
|
- [[file:blogs/2026/05-may/using-gifs-13-05.org][Sans spins for us!]] @@html:<span class="post-date">2026-05-13 13:43</span>@@
|
||||||
- [[file:blogs/2026/05-may/back-to-back-meetings-13-05.org][Back to back meetings]] @@html:<span class="post-date">2026-05-13 13:18</span>@@
|
- [[file:blogs/2026/05-may/back-to-back-meetings-13-05.org][Back to back meetings]] @@html:<span class="post-date">2026-05-13 13:18</span>@@
|
||||||
- [[file:blogs/2026/05-may/memory-observatory-12-05.org][Memory Observatory]] @@html:<span class="post-date">2026-05-12 16:30</span>@@
|
|
||||||
|
|||||||
76
sitemap.org
76
sitemap.org
@@ -110,13 +110,13 @@ flowchart TD
|
|||||||
n46 --> n52
|
n46 --> n52
|
||||||
n53["Tag: website"]
|
n53["Tag: website"]
|
||||||
n46 --> n53
|
n46 --> n53
|
||||||
n54["Tag: life"]
|
n54["Tag: update"]
|
||||||
n46 --> n54
|
n46 --> n54
|
||||||
n55["Tag: update"]
|
n55["Tag: life"]
|
||||||
n46 --> n55
|
n46 --> n55
|
||||||
n56["Tag: insights"]
|
n56["Tag: education"]
|
||||||
n46 --> n56
|
n46 --> n56
|
||||||
n57["Tag: education"]
|
n57["Tag: insights"]
|
||||||
n46 --> n57
|
n46 --> n57
|
||||||
n58["Tag: reading"]
|
n58["Tag: reading"]
|
||||||
n46 --> n58
|
n46 --> n58
|
||||||
@@ -128,32 +128,28 @@ flowchart TD
|
|||||||
root --> n61
|
root --> n61
|
||||||
n62["Sigil Press"]
|
n62["Sigil Press"]
|
||||||
n61 --> n62
|
n61 --> n62
|
||||||
n63["Ash Below the Lake"]
|
n63["Ink Pond"]
|
||||||
n61 --> n63
|
n61 --> n63
|
||||||
n64["Timeline Tangle"]
|
n64["Bookshelf Sort"]
|
||||||
n61 --> n64
|
n61 --> n64
|
||||||
n65["Ink Pond"]
|
n65["Constellation Desk"]
|
||||||
n61 --> n65
|
n61 --> n65
|
||||||
n66["Supper Wheel"]
|
n66["Memory Cabinet"]
|
||||||
n61 --> n66
|
n61 --> n66
|
||||||
n67["Bookshelf Sort"]
|
n67["Play"]
|
||||||
n61 --> n67
|
n61 --> n67
|
||||||
n68["Constellation Desk"]
|
n68["Archive Terminal"]
|
||||||
n61 --> n68
|
n61 --> n68
|
||||||
n69["Memory Cabinet"]
|
n69["Study Lamp"]
|
||||||
n61 --> n69
|
n61 --> n69
|
||||||
n70["Play"]
|
n70["Marginalia Machine"]
|
||||||
n61 --> n70
|
n61 --> n70
|
||||||
n71["Archive Terminal"]
|
n71["The Rain Index"]
|
||||||
n61 --> n71
|
n61 --> n71
|
||||||
n72["Study Lamp"]
|
n72["The House of Pages"]
|
||||||
n61 --> n72
|
n61 --> n72
|
||||||
n73["Marginalia Machine"]
|
n73["The Archive World"]
|
||||||
n61 --> n73
|
n61 --> n73
|
||||||
n74["The Rain Index"]
|
|
||||||
n61 --> n74
|
|
||||||
n75["The House of Pages"]
|
|
||||||
n61 --> n75
|
|
||||||
click n1 "index.html" "Home"
|
click n1 "index.html" "Home"
|
||||||
click n2 "recently-updated.html" "Recently Updated"
|
click n2 "recently-updated.html" "Recently Updated"
|
||||||
click n4 "blogs/blogs-intro.html" "Blogs Introduction"
|
click n4 "blogs/blogs-intro.html" "Blogs Introduction"
|
||||||
@@ -200,27 +196,25 @@ flowchart TD
|
|||||||
click n51 "tags/notes.html" "Tag: notes"
|
click n51 "tags/notes.html" "Tag: notes"
|
||||||
click n52 "tags/review.html" "Tag: review"
|
click n52 "tags/review.html" "Tag: review"
|
||||||
click n53 "tags/website.html" "Tag: website"
|
click n53 "tags/website.html" "Tag: website"
|
||||||
click n54 "tags/life.html" "Tag: life"
|
click n54 "tags/update.html" "Tag: update"
|
||||||
click n55 "tags/update.html" "Tag: update"
|
click n55 "tags/life.html" "Tag: life"
|
||||||
click n56 "tags/insights.html" "Tag: insights"
|
click n56 "tags/education.html" "Tag: education"
|
||||||
click n57 "tags/education.html" "Tag: education"
|
click n57 "tags/insights.html" "Tag: insights"
|
||||||
click n58 "tags/reading.html" "Tag: reading"
|
click n58 "tags/reading.html" "Tag: reading"
|
||||||
click n59 "tags/emacs.html" "Tag: emacs"
|
click n59 "tags/emacs.html" "Tag: emacs"
|
||||||
click n60 "tags/maths.html" "Tag: maths"
|
click n60 "tags/maths.html" "Tag: maths"
|
||||||
click n62 "play/sigil.html" "Sigil Press"
|
click n62 "play/sigil.html" "Sigil Press"
|
||||||
click n63 "play/rpg.html" "Ash Below the Lake"
|
click n63 "play/ink.html" "Ink Pond"
|
||||||
click n64 "play/timeline.html" "Timeline Tangle"
|
click n64 "play/bookshelf.html" "Bookshelf Sort"
|
||||||
click n65 "play/ink.html" "Ink Pond"
|
click n65 "play/constellation.html" "Constellation Desk"
|
||||||
click n66 "play/recipe.html" "Supper Wheel"
|
click n66 "play/memory.html" "Memory Cabinet"
|
||||||
click n67 "play/bookshelf.html" "Bookshelf Sort"
|
click n67 "play/play.html" "Play"
|
||||||
click n68 "play/constellation.html" "Constellation Desk"
|
click n68 "play/terminal.html" "Archive Terminal"
|
||||||
click n69 "play/memory.html" "Memory Cabinet"
|
click n69 "play/study.html" "Study Lamp"
|
||||||
click n70 "play/play.html" "Play"
|
click n70 "play/poem.html" "Marginalia Machine"
|
||||||
click n71 "play/terminal.html" "Archive Terminal"
|
click n71 "play/the-rain-index.html" "The Rain Index"
|
||||||
click n72 "play/study.html" "Study Lamp"
|
click n72 "play/house.html" "The House of Pages"
|
||||||
click n73 "play/poem.html" "Marginalia Machine"
|
click n73 "play/rpg.html" "The Archive World"
|
||||||
click n74 "play/the-rain-index.html" "The Rain Index"
|
|
||||||
click n75 "play/house.html" "The House of Pages"
|
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
* Pages
|
* Pages
|
||||||
@@ -277,19 +271,16 @@ flowchart TD
|
|||||||
- [[file:tags/notes.org][Tag: notes]]
|
- [[file:tags/notes.org][Tag: notes]]
|
||||||
- [[file:tags/review.org][Tag: review]]
|
- [[file:tags/review.org][Tag: review]]
|
||||||
- [[file:tags/website.org][Tag: website]]
|
- [[file:tags/website.org][Tag: website]]
|
||||||
- [[file:tags/life.org][Tag: life]]
|
|
||||||
- [[file:tags/update.org][Tag: update]]
|
- [[file:tags/update.org][Tag: update]]
|
||||||
- [[file:tags/insights.org][Tag: insights]]
|
- [[file:tags/life.org][Tag: life]]
|
||||||
- [[file:tags/education.org][Tag: education]]
|
- [[file:tags/education.org][Tag: education]]
|
||||||
|
- [[file:tags/insights.org][Tag: insights]]
|
||||||
- [[file:tags/reading.org][Tag: reading]]
|
- [[file:tags/reading.org][Tag: reading]]
|
||||||
- [[file:tags/emacs.org][Tag: emacs]]
|
- [[file:tags/emacs.org][Tag: emacs]]
|
||||||
- [[file:tags/maths.org][Tag: maths]]
|
- [[file:tags/maths.org][Tag: maths]]
|
||||||
- play
|
- play
|
||||||
- [[file:play/sigil.org][Sigil Press]]
|
- [[file:play/sigil.org][Sigil Press]]
|
||||||
- [[file:play/rpg.org][Ash Below the Lake]]
|
|
||||||
- [[file:play/timeline.org][Timeline Tangle]]
|
|
||||||
- [[file:play/ink.org][Ink Pond]]
|
- [[file:play/ink.org][Ink Pond]]
|
||||||
- [[file:play/recipe.org][Supper Wheel]]
|
|
||||||
- [[file:play/bookshelf.org][Bookshelf Sort]]
|
- [[file:play/bookshelf.org][Bookshelf Sort]]
|
||||||
- [[file:play/constellation.org][Constellation Desk]]
|
- [[file:play/constellation.org][Constellation Desk]]
|
||||||
- [[file:play/memory.org][Memory Cabinet]]
|
- [[file:play/memory.org][Memory Cabinet]]
|
||||||
@@ -298,4 +289,5 @@ flowchart TD
|
|||||||
- [[file:play/study.org][Study Lamp]]
|
- [[file:play/study.org][Study Lamp]]
|
||||||
- [[file:play/poem.org][Marginalia Machine]]
|
- [[file:play/poem.org][Marginalia Machine]]
|
||||||
- [[file:play/the-rain-index.org][The Rain Index]]
|
- [[file:play/the-rain-index.org][The Rain Index]]
|
||||||
- [[file:play/house.org][The House of Pages]]
|
- [[file:play/house.org][The House of Pages]]
|
||||||
|
- [[file:play/rpg.org][The Archive World]]
|
||||||
|
|||||||
70
tests/archive-world-state.test.cjs
Normal file
70
tests/archive-world-state.test.cjs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const state = require("../assets/scripts/pages/archive-world-state.js");
|
||||||
|
|
||||||
|
test("fresh state applies defaults", () => {
|
||||||
|
assert.deepEqual(state.fresh(" Ada ", "moss"), {
|
||||||
|
version: 1,
|
||||||
|
name: "Ada",
|
||||||
|
palette: "moss",
|
||||||
|
discovered: [],
|
||||||
|
complete: false,
|
||||||
|
position: { x: 640, y: 830 },
|
||||||
|
soundEnabled: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("names are trimmed, collapsed, and limited to twenty characters", () => {
|
||||||
|
assert.equal(state.validateName(" Archive Guest "), "Archive Guest");
|
||||||
|
assert.equal(state.validateName(""), null);
|
||||||
|
assert.equal(state.validateName("x".repeat(21)), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("only declared palettes are accepted", () => {
|
||||||
|
assert.equal(state.validatePalette("berry"), "berry");
|
||||||
|
assert.equal(state.validatePalette("ultraviolet"), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("discoveries are deduplicated and completion requires all landmarks", () => {
|
||||||
|
let current = state.fresh("Ada", "brass");
|
||||||
|
current = state.discover(current, "gate");
|
||||||
|
current = state.discover(current, "gate");
|
||||||
|
assert.deepEqual(current.discovered, ["gate"]);
|
||||||
|
assert.equal(current.complete, false);
|
||||||
|
|
||||||
|
for (const id of state.LANDMARK_IDS) current = state.discover(current, id);
|
||||||
|
assert.equal(current.complete, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalization filters unknown discoveries and clamps position", () => {
|
||||||
|
const candidate = {
|
||||||
|
version: 1,
|
||||||
|
name: "Zaine",
|
||||||
|
palette: "brass",
|
||||||
|
discovered: ["gate", "gate", "missing"],
|
||||||
|
complete: true,
|
||||||
|
position: { x: -20, y: 5000 },
|
||||||
|
soundEnabled: true
|
||||||
|
};
|
||||||
|
assert.deepEqual(state.normalize(candidate), {
|
||||||
|
version: 1,
|
||||||
|
name: "Zaine",
|
||||||
|
palette: "brass",
|
||||||
|
discovered: ["gate"],
|
||||||
|
complete: false,
|
||||||
|
position: { x: 36, y: 924 },
|
||||||
|
soundEnabled: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parse rejects malformed, unsupported, and incomplete saves", () => {
|
||||||
|
assert.equal(state.parse("{"), null);
|
||||||
|
assert.equal(state.parse(JSON.stringify({ version: 2 })), null);
|
||||||
|
assert.equal(state.parse(JSON.stringify(state.fresh("", "brass"))), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a valid save round-trips and reset creates an empty state", () => {
|
||||||
|
const saved = state.discover(state.fresh("Mira", "berry"), "garden");
|
||||||
|
assert.deepEqual(state.parse(JSON.stringify(saved)), saved);
|
||||||
|
assert.deepEqual(state.fresh("Mira", "berry").discovered, []);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user