feat: major content expansion with essence and crystal sinks

- 6 zones (up from 3): Shadow Marshes, Volcanic Depths, Astral Void
- 18 bosses (up from 4): 3 per zone with zone-based sequential unlock
- 24 quests (up from 9): 3-4 per zone, reorganised by zone
- 15 adventurer tiers (up from 10): Shadow Assassin through Divine Champion
- 28 equipment pieces (up from 12): boss drops + purchasable with essence/crystals
- 24 upgrades (up from 13): crystal-cost upgrades + new adventurer upgrades
- 22 achievements (up from 14): new milestones for bosses, quests, gold, armies
- Purchasable equipment system: buy items directly with essence or crystals
- Crystal-cost upgrades: spend crystals on global and click power boosts
- Zone-based boss progression: defeating a zone's last boss unlocks the next zone
This commit is contained in:
2026-03-06 14:36:41 -08:00
committed by Naomi Carrigan
parent 653c36c886
commit 772d733e86
15 changed files with 1202 additions and 81 deletions
+56 -8
View File
@@ -25,6 +25,8 @@ interface GameContextValue {
buyAdventurer: (adventurerId: string) => void;
/** Buy an upgrade */
buyUpgrade: (upgradeId: string) => void;
/** Purchase a buyable equipment item */
buyEquipment: (equipmentId: string) => void;
/** Start a quest */
startQuest: (questId: string) => void;
/** Challenge a boss — runs full server-side simulation */
@@ -175,6 +177,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
if (!upgrade || !upgrade.unlocked || upgrade.purchased) return prev;
if (prev.resources.gold < upgrade.costGold) return prev;
if (prev.resources.essence < upgrade.costEssence) return prev;
if (prev.resources.crystals < (upgrade.costCrystals ?? 0)) return prev;
return {
...prev,
@@ -182,6 +185,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
...prev.resources,
gold: prev.resources.gold - upgrade.costGold,
essence: prev.resources.essence - upgrade.costEssence,
crystals: prev.resources.crystals - (upgrade.costCrystals ?? 0),
},
upgrades: prev.upgrades.map((u) =>
u.id === upgradeId ? { ...u, purchased: true } : u,
@@ -225,6 +229,37 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
});
}, []);
const buyEquipment = useCallback((equipmentId: string) => {
setState((prev) => {
if (!prev) return prev;
const item = (prev.equipment ?? []).find((e) => e.id === equipmentId);
if (!item || item.owned || !item.cost) return prev;
const { gold, essence, crystals } = item.cost;
if (prev.resources.gold < gold) return prev;
if (prev.resources.essence < essence) return prev;
if (prev.resources.crystals < crystals) return prev;
const slotAlreadyEquipped = (prev.equipment ?? []).some(
(e) => e.type === item.type && e.equipped,
);
return {
...prev,
resources: {
...prev.resources,
gold: prev.resources.gold - gold,
essence: prev.resources.essence - essence,
crystals: prev.resources.crystals - crystals,
},
equipment: (prev.equipment ?? []).map((e) => {
if (e.id === equipmentId) return { ...e, owned: true, equipped: !slotAlreadyEquipped };
return e;
}),
};
});
}, []);
const challengeBoss = useCallback(async (bossId: string) => {
if (!stateRef.current) return;
const boss = stateRef.current.bosses.find((b) => b.id === bossId);
@@ -238,21 +273,33 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
if (!prev) return prev;
if (result.won) {
const bossIndex = prev.bosses.findIndex((b) => b.id === bossId);
const defeatedBoss = prev.bosses.find((b) => b.id === bossId);
const zoneBosses = prev.bosses.filter((b) => b.zoneId === defeatedBoss?.zoneId);
const zoneIdx = zoneBosses.findIndex((b) => b.id === bossId);
const nextZoneBossId = zoneBosses[zoneIdx + 1]?.id;
// Find newly unlocked zones and their first bosses
const newlyUnlockedZones = (prev.zones ?? []).filter((z) => z.unlockBossId === bossId && z.status === "locked");
const newZoneFirstBossIds = newlyUnlockedZones.map((z) => {
const firstBoss = prev.bosses.find((b) => b.zoneId === z.id);
return firstBoss?.id;
}).filter(Boolean);
return {
...prev,
bosses: prev.bosses.map((b, idx) => {
if (b.id === bossId) {
return { ...b, status: "defeated" as const, currentHp: 0 };
bosses: prev.bosses.map((b) => {
if (b.id === bossId) return { ...b, status: "defeated" as const, currentHp: 0 };
if (b.id === nextZoneBossId && b.prestigeRequirement <= prev.prestige.count) {
return { ...b, status: "available" as const };
}
if (
idx === bossIndex + 1 &&
b.prestigeRequirement <= prev.prestige.count
) {
if (newZoneFirstBossIds.includes(b.id) && b.prestigeRequirement <= prev.prestige.count) {
return { ...b, status: "available" as const };
}
return b;
}),
zones: (prev.zones ?? []).map((z) =>
z.unlockBossId === bossId ? { ...z, status: "unlocked" as const } : z,
),
resources: result.rewards
? {
...prev.resources,
@@ -332,6 +379,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
handleClick,
buyAdventurer,
buyUpgrade,
buyEquipment,
startQuest,
challengeBoss,
equipItem,