generated from nhcarrigan/template
feat: add equipment, achievements, and visual polish
- Equipment system: 12 items across weapon/armour/trinket slots with common/rare/epic/legendary rarities; starter commons auto-equipped, higher tiers drop from boss victories - Achievement system: 15 milestones with typed conditions; checked each tick and crystal rewards applied automatically - Achievement toast: slide-in notification, auto-dismisses after 4s - Floating click text: +X gold floats on each manual click - Expanded quests (9 total) and upgrades (12 total) - Upgrade panel now shows locked upgrades so players can see their progression path - formatNumber utility (K/M/B/T) used consistently across all panels - Backfill logic for existing saves to add new content gracefully - types package now emits .d.ts declarations
This commit is contained in:
+122
-10
@@ -1,4 +1,44 @@
|
||||
import type { GameState } from "@elysium/types";
|
||||
import type { Achievement, Equipment, GameState } from "@elysium/types";
|
||||
|
||||
/**
|
||||
* Checks all achievements against the current game state and returns an updated
|
||||
* achievements array, marking newly-met conditions with the current timestamp.
|
||||
*/
|
||||
const checkAchievements = (state: GameState): Achievement[] => {
|
||||
const now = Date.now();
|
||||
return (state.achievements ?? []).map((achievement) => {
|
||||
if (achievement.unlockedAt !== null) return achievement;
|
||||
|
||||
const { condition } = achievement;
|
||||
let met = false;
|
||||
|
||||
switch (condition.type) {
|
||||
case "totalGoldEarned":
|
||||
met = state.player.totalGoldEarned >= condition.amount;
|
||||
break;
|
||||
case "totalClicks":
|
||||
met = state.player.totalClicks >= condition.amount;
|
||||
break;
|
||||
case "bossesDefeated":
|
||||
met = state.bosses.filter((b) => b.status === "defeated").length >= condition.amount;
|
||||
break;
|
||||
case "questsCompleted":
|
||||
met = state.quests.filter((q) => q.status === "completed").length >= condition.amount;
|
||||
break;
|
||||
case "adventurerTotal":
|
||||
met = state.adventurers.reduce((sum, a) => sum + a.count, 0) >= condition.amount;
|
||||
break;
|
||||
case "prestigeCount":
|
||||
met = state.prestige.count >= condition.amount;
|
||||
break;
|
||||
case "equipmentOwned":
|
||||
met = (state.equipment ?? []).filter((e) => e.owned).length >= condition.amount;
|
||||
break;
|
||||
}
|
||||
|
||||
return met ? { ...achievement, unlockedAt: now } : achievement;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Pure function — applies one game tick to the state.
|
||||
@@ -6,6 +46,12 @@ import type { GameState } from "@elysium/types";
|
||||
* Returns a new GameState (does not mutate the original).
|
||||
*/
|
||||
export const applyTick = (state: GameState, deltaSeconds: number): GameState => {
|
||||
const equippedItems: Equipment[] = (state.equipment ?? []).filter((e) => e.equipped);
|
||||
const equipmentGoldMultiplier = equippedItems.reduce(
|
||||
(mult, e) => mult * (e.bonus.goldMultiplier ?? 1),
|
||||
1,
|
||||
);
|
||||
|
||||
let goldGained = 0;
|
||||
let essenceGained = 0;
|
||||
|
||||
@@ -26,7 +72,12 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
||||
const prestige = state.prestige.productionMultiplier;
|
||||
|
||||
goldGained +=
|
||||
adventurer.goldPerSecond * adventurer.count * upgradeMultiplier * prestige * deltaSeconds;
|
||||
adventurer.goldPerSecond *
|
||||
adventurer.count *
|
||||
upgradeMultiplier *
|
||||
prestige *
|
||||
equipmentGoldMultiplier *
|
||||
deltaSeconds;
|
||||
|
||||
essenceGained +=
|
||||
adventurer.essencePerSecond *
|
||||
@@ -36,12 +87,16 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
||||
deltaSeconds;
|
||||
}
|
||||
|
||||
// Complete active quests
|
||||
// Complete active quests and apply their rewards
|
||||
const now = Date.now();
|
||||
let questGold = 0;
|
||||
let questEssence = 0;
|
||||
let questCrystals = 0;
|
||||
|
||||
let updatedUpgrades = state.upgrades;
|
||||
let updatedAdventurers = state.adventurers;
|
||||
let updatedEquipment = state.equipment ?? [];
|
||||
|
||||
const updatedQuests = state.quests.map((quest) => {
|
||||
if (
|
||||
quest.status !== "active" ||
|
||||
@@ -51,7 +106,6 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
||||
return quest;
|
||||
}
|
||||
|
||||
const completed = { ...quest, status: "completed" as const };
|
||||
for (const reward of quest.rewards) {
|
||||
if (reward.type === "gold" && reward.amount != null) {
|
||||
questGold += reward.amount;
|
||||
@@ -59,15 +113,46 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
||||
questEssence += reward.amount;
|
||||
} else if (reward.type === "crystals" && reward.amount != null) {
|
||||
questCrystals += reward.amount;
|
||||
} else if (reward.type === "upgrade" && reward.targetId != null) {
|
||||
updatedUpgrades = updatedUpgrades.map((u) =>
|
||||
u.id === reward.targetId ? { ...u, unlocked: true } : u,
|
||||
);
|
||||
} else if (reward.type === "adventurer" && reward.targetId != null) {
|
||||
updatedAdventurers = updatedAdventurers.map((a) =>
|
||||
a.id === reward.targetId ? { ...a, unlocked: true } : a,
|
||||
);
|
||||
} else if (reward.type === "equipment" && reward.targetId != null) {
|
||||
const targetId = reward.targetId;
|
||||
updatedEquipment = updatedEquipment.map((e) => {
|
||||
if (e.id !== targetId) return e;
|
||||
const slotEmpty = !updatedEquipment.some(
|
||||
(other) => other.type === e.type && other.equipped,
|
||||
);
|
||||
return { ...e, owned: true, equipped: slotEmpty || e.equipped };
|
||||
});
|
||||
}
|
||||
}
|
||||
return completed;
|
||||
|
||||
return { ...quest, status: "completed" as const };
|
||||
});
|
||||
|
||||
// Unlock quests whose prerequisites are now all completed
|
||||
const completedIds = new Set(
|
||||
updatedQuests.filter((q) => q.status === "completed").map((q) => q.id),
|
||||
);
|
||||
const fullyUpdatedQuests = updatedQuests.map((quest) => {
|
||||
if (quest.status !== "locked") return quest;
|
||||
if (quest.prerequisiteIds.every((id) => completedIds.has(id))) {
|
||||
return { ...quest, status: "available" as const };
|
||||
}
|
||||
return quest;
|
||||
});
|
||||
|
||||
const newGold = state.resources.gold + goldGained + questGold;
|
||||
const newEssence = state.resources.essence + essenceGained + questEssence;
|
||||
const newTotalGoldEarned = state.player.totalGoldEarned + goldGained + questGold;
|
||||
|
||||
return {
|
||||
const partialState: GameState = {
|
||||
...state,
|
||||
resources: {
|
||||
...state.resources,
|
||||
@@ -77,20 +162,47 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
|
||||
},
|
||||
player: {
|
||||
...state.player,
|
||||
totalGoldEarned: state.player.totalGoldEarned + goldGained + questGold,
|
||||
totalGoldEarned: newTotalGoldEarned,
|
||||
},
|
||||
quests: updatedQuests,
|
||||
quests: fullyUpdatedQuests,
|
||||
upgrades: updatedUpgrades,
|
||||
adventurers: updatedAdventurers,
|
||||
equipment: updatedEquipment,
|
||||
lastTickAt: now,
|
||||
};
|
||||
|
||||
// Check achievements and apply crystal rewards for newly unlocked ones
|
||||
const updatedAchievements = checkAchievements(partialState);
|
||||
const crystalsFromAchievements = updatedAchievements.reduce((sum, a, i) => {
|
||||
const wasLocked = (state.achievements ?? [])[i]?.unlockedAt === null;
|
||||
const isNowUnlocked = a.unlockedAt !== null;
|
||||
if (wasLocked && isNowUnlocked) {
|
||||
return sum + (a.reward?.crystals ?? 0);
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
...partialState,
|
||||
achievements: updatedAchievements,
|
||||
resources: {
|
||||
...partialState.resources,
|
||||
crystals: partialState.resources.crystals + crystalsFromAchievements,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the effective click power, including upgrades.
|
||||
* Calculates the effective click power, including upgrades and equipped trinkets.
|
||||
*/
|
||||
export const calculateClickPower = (state: GameState): number => {
|
||||
const clickMultiplier = state.upgrades
|
||||
.filter((u) => u.purchased && u.target === "click")
|
||||
.reduce((mult, upgrade) => mult * upgrade.multiplier, 1);
|
||||
|
||||
return state.baseClickPower * clickMultiplier * state.prestige.productionMultiplier;
|
||||
const equipmentClickMultiplier = (state.equipment ?? [])
|
||||
.filter((e) => e.equipped && e.bonus.clickMultiplier != null)
|
||||
.reduce((mult, e) => mult * (e.bonus.clickMultiplier ?? 1), 1);
|
||||
|
||||
return state.baseClickPower * clickMultiplier * state.prestige.productionMultiplier * equipmentClickMultiplier;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user