feat: content expansion, prestige shop, and offline earnings improvements

- Expand content to 18 zones, 72 bosses, 95 quests, 32 adventurer tiers
- Add prestige shop with 24 runestone upgrades across 5 categories
- Add PrestigeUpgrade type, data files, API routes, and frontend panel
- Fix offline earnings to include equipment and runestone multipliers
- Add offline essence calculation alongside offline gold
- Update OfflineModal to display both gold and essence earned
- Add IDEAS.md for tracking planned features
This commit is contained in:
2026-03-06 21:55:42 -08:00
committed by Naomi Carrigan
parent 6bc116a86a
commit 5b4661b398
23 changed files with 2288 additions and 91 deletions
+10
View File
@@ -2,6 +2,8 @@ import type {
AuthResponse,
BossChallengeRequest,
BossChallengeResponse,
BuyPrestigeUpgradeRequest,
BuyPrestigeUpgradeResponse,
LoadResponse,
PrestigeRequest,
PrestigeResponse,
@@ -77,6 +79,14 @@ export const prestige = async (body: PrestigeRequest): Promise<PrestigeResponse>
body: JSON.stringify(body),
});
export const buyPrestigeUpgrade = async (
body: BuyPrestigeUpgradeRequest,
): Promise<BuyPrestigeUpgradeResponse> =>
request<BuyPrestigeUpgradeResponse>("/prestige/buy-upgrade", {
method: "POST",
body: JSON.stringify(body),
});
export const getPublicProfile = async (
discordId: string,
): Promise<PublicProfileResponse> =>
+13 -6
View File
@@ -1,18 +1,25 @@
import { useGame } from "../../context/GameContext.js";
export const OfflineModal = (): React.JSX.Element | null => {
const { offlineGold, dismissOfflineGold } = useGame();
const { offlineGold, offlineEssence, dismissOfflineGold, formatNumber } = useGame();
if (offlineGold <= 0) return null;
if (offlineGold <= 0 && offlineEssence <= 0) return null;
return (
<div className="modal-overlay">
<div className="modal">
<h2>Welcome back!</h2>
<p>
Your adventurers kept working whilst you were away and earned{" "}
<strong>🪙 {offlineGold.toFixed(0)} gold</strong>!
</p>
<p>Your adventurers kept working whilst you were away and earned:</p>
{offlineGold > 0 && (
<p>
<strong>🪙 {formatNumber(offlineGold)} gold</strong>
</p>
)}
{offlineEssence > 0 && (
<p>
<strong> {formatNumber(offlineEssence)} essence</strong>
</p>
)}
<p className="modal-note">Offline progress is calculated up to 8 hours.</p>
<button
className="modal-close-button"
+198 -52
View File
@@ -1,90 +1,236 @@
import type { PrestigeUpgradeCategory } from "@elysium/types";
import { useState } from "react";
import { prestige } from "../../api/client.js";
import { useGame } from "../../context/GameContext.js";
import {
PRESTIGE_UPGRADES,
PRESTIGE_UPGRADE_CATEGORY_LABELS,
} from "../../data/prestigeUpgrades.js";
const PRESTIGE_THRESHOLD = 1_000_000;
const BASE_THRESHOLD = 1_000_000;
const THRESHOLD_SCALE = 5;
const RUNESTONES_PER_LEVEL = 10;
const calculateThreshold = (prestigeCount: number): number =>
BASE_THRESHOLD * Math.pow(THRESHOLD_SCALE, prestigeCount);
const calculateProductionMultiplier = (prestigeCount: number): number =>
Math.pow(1.15, prestigeCount);
const calculateRunestonePreview = (
totalGoldEarned: number,
prestigeCount: number,
purchasedUpgradeIds: string[],
): number => {
const threshold = calculateThreshold(prestigeCount);
const base = Math.floor(Math.sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_LEVEL;
const runestoneMult = PRESTIGE_UPGRADES
.filter((u) => u.category === "runestones" && purchasedUpgradeIds.includes(u.id))
.reduce((mult, u) => mult * u.multiplier, 1);
return Math.floor(base * runestoneMult);
};
const CATEGORY_ORDER: PrestigeUpgradeCategory[] = [
"income",
"click",
"essence",
"crystals",
"runestones",
];
export const PrestigePanel = (): React.JSX.Element => {
const { state, reload, formatNumber } = useGame();
const { state, reload, formatNumber, buyPrestigeUpgrade } = useGame();
const [characterName, setCharacterName] = useState("");
const [isPending, setIsPending] = useState(false);
const [result, setResult] = useState<{ runestones: number; count: number } | null>(null);
const [error, setError] = useState<string | null>(null);
const [prestigeError, setPrestigeError] = useState<string | null>(null);
const [buyingId, setBuyingId] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<"prestige" | "shop">("prestige");
if (!state) return <section className="panel"><p>Loading...</p></section>;
const isEligible = state.player.totalGoldEarned >= PRESTIGE_THRESHOLD;
const { prestige: prestigeData, player } = state;
const threshold = calculateThreshold(prestigeData.count);
const isEligible = player.totalGoldEarned >= threshold;
const runestonePreview = calculateRunestonePreview(
player.totalGoldEarned,
prestigeData.count,
prestigeData.purchasedUpgradeIds,
);
const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1);
const handlePrestige = async (): Promise<void> => {
if (!characterName.trim()) return;
setIsPending(true);
setError(null);
setPrestigeError(null);
try {
const data = await prestige({ characterName: characterName.trim() });
setResult({ runestones: data.runestones, count: data.newPrestigeCount });
await reload();
} catch (err) {
setError(err instanceof Error ? err.message : "Prestige failed");
setPrestigeError(err instanceof Error ? err.message : "Prestige failed");
} finally {
setIsPending(false);
}
};
const handleBuyUpgrade = async (upgradeId: string): Promise<void> => {
setBuyingId(upgradeId);
try {
await buyPrestigeUpgrade(upgradeId);
} finally {
setBuyingId(null);
}
};
const upgradesByCategory = CATEGORY_ORDER.map((category) => ({
category,
label: PRESTIGE_UPGRADE_CATEGORY_LABELS[category] ?? category,
upgrades: PRESTIGE_UPGRADES.filter((u) => u.category === category),
}));
return (
<section className="panel prestige-panel">
<h2> Prestige</h2>
<p>
Prestige resets your progress but grants <strong>Runestones</strong> permanent
currency used for powerful upgrades. Each prestige also increases your global
production multiplier by 10%.
</p>
<div className="prestige-status">
<p>
Total gold earned:{" "}
<strong>{formatNumber(state.player.totalGoldEarned)}</strong>
</p>
<p>
Required: <strong>{formatNumber(PRESTIGE_THRESHOLD)}</strong>
</p>
<p>Current prestige count: <strong>{state.prestige.count}</strong></p>
<p>
Production multiplier:{" "}
<strong>×{state.prestige.productionMultiplier.toFixed(1)}</strong>
</p>
<div className="prestige-tabs">
<button
className={`prestige-tab ${activeTab === "prestige" ? "active" : ""}`}
onClick={() => { setActiveTab("prestige"); }}
type="button"
>
Ascend
</button>
<button
className={`prestige-tab ${activeTab === "shop" ? "active" : ""}`}
onClick={() => { setActiveTab("shop"); }}
type="button"
>
🔮 Runestone Shop ({formatNumber(prestigeData.runestones)} stones)
</button>
</div>
{isEligible ? (
<div className="prestige-form">
<p>You are ready to prestige! Choose your new character name:</p>
<input
type="text"
value={characterName}
onChange={(e) => { setCharacterName(e.target.value); }}
placeholder="Character name..."
maxLength={32}
disabled={isPending}
/>
<button
className="prestige-button"
onClick={() => { void handlePrestige(); }}
disabled={isPending || !characterName.trim()}
type="button"
>
{isPending ? "Ascending..." : "✨ Ascend"}
</button>
{error && <p className="error">{error}</p>}
{result && (
<p className="success">
Ascended to Prestige {result.count}! Earned {result.runestones} Runestones.
{activeTab === "prestige" && (
<>
<p>
Prestige resets your progress but grants <strong>Runestones</strong> permanent
currency used for powerful upgrades. Each prestige multiplies your global production
by ×1.15 (compounding each run).
</p>
<div className="prestige-status">
<p>
Total gold this run:{" "}
<strong>{formatNumber(player.totalGoldEarned)}</strong>
</p>
<p>
Required to prestige: <strong>{formatNumber(threshold)}</strong>
</p>
<p>
Prestige count: <strong>{prestigeData.count}</strong>
</p>
<p>
Current production multiplier:{" "}
<strong>×{prestigeData.productionMultiplier.toFixed(2)}</strong>
</p>
<p>
After next prestige:{" "}
<strong>×{nextMultiplier.toFixed(2)}</strong>
</p>
<p>
Runestones: <strong>{formatNumber(prestigeData.runestones)}</strong>
</p>
{isEligible && (
<p className="runestone-preview">
Runestones on prestige: <strong>+{formatNumber(runestonePreview)}</strong>
</p>
)}
{!isEligible && (
<p className="prestige-progress">
Progress: {formatNumber(player.totalGoldEarned)} / {formatNumber(threshold)}{" "}
({((player.totalGoldEarned / threshold) * 100).toFixed(1)}%)
</p>
)}
</div>
{isEligible ? (
<div className="prestige-form">
<p>You are ready to prestige! Choose your new character name:</p>
<input
disabled={isPending}
maxLength={32}
onChange={(e) => { setCharacterName(e.target.value); }}
placeholder="Character name..."
type="text"
value={characterName}
/>
<button
className="prestige-button"
disabled={isPending || !characterName.trim()}
onClick={() => { void handlePrestige(); }}
type="button"
>
{isPending ? "Ascending..." : `✨ Ascend (+${formatNumber(runestonePreview)} Runestones)`}
</button>
{prestigeError && <p className="error">{prestigeError}</p>}
{result && (
<p className="success">
Ascended to Prestige {result.count}! Earned {formatNumber(result.runestones)} Runestones.
</p>
)}
</div>
) : (
<p className="prestige-locked">
Earn {formatNumber(threshold - player.totalGoldEarned)} more gold to unlock prestige.
</p>
)}
</>
)}
{activeTab === "shop" && (
<div className="runestone-shop">
<p className="shop-balance">
Balance: <strong>{formatNumber(prestigeData.runestones)} Runestones</strong>
</p>
{upgradesByCategory.map(({ category, label, upgrades }) => (
<div key={category} className="shop-category">
<h3>{label}</h3>
<div className="shop-upgrades">
{upgrades.map((upgrade) => {
const purchased = prestigeData.purchasedUpgradeIds.includes(upgrade.id);
const canAfford = prestigeData.runestones >= upgrade.runestonesCost;
const isLoading = buyingId === upgrade.id;
return (
<div
key={upgrade.id}
className={`shop-upgrade-card ${purchased ? "purchased" : ""} ${!canAfford && !purchased ? "unaffordable" : ""}`}
>
<div className="shop-upgrade-info">
<h4>{upgrade.name}</h4>
<p>{upgrade.description}</p>
<p className="upgrade-cost">
{purchased ? "✅ Purchased" : `🔮 ${formatNumber(upgrade.runestonesCost)} Runestones`}
</p>
</div>
{!purchased && (
<button
className="buy-upgrade-button"
disabled={!canAfford || isLoading || buyingId !== null}
onClick={() => { void handleBuyUpgrade(upgrade.id); }}
type="button"
>
{isLoading ? "Buying..." : "Buy"}
</button>
)}
</div>
);
})}
</div>
</div>
))}
</div>
) : (
<p className="prestige-locked">
Earn {formatNumber(PRESTIGE_THRESHOLD - state.player.totalGoldEarned)} more
gold to unlock prestige.
</p>
)}
</section>
);
+24 -2
View File
@@ -18,18 +18,27 @@ const questTimeRemaining = (quest: Quest): number => {
interface QuestCardProps {
quest: Quest;
partyCombatPower: number;
unlockHint?: string | undefined;
zoneHint?: string | undefined;
}
const QuestCard = ({ quest, unlockHint, zoneHint }: QuestCardProps): React.JSX.Element => {
const QuestCard = ({ quest, partyCombatPower, unlockHint, zoneHint }: QuestCardProps): React.JSX.Element => {
const { startQuest, formatNumber } = useGame();
const cpRequired = quest.combatPowerRequired ?? 0;
const meetsCP = partyCombatPower >= cpRequired;
return (
<div className={`quest-card quest-${quest.status}`}>
<div className="quest-info">
<h3>{quest.name}</h3>
<p>{quest.description}</p>
{cpRequired > 0 && (
<p className={`quest-cp-requirement ${meetsCP ? "cp-met" : "cp-unmet"}`}>
Requires {formatNumber(cpRequired)} Combat Power
{quest.status === "available" && (meetsCP ? " ✓" : ` (you have ${formatNumber(partyCombatPower)})`)}
</p>
)}
<div className="quest-rewards">
{quest.rewards.map((reward, index) => (
// eslint-disable-next-line react/no-array-index-key -- rewards have no unique id
@@ -54,7 +63,9 @@ const QuestCard = ({ quest, unlockHint, zoneHint }: QuestCardProps): React.JSX.E
{quest.status === "available" && (
<button
className="start-quest-button"
disabled={!meetsCP}
onClick={() => { startQuest(quest.id); }}
title={meetsCP ? undefined : `Need ${formatNumber(cpRequired)} combat power`}
type="button"
>
Send Party ({formatDuration(quest.durationSeconds)})
@@ -78,6 +89,11 @@ export const QuestPanel = (): React.JSX.Element => {
if (!state) return <section className="panel"><p>Loading...</p></section>;
const partyCombatPower = state.adventurers.reduce(
(total, a) => total + a.combatPower * a.count,
0,
);
const zones = state.zones ?? [];
const zoneQuests = state.quests.filter((q) => q.zoneId === activeZoneId);
const lockedCount = zoneQuests.filter((q) => q.status === "locked").length;
@@ -124,7 +140,13 @@ export const QuestPanel = (): React.JSX.Element => {
<div className="quest-list">
{visibleQuests.map((quest) => (
<QuestCard key={quest.id} quest={quest} unlockHint={questUnlockHints.get(quest.id)} zoneHint={questZoneHints.get(quest.id)} />
<QuestCard
key={quest.id}
partyCombatPower={partyCombatPower}
quest={quest}
unlockHint={questUnlockHints.get(quest.id)}
zoneHint={questZoneHints.get(quest.id)}
/>
))}
{visibleQuests.length === 0 && (
<p className="empty-zone">No quests to show in this zone.</p>
+41 -2
View File
@@ -7,7 +7,12 @@ import {
useRef,
useState,
} from "react";
import { challengeBoss as challengeBossApi, loadGame, saveGame } from "../api/client.js";
import {
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
challengeBoss as challengeBossApi,
loadGame,
saveGame,
} from "../api/client.js";
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
import { formatNumber as formatNumberUtil } from "../utils/format.js";
@@ -47,7 +52,9 @@ interface GameContextValue {
syncError: string | null;
/** Offline gold earned on login */
offlineGold: number;
/** Dismiss the offline gold notification */
/** Offline essence earned on login */
offlineEssence: number;
/** Dismiss the offline earnings notification */
dismissOfflineGold: () => void;
/** Battle result to display in the modal (null when no battle pending) */
battleResult: BattleResult | null;
@@ -63,6 +70,8 @@ interface GameContextValue {
setNumberFormat: (format: NumberFormat) => void;
/** Format a number using the player's chosen notation style */
formatNumber: (value: number) => string;
/** Buy a prestige upgrade from the runestone shop */
buyPrestigeUpgrade: (upgradeId: string) => Promise<void>;
}
const GameContext = createContext<GameContextValue | null>(null);
@@ -74,6 +83,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [offlineGold, setOfflineGold] = useState(0);
const [offlineEssence, setOfflineEssence] = useState(0);
const [battleResult, setBattleResult] = useState<BattleResult | null>(null);
const [newAchievements, setNewAchievements] = useState<Achievement[]>([]);
const [lastSavedAt, setLastSavedAt] = useState<number | null>(null);
@@ -104,6 +114,9 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
if (data.offlineGold > 0) {
setOfflineGold(data.offlineGold);
}
if (data.offlineEssence > 0) {
setOfflineEssence(data.offlineEssence);
}
// Fetch number format preference from profile (fire-and-forget, non-blocking)
void fetch(`/api/profile/${data.state.player.discordId}`)
.then(async (res) => {
@@ -353,6 +366,29 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
});
}, []);
const buyPrestigeUpgrade = useCallback(async (upgradeId: string) => {
try {
const result = await buyPrestigeUpgradeApi({ upgradeId });
setState((prev) => {
if (!prev) return prev;
return {
...prev,
prestige: {
...prev.prestige,
runestones: result.runestonesRemaining,
purchasedUpgradeIds: result.purchasedUpgradeIds,
runestonesIncomeMultiplier: result.runestonesIncomeMultiplier,
runestonesClickMultiplier: result.runestonesClickMultiplier,
runestonesEssenceMultiplier: result.runestonesEssenceMultiplier,
runestonesCrystalMultiplier: result.runestonesCrystalMultiplier,
},
};
});
} catch {
// Silently ignore — server errors shouldn't crash the UI
}
}, []);
const challengeBoss = useCallback(async (bossId: string) => {
if (!stateRef.current) return;
const boss = stateRef.current.bosses.find((b) => b.id === bossId);
@@ -464,6 +500,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
const dismissOfflineGold = useCallback(() => {
setOfflineGold(0);
setOfflineEssence(0);
}, []);
const dismissBattle = useCallback(() => {
@@ -498,6 +535,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
forceSync,
syncError,
offlineGold,
offlineEssence,
dismissOfflineGold,
battleResult,
dismissBattle,
@@ -506,6 +544,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
numberFormat,
setNumberFormat,
formatNumber: boundFormatNumber,
buyPrestigeUpgrade,
}}
>
{children}
+209
View File
@@ -0,0 +1,209 @@
import type { PrestigeUpgrade } from "@elysium/types";
export const PRESTIGE_UPGRADES: PrestigeUpgrade[] = [
// ── Global Income Tiers ───────────────────────────────────────────────────
{
id: "income_1",
name: "Runestone Blessing I",
description: "The first runestone awakens dormant power in your guild. All production ×1.25.",
category: "income",
runestonesCost: 10,
multiplier: 1.25,
},
{
id: "income_2",
name: "Runestone Blessing II",
description: "Deeper runestone resonance amplifies your workforce. All production ×1.5.",
category: "income",
runestonesCost: 25,
multiplier: 1.5,
},
{
id: "income_3",
name: "Runestone Blessing III",
description: "The runes sing with accumulated wisdom. All production ×2.",
category: "income",
runestonesCost: 60,
multiplier: 2,
},
{
id: "income_4",
name: "Runic Surge I",
description: "Runestone energy surges through your guild's operations. All production ×3.",
category: "income",
runestonesCost: 150,
multiplier: 3,
},
{
id: "income_5",
name: "Runic Surge II",
description: "The surge intensifies, pushing limits thought impossible. All production ×5.",
category: "income",
runestonesCost: 350,
multiplier: 5,
},
{
id: "income_6",
name: "Runic Surge III",
description: "An overwhelming tide of runic energy floods your operations. All production ×10.",
category: "income",
runestonesCost: 800,
multiplier: 10,
},
{
id: "income_7",
name: "Ancient Inscription I",
description: "You decipher ancient runic inscriptions that unlock vast potential. All production ×25.",
category: "income",
runestonesCost: 2_000,
multiplier: 25,
},
{
id: "income_8",
name: "Ancient Inscription II",
description: "Deeper inscriptions reveal secrets of primordial power. All production ×50.",
category: "income",
runestonesCost: 5_000,
multiplier: 50,
},
{
id: "income_9",
name: "Ancient Inscription III",
description: "The full inscription blazes with world-shaping power. All production ×100.",
category: "income",
runestonesCost: 12_000,
multiplier: 100,
},
{
id: "income_10",
name: "Eternal Rune I",
description: "The oldest runes, carved before memory began, yield their secrets at last. All production ×500.",
category: "income",
runestonesCost: 30_000,
multiplier: 500,
},
{
id: "income_11",
name: "Eternal Rune II",
description: "Eternal runes resonate with the heartbeat of creation itself. All production ×1,000.",
category: "income",
runestonesCost: 80_000,
multiplier: 1_000,
},
// ── Click Power ───────────────────────────────────────────────────────────
{
id: "click_power_1",
name: "Runic Strike I",
description: "Infuse your personal strikes with runestone energy. Click power ×2.",
category: "click",
runestonesCost: 15,
multiplier: 2,
},
{
id: "click_power_2",
name: "Runic Strike II",
description: "Your strikes crackle with compounded runic force. Click power ×5.",
category: "click",
runestonesCost: 75,
multiplier: 5,
},
{
id: "click_power_3",
name: "Runic Strike III",
description: "Every click channels the weight of all your past lives. Click power ×20.",
category: "click",
runestonesCost: 400,
multiplier: 20,
},
{
id: "click_power_4",
name: "World-Breaker Click",
description: "A single click now carries the force of a falling empire. Click power ×100.",
category: "click",
runestonesCost: 2_500,
multiplier: 100,
},
// ── Essence Production ────────────────────────────────────────────────────
{
id: "essence_1",
name: "Essence Attunement I",
description: "Runestone resonance amplifies your essence gathering. Essence production ×2.",
category: "essence",
runestonesCost: 20,
multiplier: 2,
},
{
id: "essence_2",
name: "Essence Attunement II",
description: "Deep attunement draws essence from previously invisible sources. Essence production ×5.",
category: "essence",
runestonesCost: 120,
multiplier: 5,
},
{
id: "essence_3",
name: "Essence Attunement III",
description: "Your guild breathes essence as naturally as air. Essence production ×20.",
category: "essence",
runestonesCost: 700,
multiplier: 20,
},
{
id: "essence_4",
name: "Essence Attunement IV",
description: "Essence flows in torrents from every corner of every world. Essence production ×100.",
category: "essence",
runestonesCost: 4_000,
multiplier: 100,
},
// ── Crystal Production ────────────────────────────────────────────────────
{
id: "crystal_1",
name: "Crystal Resonance I",
description: "Runestones vibrate in harmony with crystal structures. Crystal rewards ×2.",
category: "crystals",
runestonesCost: 30,
multiplier: 2,
},
{
id: "crystal_2",
name: "Crystal Resonance II",
description: "The resonance deepens, shattering crystal barriers. Crystal rewards ×5.",
category: "crystals",
runestonesCost: 200,
multiplier: 5,
},
{
id: "crystal_3",
name: "Crystal Resonance III",
description: "Pure resonance crystallises reality into abundance. Crystal rewards ×25.",
category: "crystals",
runestonesCost: 1_200,
multiplier: 25,
},
// ── Runestone Meta-Upgrades ───────────────────────────────────────────────
{
id: "runestone_gain_1",
name: "Runic Legacy",
description: "Your runestone attunement grows with each prestige. Earn 25% more runestones from future prestiges.",
category: "runestones",
runestonesCost: 50,
multiplier: 1.25,
},
{
id: "runestone_gain_2",
name: "Eternal Legacy",
description: "Your legend transcends individual lifetimes. Earn 50% more runestones from future prestiges.",
category: "runestones",
runestonesCost: 500,
multiplier: 1.5,
},
];
export const PRESTIGE_UPGRADE_CATEGORY_LABELS: Record<string, string> = {
income: "🪙 Global Income",
click: "👆 Click Power",
essence: "✨ Essence Production",
crystals: "💎 Crystal Rewards",
runestones: "🔮 Runestone Gain",
};
+16 -2
View File
@@ -57,6 +57,10 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
1,
);
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
const runestonesCrystal = state.prestige.runestonesCrystalMultiplier ?? 1;
let goldGained = 0;
let essenceGained = 0;
@@ -81,6 +85,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
adventurer.count *
upgradeMultiplier *
prestige *
runestonesIncome *
equipmentGoldMultiplier *
deltaSeconds;
@@ -89,6 +94,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
adventurer.count *
upgradeMultiplier *
prestige *
runestonesEssence *
deltaSeconds;
}
@@ -117,7 +123,7 @@ export const applyTick = (state: GameState, deltaSeconds: number): GameState =>
} else if (reward.type === "essence" && reward.amount != null) {
questEssence += reward.amount;
} else if (reward.type === "crystals" && reward.amount != null) {
questCrystals += reward.amount;
questCrystals += reward.amount * runestonesCrystal;
} else if (reward.type === "upgrade" && reward.targetId != null) {
updatedUpgrades = updatedUpgrades.map((u) =>
u.id === reward.targetId ? { ...u, unlocked: true } : u,
@@ -250,5 +256,13 @@ export const calculateClickPower = (state: GameState): number => {
.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;
const runestonesClick = state.prestige.runestonesClickMultiplier ?? 1;
return (
state.baseClickPower *
clickMultiplier *
state.prestige.productionMultiplier *
runestonesClick *
equipmentClickMultiplier
);
};