generated from nhcarrigan/template
fix: runestone formula, prestige/transcendence rebalance, exploration fixes, and comprehensive balance audit (#135)
## What changed and why ### Runestone formula (`prestige.ts`) - Swapped `sqrt` for `cbrt` — much stronger diminishing returns for large gold values - Added base cap of **200** (→ ~1,125 max with all upgrades at 5.625× multiplier) - Prevents extended AFK sessions from producing runestone windfalls that allow immediate upgrade purchasing and rapid prestige chaining ### Prestige threshold formula (`prestige.ts`) - Old: `1,000,000 × 5^n` — exponential, grows impossibly fast, prestige 10+ takes years - New: `1,000,000 × (n+1)²` — polynomial, peaks at ~1 day/run around P8–10, then gets *easier* as the production multiplier overtakes it - Removed `thresholdScaleFactor` constant (no longer needed) ### Production multiplier (`prestige.ts`) - Old: `1.15^n` - New: `1.25^n` — compounds faster, ensures the polynomial threshold eventually gets easy in the late game ### Boss prestige requirements (`bosses.ts`) - Rescaled proportionally from 0–88 range to 0–20 range - The Absolute One now requires prestige **20** (was 88), making transcendence reachable in a few weeks of idle play ### Echo formula (`transcendence.ts`) - Constant changed from 853 → **224** - At the target prestige of 20: `floor(224 / sqrt(20)) = 50 echoes` per transcendence (no meta upgrades) - With all echo_meta upgrades (3.75× total): up to **187 echoes** per transcendence ### Transcendence upgrade costs (`transcendenceUpgrades.ts`) - Old total: **866 echoes** → New total: **400 echoes** (roughly halved across all categories) - Apotheosis still requires **all 15 upgrades** purchased ### Balance fixes (closes #141, #142, #143, #144, #145) - Equipment: `philosophers_stone` click multiplier 2.25→2.5, `crystal_shard` 1.55→1.65 (#144) - Recipes: added `primal_omega_lens` cross-zone click_power recipe at 1.38× (#142) - Adventurers: `celestial_guard` base cost adjusted to smooth tier 14→15→16 cost curve (#145) ### Quest reward rebalancing (closes #136, #137) - Shadow Marshes: buffed `shadow_mere`, `witch_coven`, `plague_ruins` rewards to match combat requirements (#136) - Astral Void: added gold to `void_rift`, increased rewards across all Astral Void quests (#137) ### Boss reward additions (closes #138, #139, #140) - Assigned 9 unassigned adventurer-specific upgrades to Crystalline Spire through Eternal Throne bosses that had empty `upgradeRewards` arrays (#140) ### Combat power documentation (closes #153) - Expanded JSDoc on `computePartyCombatPower` to clarify companion `bossDamage` multiplier behaviour ### Effective adventurer stats (closes #154) - Added `computeEffectiveAdventurerStats` to `tick.ts` and updated `AdventurerCard` to display effective post-multiplier stats ### Adventurer upgrade timing (closes #158) - Audited every adventurer-specific upgrade reward — upgrades now land within the same progression window where that adventurer tier is still a meaningful contributor ### Sync and save fixes (closes #147, #148, #151) - Fixed sync new content count to report only genuinely changed items (#147) - Fixed signature mismatch after first auto-boss completion (#148) - Added auto-buy cap (100) on non-max-tier adventurers (#151) ### Auto-adventurer persistence (closes #156) - Auto-buy preference now preserved across prestige resets ### Broken CDN image (closes #159) - Uploaded missing `auto_adventurer.jpg` to CDN ### Codex unlock hints (closes #146) - Locked codex entries now display a hint generated from `sourceType` and `sourceId` ### Exploration bug fixes (closes #160, #161) - Fixed auto-save race condition discarding exploration materials collected mid-tick (#160) - Fixed exploration areas failing to unlock when zone was unlocked via boss kill or quest completion (#161) ### Concurrent prestige fix (closes #162) - Added optimistic locking via `updatedAt` — concurrent prestige requests return 409 ### Prestige UX (closes #163) - Added `reloadSilent` to game context — no loading screen flash after prestige ### Balance adjustments (closes #164, #165, #166, #167) - Reduced `shadow_mere` CP requirement 5,000,000 → 2,000,000 (#164) - Buffed crystal drops from Shadow Marshes bosses and quests (#165) - Increased runestone yield from 10 → 15 per prestige level (#166) - Daily challenge set always includes a clicks challenge (#167) ### Progression QoL (closes #168, #169) - Added `computeProjectedRunestones()` and persistent `+N On Prestige` resource bar row (#168) - Added `enablePrestigeAnnouncements` setting per player (#169) --- ## Comprehensive balance audit (closes #187, #191, #192, #193, #194, #195, #196, #197, #198) ### Crystal economy fixes - Zeroed crystal rewards for all Zone 7+ boss drops (Celestial Reaches onwards) — crystals are an early/mid-game currency and should not flow freely into the endgame (#187) - Zeroed crystal rewards for all Zone 9+ quest rewards (Infernal Court onwards) — same rationale (#191) ### Achievement additions and fixes - Added quest milestone achievements at 75 quests (10,000 crystals) and 100 quests (15,000 crystals) - Added boss milestone achievement at 50 bosses (15,000 crystals) - Added prestige milestone achievements at P50, P100, P150, P200 — rewarding **runestones** rather than crystals to match the late-game economy - Added gold milestone achievements through 1e90 gold earned - Fixed `quest_eternal` condition from 122 → **112** (actual quest count) — was permanently impossible (#197) - Fixed `fully_equipped` condition from 65 → **78** (actual equipment count after new items) (#197) - Fixed `devourer_slayer` description to remove incorrect zone reference ### Upgrade balance - Fixed Essence Guild multiplier 1.5× → **2×** — was identical to the cheaper Merchant Alliance for 5× the cost (#194) - Raised Void Ascendancy crystal cost 10M → **50M** — was trivially cheap compared to the parallel Celestial Mandate upgrade (100B essence + 50T gold) (#195) - Fixed Sunken Temple quest rewards (gold 2M → 60M, essence 1,500 → 25,000, crystals 75 → 400) — was rewarding less than its easier prerequisite Witch Coven (#193) ### Equipment balance - Buffed Eternal Prism stats to click 5×, combat **3×**, gold **2.5×** — was only marginally better than the free Eternity Stone boss drop for 100M crystals (#196) ### Missing content - Created **13 missing equipment items** for Zones 15–18 (primordial_chaos through the_absolute) that were referenced by late-game boss `equipmentRewards` arrays but never existed in `equipment.ts` (#198): - `chaos_mantle`, `titan_core` (Primordial Chaos) - `expanse_blade`, `void_armour_mk2` (Infinite Expanse) - `cosmos_blade`, `reality_plate` (Reality Forge) - `maelstrom_edge`, `cosmic_plate` (Cosmic Maelstrom) - `primeval_blade`, `ancient_aegis` (Primeval Sanctum) - `absolute_blade`, `eternity_plate`, `omniversal_core` (The Absolute) - Stats scale from combat 14× / gold 9× (Zone 15) up to combat 28× / gold 20× for the final boss drops ### Type system - Extended `AchievementReward` type to support `runestones` field - Updated tick engine achievement processing to award both crystals and runestones --- ## Target progression timeline (optimal play, ~16h/day idle) - First cycle to P20: ~375h (~3.3 weeks) - Each subsequent cycle gets faster as echo upgrades boost income/combat/threshold - Expected **~5 transcendences** before apotheosis at 50–187 echoes/transcendence - **~6 months** to apotheosis for a dedicated player ## Test plan - [ ] Lint, build, and test pipeline passes (100% coverage maintained) - [ ] Prestige threshold at P0 is still 1,000,000 gold - [ ] Prestige runs feel ~1 day long around P8–10 and get easier after - [ ] The Absolute One is locked until prestige 20 - [ ] Transcendence at P20 awards 50 echoes (no meta upgrades) - [ ] All 15 transcendence upgrades cost 400 echoes total - [ ] Bosses in Zones 7+ drop 0 crystals; Zones 1–6 retain crystal drops - [ ] Quests in Zones 9+ reward 0 crystals; Zones 1–8 retain crystal rewards - [ ] Sunken Temple rewards more gold/essence/crystals than Witch Coven - [ ] Essence Guild gives 2× income (stronger than Merchant Alliance 1.5×) - [ ] Void Ascendancy costs 50M crystals - [ ] Eternal Prism stats are click 5×, combat 3×, gold 2.5× - [ ] Late-game bosses (primordial_titan through the_absolute_one) drop equipment on kill - [ ] `quest_eternal` achievement requires 112 quests - [ ] `fully_equipped` achievement requires 78 equipment pieces - [ ] P50/P100/P150/P200 prestige achievements reward runestones - [ ] Adventurer cards show effective post-multiplier stats - [ ] Exploration areas unlock correctly when their zone is unlocked - [ ] Concurrent prestige requests return 409 - [ ] No loading screen flash after prestige - [ ] Daily challenge set always includes a clicks challenge - [ ] Resource bar shows `+N On Prestige` runestone preview ✨ This PR was crafted with help from Hikari~ 🌸 Reviewed-on: #135 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #135.
This commit is contained in:
+335
-14
@@ -11,7 +11,6 @@
|
||||
/* eslint-disable max-lines -- Engine file necessarily exceeds line limit */
|
||||
/* eslint-disable import/group-exports -- Exports appear alongside their definitions for readability */
|
||||
/* eslint-disable import/exports-last -- Exports appear alongside their definitions for readability */
|
||||
/* eslint-disable unicorn/no-array-reduce -- reduce is the most readable approach for multiplier chains */
|
||||
/* eslint-disable max-nested-callbacks -- Tick engine requires nested array operations for game logic */
|
||||
import {
|
||||
type Achievement,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
getActiveCompanionBonus,
|
||||
} from "@elysium/types";
|
||||
import { EQUIPMENT_SETS } from "../data/equipmentSets.js";
|
||||
import { EXPLORATION_AREAS } from "../data/explorations.js";
|
||||
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
|
||||
|
||||
/**
|
||||
@@ -83,6 +83,12 @@ const checkAchievements = (state: GameState): Array<Achievement> => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Exponential base for the prestige combat multiplier: Math.pow(base, prestigeCount).
|
||||
* Replaces the former linear formula (1 + count * 0.1) to enable late-game zone progression.
|
||||
*/
|
||||
export const PRESTIGE_COMBAT_BASE = 4;
|
||||
|
||||
/**
|
||||
* Maximum value any resource can accumulate to. Beyond this JS floats lose all useful precision.
|
||||
*/
|
||||
@@ -195,6 +201,285 @@ export const computeGoldPerSecond = (state: GameState): number => {
|
||||
return goldPerSecond;
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the current essence per second for the given game state,
|
||||
* applying all relevant multipliers (upgrades, prestige, echo, crafted, companion).
|
||||
* @param state - The current game state.
|
||||
* @returns The total essence per second.
|
||||
*/
|
||||
export const computeEssencePerSecond = (state: GameState): number => {
|
||||
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
|
||||
const craftedEssenceMultiplier
|
||||
= state.exploration?.craftedEssenceMultiplier ?? 1;
|
||||
const companionBonus = getActiveCompanionBonus(
|
||||
state.companions?.activeCompanionId,
|
||||
state.companions?.unlockedCompanionIds ?? [],
|
||||
);
|
||||
const companionEssenceMult
|
||||
= companionBonus?.type === "essenceIncome"
|
||||
? 1 + companionBonus.value
|
||||
: 1;
|
||||
|
||||
let essencePerSecond = 0;
|
||||
for (const adventurer of state.adventurers) {
|
||||
if (!adventurer.unlocked || adventurer.count === 0) {
|
||||
continue;
|
||||
}
|
||||
const upgradeMultiplier = state.upgrades.
|
||||
filter((upgrade) => {
|
||||
const isGlobal = upgrade.target === "global";
|
||||
const isThisAdventurer
|
||||
= upgrade.target === "adventurer"
|
||||
&& upgrade.adventurerId === adventurer.id;
|
||||
return upgrade.purchased && (isGlobal || isThisAdventurer);
|
||||
}).
|
||||
reduce((mult, upgrade) => {
|
||||
return mult * upgrade.multiplier;
|
||||
}, 1);
|
||||
const contribution
|
||||
= adventurer.essencePerSecond
|
||||
* adventurer.count
|
||||
* upgradeMultiplier
|
||||
* state.prestige.productionMultiplier
|
||||
* runestonesEssence
|
||||
* craftedEssenceMultiplier
|
||||
* companionEssenceMult;
|
||||
essencePerSecond = essencePerSecond + contribution;
|
||||
}
|
||||
return essencePerSecond;
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the effective per-unit stats for a single adventurer type,
|
||||
* applying all active multipliers (upgrades, prestige, equipment, echo,
|
||||
* crafted, companion). The returned values represent what a single
|
||||
* adventurer of this type currently contributes per second, matching the
|
||||
* per-unit contribution used by computeGoldPerSecond and
|
||||
* computeEssencePerSecond.
|
||||
* @param state - The current game state.
|
||||
* @param adventurerId - The ID of the adventurer to compute stats for.
|
||||
* @returns Effective per-unit goldPerSecond, essencePerSecond, and combatPower.
|
||||
*/
|
||||
export const computeEffectiveAdventurerStats = (
|
||||
state: GameState,
|
||||
adventurerId: string,
|
||||
): { combatPower: number; essencePerSecond: number; goldPerSecond: number } => {
|
||||
const adventurer = state.adventurers.find((a) => {
|
||||
return a.id === adventurerId;
|
||||
});
|
||||
|
||||
/* V8 ignore next 3 -- @preserve */
|
||||
if (adventurer === undefined) {
|
||||
return { combatPower: 0, essencePerSecond: 0, goldPerSecond: 0 };
|
||||
}
|
||||
|
||||
const upgradeMultiplier = state.upgrades.
|
||||
filter((upgrade) => {
|
||||
const isGlobal = upgrade.target === "global";
|
||||
const isThisAdventurer
|
||||
= upgrade.target === "adventurer"
|
||||
&& upgrade.adventurerId === adventurerId;
|
||||
return upgrade.purchased && (isGlobal || isThisAdventurer);
|
||||
}).
|
||||
reduce((mult, upgrade) => {
|
||||
return mult * upgrade.multiplier;
|
||||
}, 1);
|
||||
|
||||
const equippedItems = state.equipment.filter((item) => {
|
||||
return item.equipped;
|
||||
});
|
||||
const equipmentGoldMultiplier = equippedItems.reduce((mult, item) => {
|
||||
return mult * (item.bonus.goldMultiplier ?? 1);
|
||||
}, 1);
|
||||
const equipmentCombatMultiplier = equippedItems.reduce((mult, item) => {
|
||||
return mult * (item.bonus.combatMultiplier ?? 1);
|
||||
}, 1);
|
||||
const equippedItemIds = equippedItems.map((item) => {
|
||||
return item.id;
|
||||
});
|
||||
const setBonuses = computeSetBonuses(equippedItemIds, EQUIPMENT_SETS);
|
||||
|
||||
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
|
||||
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
|
||||
const prestigeCombatMultiplier = PRESTIGE_COMBAT_BASE ** state.prestige.count;
|
||||
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
||||
const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1;
|
||||
const craftedGoldMultiplier
|
||||
= state.exploration?.craftedGoldMultiplier ?? 1;
|
||||
const craftedEssenceMultiplier
|
||||
= state.exploration?.craftedEssenceMultiplier ?? 1;
|
||||
const craftedCombatMultiplier
|
||||
= state.exploration?.craftedCombatMultiplier ?? 1;
|
||||
|
||||
const companionBonus = getActiveCompanionBonus(
|
||||
state.companions?.activeCompanionId,
|
||||
state.companions?.unlockedCompanionIds ?? [],
|
||||
);
|
||||
const companionGoldMult
|
||||
= companionBonus?.type === "passiveGold"
|
||||
? 1 + companionBonus.value
|
||||
: 1;
|
||||
const companionEssenceMult
|
||||
= companionBonus?.type === "essenceIncome"
|
||||
? 1 + companionBonus.value
|
||||
: 1;
|
||||
const companionCombatMult
|
||||
= companionBonus?.type === "bossDamage"
|
||||
? 1 + companionBonus.value
|
||||
: 1;
|
||||
|
||||
const goldPerSecond
|
||||
= adventurer.goldPerSecond
|
||||
* upgradeMultiplier
|
||||
* state.prestige.productionMultiplier
|
||||
* runestonesIncome
|
||||
* echoIncome
|
||||
* equipmentGoldMultiplier
|
||||
* setBonuses.goldMultiplier
|
||||
* craftedGoldMultiplier
|
||||
* companionGoldMult;
|
||||
|
||||
const essencePerSecond
|
||||
= adventurer.essencePerSecond
|
||||
* upgradeMultiplier
|
||||
* state.prestige.productionMultiplier
|
||||
* runestonesEssence
|
||||
* craftedEssenceMultiplier
|
||||
* companionEssenceMult;
|
||||
|
||||
const combatPower
|
||||
= adventurer.combatPower
|
||||
* upgradeMultiplier
|
||||
* prestigeCombatMultiplier
|
||||
* equipmentCombatMultiplier
|
||||
* setBonuses.combatMultiplier
|
||||
* echoCombatMultiplier
|
||||
* craftedCombatMultiplier
|
||||
* companionCombatMult;
|
||||
|
||||
return { combatPower, essencePerSecond, goldPerSecond };
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the party's total combat power, applying all active multipliers
|
||||
* (upgrades, prestige, equipment, set bonuses, echo, crafted, companion).
|
||||
* This mirrors the server-side calculatePartyStats in boss.ts and is the
|
||||
* single source of truth for all combat-power checks in the client:
|
||||
* - Displayed as "Combat Power" in the resource bar
|
||||
* - Displayed as "Party DPS" in the boss panel
|
||||
* - Used to gate quest availability
|
||||
* Note: the active companion's bossDamage bonus is intentionally included
|
||||
* here, as it applies to the full combat power calculation (boss fights and
|
||||
* quest gating alike), matching the server-side behaviour.
|
||||
* @param state - The current game state.
|
||||
* @returns The total party combat power.
|
||||
*/
|
||||
export const computePartyCombatPower = (state: GameState): number => {
|
||||
let globalMultiplier = 1;
|
||||
for (const upgrade of state.upgrades) {
|
||||
if (upgrade.purchased && upgrade.target === "global") {
|
||||
globalMultiplier = globalMultiplier * upgrade.multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
const prestigeMultiplier = PRESTIGE_COMBAT_BASE ** state.prestige.count;
|
||||
|
||||
const equipmentCombatMultiplier = state.equipment.
|
||||
filter((item) => {
|
||||
return item.equipped && item.bonus.combatMultiplier !== undefined;
|
||||
}).
|
||||
reduce((mult, item) => {
|
||||
return mult * (item.bonus.combatMultiplier ?? 1);
|
||||
}, 1);
|
||||
|
||||
const equippedItemIds = state.equipment.
|
||||
filter((item) => {
|
||||
return item.equipped;
|
||||
}).
|
||||
map((item) => {
|
||||
return item.id;
|
||||
});
|
||||
const { combatMultiplier: setCombatMultiplier } = computeSetBonuses(
|
||||
equippedItemIds,
|
||||
EQUIPMENT_SETS,
|
||||
);
|
||||
|
||||
const echoCombatMultiplier
|
||||
= state.transcendence?.echoCombatMultiplier ?? 1;
|
||||
const craftedCombatMultiplier
|
||||
= state.exploration?.craftedCombatMultiplier ?? 1;
|
||||
|
||||
const companionBonus = getActiveCompanionBonus(
|
||||
state.companions?.activeCompanionId,
|
||||
state.companions?.unlockedCompanionIds ?? [],
|
||||
);
|
||||
const companionCombatMult
|
||||
= companionBonus?.type === "bossDamage"
|
||||
? 1 + companionBonus.value
|
||||
: 1;
|
||||
|
||||
let partyCombatPower = 0;
|
||||
for (const adventurer of state.adventurers) {
|
||||
if (adventurer.count === 0) {
|
||||
continue;
|
||||
}
|
||||
let adventurerMultiplier = 1;
|
||||
for (const upgrade of state.upgrades) {
|
||||
if (
|
||||
upgrade.purchased
|
||||
&& upgrade.target === "adventurer"
|
||||
&& upgrade.adventurerId === adventurer.id
|
||||
) {
|
||||
adventurerMultiplier = adventurerMultiplier * upgrade.multiplier;
|
||||
}
|
||||
}
|
||||
const contribution
|
||||
= adventurer.combatPower
|
||||
* adventurer.count
|
||||
* adventurerMultiplier
|
||||
* globalMultiplier
|
||||
* prestigeMultiplier;
|
||||
partyCombatPower = partyCombatPower + contribution;
|
||||
}
|
||||
|
||||
return partyCombatPower
|
||||
* equipmentCombatMultiplier
|
||||
* setCombatMultiplier
|
||||
* echoCombatMultiplier
|
||||
* craftedCombatMultiplier
|
||||
* companionCombatMult;
|
||||
};
|
||||
|
||||
const basePrestigeThreshold = 1_000_000;
|
||||
const runestonesPerPrestigeLevelClient = 15;
|
||||
const maxBaseRunestones = 200;
|
||||
|
||||
/**
|
||||
* Computes the projected runestone reward if the player were to prestige right now.
|
||||
* Mirrors the server-side calculateRunestones formula exactly.
|
||||
* @param state - The current game state.
|
||||
* @returns The number of runestones the player would earn from a prestige now.
|
||||
*/
|
||||
export const computeProjectedRunestones = (state: GameState): number => {
|
||||
const { count, purchasedUpgradeIds } = state.prestige;
|
||||
const threshold = basePrestigeThreshold * Math.pow(count + 1, 2);
|
||||
const base = Math.min(
|
||||
Math.floor(Math.cbrt(state.player.totalGoldEarned / threshold))
|
||||
* runestonesPerPrestigeLevelClient,
|
||||
maxBaseRunestones,
|
||||
);
|
||||
const gain1Mult = purchasedUpgradeIds.includes("runestone_gain_1")
|
||||
? 1.25
|
||||
: 1;
|
||||
const gain2Mult = purchasedUpgradeIds.includes("runestone_gain_2")
|
||||
? 1.5
|
||||
: 1;
|
||||
const runestoneMult = gain1Mult * gain2Mult;
|
||||
const echoMult: number
|
||||
= state.transcendence?.echoPrestigeRunestoneMultiplier ?? 1;
|
||||
return Math.floor(base * runestoneMult * echoMult);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pure function — applies one game tick to the state.
|
||||
* DeltaSeconds: time elapsed since last tick.
|
||||
@@ -469,6 +754,19 @@ export const applyTick = (
|
||||
challengeCrystals = result.crystalsAwarded;
|
||||
}
|
||||
|
||||
// Auto-unlock adventurer-specific upgrades when their adventurer is recruited
|
||||
updatedUpgrades = updatedUpgrades.map((upgrade) => {
|
||||
if (upgrade.unlocked || upgrade.adventurerId === undefined) {
|
||||
return upgrade;
|
||||
}
|
||||
const adventurer = updatedAdventurers.find((a) => {
|
||||
return a.id === upgrade.adventurerId;
|
||||
});
|
||||
return adventurer !== undefined && adventurer.count > 0
|
||||
? { ...upgrade, unlocked: true }
|
||||
: upgrade;
|
||||
});
|
||||
|
||||
const goldValue = capResource(state.resources.gold + goldGained + questGold);
|
||||
const essenceValue = capResource(
|
||||
state.resources.essence + essenceGained + questEssence,
|
||||
@@ -489,6 +787,23 @@ export const applyTick = (
|
||||
...updatedDailyChallenges === undefined
|
||||
? {}
|
||||
: { dailyChallenges: updatedDailyChallenges },
|
||||
...newlyUnlockedZoneIds.size === 0 || state.exploration === undefined
|
||||
? {}
|
||||
: {
|
||||
exploration: {
|
||||
...state.exploration,
|
||||
areas: state.exploration.areas.map((area) => {
|
||||
const areaDefinition = EXPLORATION_AREAS.find((definition) => {
|
||||
return definition.id === area.id;
|
||||
});
|
||||
return areaDefinition !== undefined
|
||||
&& newlyUnlockedZoneIds.has(areaDefinition.zoneId)
|
||||
&& area.status === "locked"
|
||||
? { ...area, status: "available" as const }
|
||||
: area;
|
||||
}),
|
||||
},
|
||||
},
|
||||
adventurers: updatedAdventurers,
|
||||
bosses: updatedBosses,
|
||||
equipment: updatedEquipmentReference,
|
||||
@@ -502,24 +817,30 @@ export const applyTick = (
|
||||
zones: updatedZones,
|
||||
};
|
||||
|
||||
// Check achievements and apply crystal rewards for newly unlocked ones
|
||||
// Check achievements and apply crystal and runestone rewards for newly unlocked ones
|
||||
const updatedAchievements = checkAchievements(partialState);
|
||||
const crystalsFromAchievements = updatedAchievements.reduce(
|
||||
(sum, achievement, index) => {
|
||||
const wasLocked = state.achievements[index]?.unlockedAt === null;
|
||||
const isNowUnlocked = achievement.unlockedAt !== null;
|
||||
if (wasLocked && isNowUnlocked) {
|
||||
return sum + (achievement.reward?.crystals ?? 0);
|
||||
}
|
||||
return sum;
|
||||
},
|
||||
0,
|
||||
);
|
||||
let crystalsFromAchievements = 0;
|
||||
let runestonesFromAchievements = 0;
|
||||
for (const [ index, achievement ] of updatedAchievements.entries()) {
|
||||
const wasLocked = state.achievements[index]?.unlockedAt === null;
|
||||
const isNowUnlocked = achievement.unlockedAt !== null;
|
||||
if (wasLocked && isNowUnlocked) {
|
||||
crystalsFromAchievements
|
||||
= crystalsFromAchievements + (achievement.reward?.crystals ?? 0);
|
||||
runestonesFromAchievements
|
||||
= runestonesFromAchievements + (achievement.reward?.runestones ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...partialState,
|
||||
achievements: updatedAchievements,
|
||||
resources: {
|
||||
prestige: {
|
||||
...partialState.prestige,
|
||||
runestones:
|
||||
partialState.prestige.runestones + runestonesFromAchievements,
|
||||
},
|
||||
resources: {
|
||||
...partialState.resources,
|
||||
crystals: capResource(
|
||||
partialState.resources.crystals + crystalsFromAchievements,
|
||||
|
||||
Reference in New Issue
Block a user