Refactor org platform navigation and content flows
All checks were successful
Build Org Website / build (push) Successful in 38s
All checks were successful
Build Org Website / build (push) Successful in 38s
This commit is contained in:
192
assets/scripts/pages/archive-world-systems.js
Normal file
192
assets/scripts/pages/archive-world-systems.js
Normal file
@@ -0,0 +1,192 @@
|
||||
(function (root, factory) {
|
||||
"use strict";
|
||||
const Data = root.ArchiveWorldData || (typeof require === "function" ? require("./archive-world-data.js") : null);
|
||||
const State = root.ArchiveWorldState || (typeof require === "function" ? require("./archive-world-state.js") : null);
|
||||
const api = factory(Data, State);
|
||||
root.ArchiveWorldSystems = api;
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
}(typeof globalThis !== "undefined" ? globalThis : this, function (Data, State) {
|
||||
"use strict";
|
||||
|
||||
function copy(state) {
|
||||
return State.normalize(JSON.parse(JSON.stringify(state)));
|
||||
}
|
||||
|
||||
function normalizedVector(x, y, speed) {
|
||||
const length = Math.hypot(Number(x) || 0, Number(y) || 0);
|
||||
if (!length) return { x: 0, y: 0 };
|
||||
return { x: (x / length) * speed, y: (y / length) * speed };
|
||||
}
|
||||
|
||||
function discover(state, landmarkId) {
|
||||
const next = copy(state);
|
||||
if (!next || !Data.LANDMARK_IDS.includes(landmarkId)) return next;
|
||||
if (!next.discovered.includes(landmarkId)) next.discovered.push(landmarkId);
|
||||
next.complete = Data.LANDMARK_IDS.every((id) => next.discovered.includes(id));
|
||||
return State.normalize(next);
|
||||
}
|
||||
|
||||
function startQuest(state, questId) {
|
||||
const next = copy(state);
|
||||
if (!next || !Data.QUESTS[questId]) return next;
|
||||
if (next.quests[questId].status === "locked") next.quests[questId] = { status: "active", step: 0, count: 0 };
|
||||
if (next.story.stage === "arrival" && questId === "gate") next.story.stage = "first_act";
|
||||
return State.normalize(next);
|
||||
}
|
||||
|
||||
function progressQuest(state, questId, amount) {
|
||||
const next = startQuest(state, questId);
|
||||
if (!next || next.quests[questId].status === "complete") return next;
|
||||
next.quests[questId].count = Math.min(99, next.quests[questId].count + Math.max(1, Number(amount) || 1));
|
||||
return State.normalize(next);
|
||||
}
|
||||
|
||||
function completeQuest(state, questId) {
|
||||
let next = startQuest(state, questId);
|
||||
if (!next || next.quests[questId].status === "complete") return next;
|
||||
const quest = Data.QUESTS[questId];
|
||||
next.quests[questId] = { status: "complete", step: 99, count: next.quests[questId].count };
|
||||
if (Data.LANDMARK_IDS.includes(quest.landmark) && !next.sigils.includes(quest.landmark)) next.sigils.push(quest.landmark);
|
||||
if (!next.solvedPuzzles.includes(questId)) next.solvedPuzzles.push(questId);
|
||||
|
||||
const rewards = {
|
||||
gate: [["lantern", 1], ["ink_vial", 1]],
|
||||
library: [["archive_key", 1], ["lore_page", 1]],
|
||||
study: [["ink_vial", 2]],
|
||||
inn: [["recall_stew", 1]],
|
||||
workshop: [["workshop_cog", 3]],
|
||||
playroom: [["swiftstep_boots", 1]],
|
||||
museum: [["museum_lens", 1], ["lore_page", 1]],
|
||||
garden: [["garden_seed", 1], ["memory_fragment", 1]],
|
||||
lost_letter: [["lore_page", 1]]
|
||||
};
|
||||
(rewards[questId] || []).forEach(([id, quantity]) => { next = addItem(next, id, quantity); });
|
||||
if (questId === "playroom") next.equipment.boots = true;
|
||||
if (questId === "museum") next.equipment.lens = true;
|
||||
if (questId === "workshop" && !next.defeatedBosses.includes("sentinel")) next.defeatedBosses.push("sentinel");
|
||||
next = grantXp(next, questId === "workshop" ? 120 : questId === "museum" ? 100 : questId === "lost_letter" ? 40 : 90);
|
||||
return updateStory(next);
|
||||
}
|
||||
|
||||
function updateStory(state) {
|
||||
const next = copy(state);
|
||||
if (!next) return next;
|
||||
if (next.sigils.length >= 4 && !next.story.midpointSeen) next.story.stage = "midpoint";
|
||||
if (next.story.midpointSeen && next.sigils.length < 8) next.story.stage = "escalation";
|
||||
if (next.sigils.length === 8 && !next.story.ending) next.story.stage = "vault";
|
||||
if (next.story.ending) next.story.stage = "postgame";
|
||||
return State.normalize(next);
|
||||
}
|
||||
|
||||
function addItem(state, itemId, quantity) {
|
||||
const next = copy(state);
|
||||
if (!next || !Data.ITEMS[itemId]) return next;
|
||||
const cap = Data.ITEMS[itemId].stack || 1;
|
||||
const found = next.inventory.find((item) => item.id === itemId);
|
||||
if (found) found.quantity = Math.min(cap, found.quantity + Math.max(1, Math.floor(Number(quantity) || 1)));
|
||||
else next.inventory.push({ id: itemId, quantity: Math.min(cap, Math.max(1, Math.floor(Number(quantity) || 1))) });
|
||||
return State.normalize(next);
|
||||
}
|
||||
|
||||
function itemQuantity(state, itemId) {
|
||||
const found = state && state.inventory && state.inventory.find((item) => item.id === itemId);
|
||||
return found ? found.quantity : 0;
|
||||
}
|
||||
|
||||
function consumeItem(state, itemId) {
|
||||
let next = copy(state);
|
||||
const item = Data.ITEMS[itemId];
|
||||
if (!next || !item || item.type !== "consumable" || itemQuantity(next, itemId) < 1 || next.health >= next.maxHealth) {
|
||||
return { state: next, used: false, amount: 0 };
|
||||
}
|
||||
const amount = itemId === "recall_stew" ? next.maxHealth : 35;
|
||||
next.health = Math.min(next.maxHealth, next.health + amount);
|
||||
const found = next.inventory.find((entry) => entry.id === itemId);
|
||||
found.quantity -= 1;
|
||||
next.inventory = next.inventory.filter((entry) => entry.quantity > 0 || entry.id === "living_bookmark");
|
||||
next = State.normalize(next);
|
||||
return { state: next, used: true, amount };
|
||||
}
|
||||
|
||||
function grantXp(state, amount) {
|
||||
const next = copy(state);
|
||||
if (!next) return next;
|
||||
const oldLevel = next.level;
|
||||
next.xp = Math.min(99999, next.xp + Math.max(0, Math.floor(Number(amount) || 0)));
|
||||
next.level = State.levelForXp(next.xp);
|
||||
next.maxHealth = 100 + (next.level >= 2 ? 20 : 0) + (next.level >= 4 ? 20 : 0);
|
||||
if (next.level > oldLevel) next.health = next.maxHealth;
|
||||
return State.normalize(next);
|
||||
}
|
||||
|
||||
function takeDamage(state, amount, now, lastHitAt) {
|
||||
const next = copy(state);
|
||||
const invulnerability = next && next.settings.assist ? 1400 : 850;
|
||||
if (!next || Number(now) - Number(lastHitAt || 0) < invulnerability) return { state: next, hit: false, defeated: false };
|
||||
const scale = next.settings.assist ? 0.5 : 1;
|
||||
next.health = Math.max(0, next.health - Math.max(1, Math.round(amount * scale)));
|
||||
return { state: State.normalize(next), hit: true, defeated: next.health <= 0 };
|
||||
}
|
||||
|
||||
function respawn(state) {
|
||||
const next = copy(state);
|
||||
if (!next) return next;
|
||||
next.health = Math.max(50, Math.ceil(next.maxHealth * 0.6));
|
||||
next.position = {
|
||||
area: next.checkpoint.area, spawn: next.checkpoint.spawn,
|
||||
x: next.checkpoint.x, y: next.checkpoint.y, facing: "north"
|
||||
};
|
||||
return State.normalize(next);
|
||||
}
|
||||
|
||||
function recordBoss(state, bossId) {
|
||||
let next = copy(state);
|
||||
if (!next || !Data.BOSS_IDS.includes(bossId)) return next;
|
||||
if (!next.defeatedBosses.includes(bossId)) next.defeatedBosses.push(bossId);
|
||||
if (bossId === "sentinel") next = completeQuest(next, "workshop");
|
||||
if (bossId === "redactor") {
|
||||
next.story.ending = true;
|
||||
next.story.midpointSeen = true;
|
||||
next = grantXp(next, 250);
|
||||
next = updateStory(next);
|
||||
}
|
||||
return State.normalize(next);
|
||||
}
|
||||
|
||||
function pointInRect(x, y, item, padding) {
|
||||
const pad = Number(padding) || 0;
|
||||
return x >= item.x - pad && x <= item.x + item.width + pad && y >= item.y - pad && y <= item.y + item.height + pad;
|
||||
}
|
||||
|
||||
function isSafe(areaId, x, y) {
|
||||
const area = Data.AREAS[areaId];
|
||||
if (!area || x < 24 || y < 24 || x > area.width - 24 || y > area.height - 24) return false;
|
||||
return !area.collisions.some((item) => pointInRect(x, y, item, 10));
|
||||
}
|
||||
|
||||
function nearestSafeSpawn(areaId, x, y) {
|
||||
const area = Data.AREAS[areaId] || Data.AREAS.town;
|
||||
const points = Object.entries(area.safeSpawns || { entrance: area.spawn });
|
||||
const safeCurrent = isSafe(areaId, x, y);
|
||||
if (safeCurrent) return { area: areaId, spawn: "saved", x, y };
|
||||
const best = points.sort((a, b) =>
|
||||
Math.hypot(a[1].x - x, a[1].y - y) - Math.hypot(b[1].x - x, b[1].y - y))[0];
|
||||
return { area: areaId, spawn: best[0], x: best[1].x, y: best[1].y };
|
||||
}
|
||||
|
||||
function currentObjective(state) {
|
||||
if (!state) return "Choose a traveler.";
|
||||
if (state.story.ending) return "Archive Town remembers. Explore freely.";
|
||||
if (state.sigils.length === 8) return "Enter the Grand Library Vault and confront the Redactor.";
|
||||
const active = Data.QUEST_IDS.find((id) => state.quests[id] && state.quests[id].status === "active");
|
||||
if (active) return Data.QUESTS[active].objective;
|
||||
if (!state.sigils.includes("gate")) return "Speak with Gatekeeper Orin at the Town Gate.";
|
||||
if (state.sigils.length >= 4 && !state.story.midpointSeen) return "Bring four sigils to Curator Lima.";
|
||||
return `Recover the Archive Sigils (${state.sigils.length} / 8).`;
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
normalizedVector, discover, startQuest, progressQuest, completeQuest, updateStory, addItem, itemQuantity,
|
||||
consumeItem, grantXp, takeDamage, respawn, recordBoss, isSafe, nearestSafeSpawn, currentObjective
|
||||
});
|
||||
}));
|
||||
Reference in New Issue
Block a user