generated from nhcarrigan/template
feat: add main questline / overarching story system (#24)
- 22 chapters across the full game arc (18 zone bosses + 4 milestones)
- Choice-based narrative with {characterName} dynamic substitution
- Story progress (unlocked + completed chapters) is permanent across all resets
- Server-side anti-cheat: chapters/choices can only accumulate, never be removed
- Tab badge and lower-right toast notifications for newly unlocked chapters
- Story choices displayed on the Character Sheet and Character Page
- How to Play entry added to About panel
This commit is contained in:
@@ -373,7 +373,28 @@ const validateAndSanitize = (incoming: GameState, previous: GameState): GameStat
|
||||
};
|
||||
})();
|
||||
|
||||
return { ...incoming, resources, bosses, quests, achievements, prestige, ...transcendenceSpread, ...apotheosisSpread, ...explorationSpread };
|
||||
// Story progress: completed chapters can only grow, unlocked IDs can only grow.
|
||||
// Low cheat risk (no rewards), so we allow all incoming additions.
|
||||
const storySpread = (() => {
|
||||
if (!incoming.story) return previous.story ? { story: previous.story } : {};
|
||||
const prevUnlocked = previous.story?.unlockedChapterIds ?? [];
|
||||
const prevCompleted = previous.story?.completedChapters ?? [];
|
||||
// Allow new chapter IDs to be added; never remove existing ones
|
||||
const unlockedChapterIds = [
|
||||
...prevUnlocked,
|
||||
...incoming.story.unlockedChapterIds.filter((id) => !prevUnlocked.includes(id)),
|
||||
];
|
||||
// Allow new completed chapters; never remove existing ones (one entry per chapter)
|
||||
const completedChapters = [
|
||||
...prevCompleted,
|
||||
...incoming.story.completedChapters.filter(
|
||||
(c) => !prevCompleted.some((p) => p.chapterId === c.chapterId),
|
||||
),
|
||||
];
|
||||
return { story: { unlockedChapterIds, completedChapters } };
|
||||
})();
|
||||
|
||||
return { ...incoming, resources, bosses, quests, achievements, prestige, ...transcendenceSpread, ...apotheosisSpread, ...explorationSpread, ...storySpread };
|
||||
};
|
||||
|
||||
export const gameRouter = new Hono<HonoEnv>();
|
||||
|
||||
@@ -35,6 +35,8 @@ export const buildPostApotheosisState = (
|
||||
...(currentState.codex ? { codex: currentState.codex } : {}),
|
||||
// Apotheosis data is eternal — never wiped by any reset
|
||||
apotheosis: newApotheosisData,
|
||||
// Story chapter progress is permanent — survives all resets
|
||||
...(currentState.story ? { story: currentState.story } : {}),
|
||||
};
|
||||
|
||||
return { newState, newApotheosisData };
|
||||
|
||||
@@ -122,6 +122,8 @@ export const buildPostPrestigeState = (
|
||||
...(currentState.transcendence ? { transcendence: currentState.transcendence } : {}),
|
||||
// Apotheosis data is eternal — never wiped by prestige
|
||||
...(currentState.apotheosis ? { apotheosis: currentState.apotheosis } : {}),
|
||||
// Story chapter progress is permanent — survives all resets
|
||||
...(currentState.story ? { story: currentState.story } : {}),
|
||||
};
|
||||
|
||||
return { newState, newPrestigeData, runestonesEarned, milestoneRunestones };
|
||||
|
||||
@@ -80,6 +80,8 @@ export const buildPostTranscendenceState = (
|
||||
transcendence: newTranscendenceData,
|
||||
// Apotheosis data is eternal — never wiped by transcendence
|
||||
...(currentState.apotheosis ? { apotheosis: currentState.apotheosis } : {}),
|
||||
// Story chapter progress is permanent — survives all resets
|
||||
...(currentState.story ? { story: currentState.story } : {}),
|
||||
};
|
||||
|
||||
return { newState, newTranscendenceData, echoesEarned };
|
||||
|
||||
Reference in New Issue
Block a user