generated from nhcarrigan/template
@@ -642,6 +642,14 @@ const patchAdventurerStats = (state: GameState): number => {
|
||||
if (defaultAdventurer === undefined) {
|
||||
continue;
|
||||
}
|
||||
const hasChanged
|
||||
= savedAdventurer.baseCost !== defaultAdventurer.baseCost
|
||||
|| savedAdventurer.class !== defaultAdventurer.class
|
||||
|| savedAdventurer.combatPower !== defaultAdventurer.combatPower
|
||||
|| savedAdventurer.essencePerSecond !== defaultAdventurer.essencePerSecond
|
||||
|| savedAdventurer.goldPerSecond !== defaultAdventurer.goldPerSecond
|
||||
|| savedAdventurer.level !== defaultAdventurer.level
|
||||
|| savedAdventurer.name !== defaultAdventurer.name;
|
||||
savedAdventurer.baseCost = defaultAdventurer.baseCost;
|
||||
savedAdventurer.class = defaultAdventurer.class;
|
||||
savedAdventurer.combatPower = defaultAdventurer.combatPower;
|
||||
@@ -649,7 +657,9 @@ const patchAdventurerStats = (state: GameState): number => {
|
||||
savedAdventurer.goldPerSecond = defaultAdventurer.goldPerSecond;
|
||||
savedAdventurer.level = defaultAdventurer.level;
|
||||
savedAdventurer.name = defaultAdventurer.name;
|
||||
patched = patched + 1;
|
||||
if (hasChanged) {
|
||||
patched = patched + 1;
|
||||
}
|
||||
}
|
||||
return patched;
|
||||
};
|
||||
@@ -670,6 +680,15 @@ const patchQuestStats = (state: GameState): number => {
|
||||
if (defaultQuest === undefined) {
|
||||
continue;
|
||||
}
|
||||
const savedPrereqs = JSON.stringify(savedQuest.prerequisiteIds);
|
||||
const defaultPrereqs = JSON.stringify(defaultQuest.prerequisiteIds);
|
||||
const hasChanged
|
||||
= savedQuest.name !== defaultQuest.name
|
||||
|| savedQuest.description !== defaultQuest.description
|
||||
|| savedQuest.durationSeconds !== defaultQuest.durationSeconds
|
||||
|| savedPrereqs !== defaultPrereqs
|
||||
|| savedQuest.zoneId !== defaultQuest.zoneId
|
||||
|| savedQuest.combatPowerRequired !== defaultQuest.combatPowerRequired;
|
||||
savedQuest.name = defaultQuest.name;
|
||||
savedQuest.description = defaultQuest.description;
|
||||
savedQuest.durationSeconds = defaultQuest.durationSeconds;
|
||||
@@ -678,7 +697,9 @@ const patchQuestStats = (state: GameState): number => {
|
||||
if (defaultQuest.combatPowerRequired !== undefined) {
|
||||
savedQuest.combatPowerRequired = defaultQuest.combatPowerRequired;
|
||||
}
|
||||
patched = patched + 1;
|
||||
if (hasChanged) {
|
||||
patched = patched + 1;
|
||||
}
|
||||
}
|
||||
return patched;
|
||||
};
|
||||
@@ -689,6 +710,7 @@ const patchQuestStats = (state: GameState): number => {
|
||||
* @param state - The player's current game state (mutated in place).
|
||||
* @returns The number of boss entries whose stats were updated.
|
||||
*/
|
||||
/* eslint-disable-next-line complexity, max-statements -- Comparing many boss stat fields for change detection */
|
||||
const patchBossStats = (state: GameState): number => {
|
||||
const defaultBossMap = new Map(defaultBosses.map((boss) => {
|
||||
return [ boss.id, boss ] as const;
|
||||
@@ -699,6 +721,20 @@ const patchBossStats = (state: GameState): number => {
|
||||
if (defaultBoss === undefined) {
|
||||
continue;
|
||||
}
|
||||
const savedRewards = JSON.stringify(savedBoss.equipmentRewards);
|
||||
const defaultRewards = JSON.stringify(defaultBoss.equipmentRewards);
|
||||
const hasChanged
|
||||
= savedBoss.name !== defaultBoss.name
|
||||
|| savedBoss.description !== defaultBoss.description
|
||||
|| savedBoss.maxHp !== defaultBoss.maxHp
|
||||
|| savedBoss.damagePerSecond !== defaultBoss.damagePerSecond
|
||||
|| savedBoss.goldReward !== defaultBoss.goldReward
|
||||
|| savedBoss.essenceReward !== defaultBoss.essenceReward
|
||||
|| savedBoss.crystalReward !== defaultBoss.crystalReward
|
||||
|| savedRewards !== defaultRewards
|
||||
|| savedBoss.prestigeRequirement !== defaultBoss.prestigeRequirement
|
||||
|| savedBoss.zoneId !== defaultBoss.zoneId
|
||||
|| savedBoss.bountyRunestones !== defaultBoss.bountyRunestones;
|
||||
savedBoss.name = defaultBoss.name;
|
||||
savedBoss.description = defaultBoss.description;
|
||||
savedBoss.maxHp = defaultBoss.maxHp;
|
||||
@@ -710,7 +746,9 @@ const patchBossStats = (state: GameState): number => {
|
||||
savedBoss.prestigeRequirement = defaultBoss.prestigeRequirement;
|
||||
savedBoss.zoneId = defaultBoss.zoneId;
|
||||
savedBoss.bountyRunestones = defaultBoss.bountyRunestones;
|
||||
patched = patched + 1;
|
||||
if (hasChanged) {
|
||||
patched = patched + 1;
|
||||
}
|
||||
}
|
||||
return patched;
|
||||
};
|
||||
@@ -731,12 +769,20 @@ const patchZoneStats = (state: GameState): number => {
|
||||
if (defaultZone === undefined) {
|
||||
continue;
|
||||
}
|
||||
const hasChanged
|
||||
= savedZone.name !== defaultZone.name
|
||||
|| savedZone.description !== defaultZone.description
|
||||
|| savedZone.emoji !== defaultZone.emoji
|
||||
|| savedZone.unlockBossId !== defaultZone.unlockBossId
|
||||
|| savedZone.unlockQuestId !== defaultZone.unlockQuestId;
|
||||
savedZone.name = defaultZone.name;
|
||||
savedZone.description = defaultZone.description;
|
||||
savedZone.emoji = defaultZone.emoji;
|
||||
savedZone.unlockBossId = defaultZone.unlockBossId;
|
||||
savedZone.unlockQuestId = defaultZone.unlockQuestId;
|
||||
patched = patched + 1;
|
||||
if (hasChanged) {
|
||||
patched = patched + 1;
|
||||
}
|
||||
}
|
||||
return patched;
|
||||
};
|
||||
@@ -747,6 +793,7 @@ const patchZoneStats = (state: GameState): number => {
|
||||
* @param state - The player's current game state (mutated in place).
|
||||
* @returns The number of upgrade entries whose stats were updated.
|
||||
*/
|
||||
/* eslint-disable-next-line complexity -- Comparing many upgrade stat fields for change detection */
|
||||
const patchUpgradeStats = (state: GameState): number => {
|
||||
const defaultUpgradeMap = new Map(defaultUpgrades.map((upgrade) => {
|
||||
return [ upgrade.id, upgrade ] as const;
|
||||
@@ -757,6 +804,15 @@ const patchUpgradeStats = (state: GameState): number => {
|
||||
if (defaultUpgrade === undefined) {
|
||||
continue;
|
||||
}
|
||||
const hasChanged
|
||||
= savedUpgrade.name !== defaultUpgrade.name
|
||||
|| savedUpgrade.description !== defaultUpgrade.description
|
||||
|| savedUpgrade.target !== defaultUpgrade.target
|
||||
|| savedUpgrade.adventurerId !== defaultUpgrade.adventurerId
|
||||
|| savedUpgrade.multiplier !== defaultUpgrade.multiplier
|
||||
|| savedUpgrade.costGold !== defaultUpgrade.costGold
|
||||
|| savedUpgrade.costEssence !== defaultUpgrade.costEssence
|
||||
|| savedUpgrade.costCrystals !== defaultUpgrade.costCrystals;
|
||||
savedUpgrade.name = defaultUpgrade.name;
|
||||
savedUpgrade.description = defaultUpgrade.description;
|
||||
savedUpgrade.target = defaultUpgrade.target;
|
||||
@@ -767,7 +823,9 @@ const patchUpgradeStats = (state: GameState): number => {
|
||||
savedUpgrade.costGold = defaultUpgrade.costGold;
|
||||
savedUpgrade.costEssence = defaultUpgrade.costEssence;
|
||||
savedUpgrade.costCrystals = defaultUpgrade.costCrystals;
|
||||
patched = patched + 1;
|
||||
if (hasChanged) {
|
||||
patched = patched + 1;
|
||||
}
|
||||
}
|
||||
return patched;
|
||||
};
|
||||
@@ -778,6 +836,7 @@ const patchUpgradeStats = (state: GameState): number => {
|
||||
* @param state - The player's current game state (mutated in place).
|
||||
* @returns The number of equipment entries whose stats were updated.
|
||||
*/
|
||||
/* eslint-disable-next-line complexity, max-statements -- Comparing many equipment stat fields for change detection */
|
||||
const patchEquipmentStats = (state: GameState): number => {
|
||||
const defaultEquipmentMap = new Map(defaultEquipment.map((item) => {
|
||||
return [ item.id, item ] as const;
|
||||
@@ -788,6 +847,18 @@ const patchEquipmentStats = (state: GameState): number => {
|
||||
if (defaultItem === undefined) {
|
||||
continue;
|
||||
}
|
||||
const savedBonus = JSON.stringify(savedItem.bonus);
|
||||
const defaultBonus = JSON.stringify(defaultItem.bonus);
|
||||
const savedCost = JSON.stringify(savedItem.cost);
|
||||
const defaultCost = JSON.stringify(defaultItem.cost);
|
||||
const hasChanged
|
||||
= savedItem.name !== defaultItem.name
|
||||
|| savedItem.description !== defaultItem.description
|
||||
|| savedItem.type !== defaultItem.type
|
||||
|| savedItem.rarity !== defaultItem.rarity
|
||||
|| savedBonus !== defaultBonus
|
||||
|| savedCost !== defaultCost
|
||||
|| savedItem.setId !== defaultItem.setId;
|
||||
savedItem.name = defaultItem.name;
|
||||
savedItem.description = defaultItem.description;
|
||||
savedItem.type = defaultItem.type;
|
||||
@@ -799,7 +870,9 @@ const patchEquipmentStats = (state: GameState): number => {
|
||||
if (defaultItem.setId !== undefined) {
|
||||
savedItem.setId = defaultItem.setId;
|
||||
}
|
||||
patched = patched + 1;
|
||||
if (hasChanged) {
|
||||
patched = patched + 1;
|
||||
}
|
||||
}
|
||||
return patched;
|
||||
};
|
||||
@@ -820,6 +893,16 @@ const patchAchievementStats = (state: GameState): number => {
|
||||
if (defaultAchievement === undefined) {
|
||||
continue;
|
||||
}
|
||||
const savedCondition = JSON.stringify(savedAchievement.condition);
|
||||
const defaultCondition = JSON.stringify(defaultAchievement.condition);
|
||||
const savedReward = JSON.stringify(savedAchievement.reward);
|
||||
const defaultReward = JSON.stringify(defaultAchievement.reward);
|
||||
const hasChanged
|
||||
= savedAchievement.name !== defaultAchievement.name
|
||||
|| savedAchievement.description !== defaultAchievement.description
|
||||
|| savedAchievement.icon !== defaultAchievement.icon
|
||||
|| savedCondition !== defaultCondition
|
||||
|| savedReward !== defaultReward;
|
||||
savedAchievement.name = defaultAchievement.name;
|
||||
savedAchievement.description = defaultAchievement.description;
|
||||
savedAchievement.icon = defaultAchievement.icon;
|
||||
@@ -827,7 +910,9 @@ const patchAchievementStats = (state: GameState): number => {
|
||||
if (defaultAchievement.reward !== undefined) {
|
||||
savedAchievement.reward = { ...defaultAchievement.reward };
|
||||
}
|
||||
patched = patched + 1;
|
||||
if (hasChanged) {
|
||||
patched = patched + 1;
|
||||
}
|
||||
}
|
||||
return patched;
|
||||
};
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
/* eslint-disable max-lines -- Boss panel with sub-component and helper function */
|
||||
import { type JSX, useState } from "react";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import { computePartyCombatPower } from "../../engine/tick.js";
|
||||
import { cdnImage } from "../../utils/cdn.js";
|
||||
import { LockToggle } from "../ui/lockToggle.js";
|
||||
import { ZoneSelector } from "./zoneSelector.js";
|
||||
import type { Boss, GameState } from "@elysium/types";
|
||||
import type { Boss } from "@elysium/types";
|
||||
|
||||
interface BossCardProperties {
|
||||
readonly boss: Boss;
|
||||
@@ -157,72 +158,6 @@ const BossCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes party DPS and HP from the current game state.
|
||||
* @param state - The full game state.
|
||||
* @returns The computed party DPS and HP values.
|
||||
*/
|
||||
const computePartyStats = (
|
||||
state: GameState,
|
||||
): {
|
||||
partyDps: number;
|
||||
partyHp: number;
|
||||
} => {
|
||||
const { upgrades, adventurers, equipment, prestige } = state;
|
||||
let globalMultiplier = 1;
|
||||
for (const upgrade of upgrades) {
|
||||
const { purchased, target, multiplier } = upgrade;
|
||||
if (purchased && target === "global") {
|
||||
globalMultiplier = globalMultiplier * multiplier;
|
||||
}
|
||||
}
|
||||
const prestigeBonus = prestige.count * 0.1;
|
||||
const prestigeMultiplier = 1 + prestigeBonus;
|
||||
const equipmentCombatMultiplier = equipment.
|
||||
filter((item) => {
|
||||
return item.equipped && item.bonus.combatMultiplier !== undefined;
|
||||
}).
|
||||
reduce((multiplier, item) => {
|
||||
return multiplier * (item.bonus.combatMultiplier ?? 1);
|
||||
}, 1);
|
||||
|
||||
let partyDps = 0;
|
||||
let partyHp = 0;
|
||||
for (const adventurer of adventurers) {
|
||||
const { count, id: adventurerId, combatPower, level } = adventurer;
|
||||
if (count === 0) {
|
||||
continue;
|
||||
}
|
||||
let adventurerMultiplier = 1;
|
||||
for (const upgrade of upgrades) {
|
||||
const {
|
||||
purchased,
|
||||
target,
|
||||
multiplier,
|
||||
adventurerId: upgradeAdventurerId,
|
||||
} = upgrade;
|
||||
if (
|
||||
purchased
|
||||
&& target === "adventurer"
|
||||
&& upgradeAdventurerId === adventurerId
|
||||
) {
|
||||
adventurerMultiplier = adventurerMultiplier * multiplier;
|
||||
}
|
||||
}
|
||||
const dps
|
||||
= combatPower
|
||||
* count
|
||||
* adventurerMultiplier
|
||||
* globalMultiplier
|
||||
* prestigeMultiplier;
|
||||
partyDps = partyDps + dps;
|
||||
const hp = level * 50 * count;
|
||||
partyHp = partyHp + hp;
|
||||
}
|
||||
partyDps = partyDps * equipmentCombatMultiplier;
|
||||
return { partyDps, partyHp };
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the boss panel with zone selection and boss list.
|
||||
* @returns The JSX element.
|
||||
@@ -266,7 +201,14 @@ const BossPanel = (): JSX.Element => {
|
||||
void handleChallenge(bossId);
|
||||
}
|
||||
|
||||
const { zones, bosses, quests, autoBoss, prestige: playerPrestige } = state;
|
||||
const {
|
||||
adventurers,
|
||||
autoBoss,
|
||||
bosses,
|
||||
prestige: playerPrestige,
|
||||
quests,
|
||||
zones,
|
||||
} = state;
|
||||
|
||||
const activeZone = zones.find((zone) => {
|
||||
return zone.id === activeZoneId;
|
||||
@@ -349,7 +291,12 @@ const BossPanel = (): JSX.Element => {
|
||||
}
|
||||
|
||||
const autoBossOn = autoBoss === true;
|
||||
const { partyDps, partyHp } = computePartyStats(state);
|
||||
const partyDps = computePartyCombatPower(state);
|
||||
let partyHp = 0;
|
||||
for (const { level, count } of adventurers) {
|
||||
// eslint-disable-next-line stylistic/no-mixed-operators -- level * 50 * count is clear
|
||||
partyHp = partyHp + level * 50 * count;
|
||||
}
|
||||
const { count: prestigeCount } = playerPrestige;
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
/* eslint-disable react/no-multi-comp -- Sub-component is tightly coupled to the panel */
|
||||
/* eslint-disable max-lines-per-function -- Complex component with many render paths */
|
||||
/* eslint-disable complexity -- UpgradeCard has many conditional render paths for states */
|
||||
/* eslint-disable max-statements -- UpgradePanel builds hints from three sources */
|
||||
/* eslint-disable max-lines -- Upgrade panel with sub-component exceeds line limit */
|
||||
import { type JSX, useState } from "react";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import { cdnImage } from "../../utils/cdn.js";
|
||||
@@ -238,6 +240,22 @@ const UpgradePanel = (): JSX.Element => {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const upgrade of locked) {
|
||||
if (
|
||||
!upgradeUnlockHints.has(upgrade.id)
|
||||
&& upgrade.adventurerId !== undefined
|
||||
) {
|
||||
const adventurerForHint = adventurers.find((a) => {
|
||||
return a.id === upgrade.adventurerId;
|
||||
});
|
||||
if (adventurerForHint !== undefined) {
|
||||
upgradeUnlockHints.set(
|
||||
upgrade.id,
|
||||
`🗡️ Recruit: ${adventurerForHint.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggle(): void {
|
||||
setShowLocked((current) => {
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
/* eslint-disable complexity -- Many conditional resource and badge render paths */
|
||||
import { useState, type FocusEvent, type JSX } from "react";
|
||||
import { useGame } from "../../context/gameContext.js";
|
||||
import { RESOURCE_CAP, computeGoldPerSecond } from "../../engine/tick.js";
|
||||
import {
|
||||
RESOURCE_CAP,
|
||||
computeEssencePerSecond,
|
||||
computeGoldPerSecond,
|
||||
computePartyCombatPower,
|
||||
} from "../../engine/tick.js";
|
||||
import type { Resource } from "@elysium/types";
|
||||
|
||||
interface ResourceBarProperties {
|
||||
@@ -83,12 +88,11 @@ const ResourceBar = ({
|
||||
const { gold, essence, crystals } = resources;
|
||||
let partyCombatPower = 0;
|
||||
let goldPerSecond = 0;
|
||||
let essencePerSecond = 0;
|
||||
if (state !== null) {
|
||||
for (const adventurer of state.adventurers) {
|
||||
const contribution = adventurer.combatPower * adventurer.count;
|
||||
partyCombatPower = partyCombatPower + contribution;
|
||||
}
|
||||
partyCombatPower = computePartyCombatPower(state);
|
||||
goldPerSecond = computeGoldPerSecond(state);
|
||||
essencePerSecond = computeEssencePerSecond(state);
|
||||
}
|
||||
|
||||
let avatarUrl: string | null = null;
|
||||
@@ -182,6 +186,13 @@ const ResourceBar = ({
|
||||
</span>
|
||||
<span className="resource-label">{"Gold/s"}</span>
|
||||
</div>
|
||||
<div className="resource">
|
||||
<span className="resource-icon">{"⚡"}</span>
|
||||
<span className="resource-value">
|
||||
{formatNumber(essencePerSecond)}
|
||||
</span>
|
||||
<span className="resource-label">{"Essence/s"}</span>
|
||||
</div>
|
||||
<div className={`resource${essenceFull
|
||||
? " resource-full"
|
||||
: ""}`}>
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
RESOURCE_CAP,
|
||||
applyTick,
|
||||
calculateClickPower,
|
||||
computePartyCombatPower,
|
||||
} from "../engine/tick.js";
|
||||
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
|
||||
import { formatNumber as formatNumberUtil } from "../utils/format.js";
|
||||
@@ -1078,11 +1079,7 @@ export const GameProvider = ({
|
||||
return q.status === "active";
|
||||
});
|
||||
if (!hasActiveQuest) {
|
||||
// eslint-disable-next-line unicorn/no-array-reduce -- Need the total!
|
||||
const partyCombatPower = next.adventurers.reduce((total, a) => {
|
||||
const power = total + a.combatPower;
|
||||
return power * a.count;
|
||||
}, 0);
|
||||
const partyCombatPower = computePartyCombatPower(next);
|
||||
const zoneOrder = new Map(
|
||||
next.zones.map((z, index) => {
|
||||
return [ z.id, index ];
|
||||
@@ -1120,14 +1117,31 @@ export const GameProvider = ({
|
||||
next.autoAdventurer === true
|
||||
&& next.prestige.purchasedUpgradeIds.includes("auto_adventurer")
|
||||
) {
|
||||
const maxAdventurerLevel = Math.max(
|
||||
...next.adventurers.
|
||||
filter((a) => {
|
||||
return a.unlocked;
|
||||
}).
|
||||
map((a) => {
|
||||
return a.level;
|
||||
}),
|
||||
);
|
||||
const autoBuyCap = 100;
|
||||
const [ bestAdventurer ] = next.adventurers.
|
||||
filter((adventurer) => {
|
||||
const cost
|
||||
= adventurer.baseCost * Math.pow(1.15, adventurer.count);
|
||||
return adventurer.unlocked && next.resources.gold >= cost;
|
||||
const isMaxTier = adventurer.level === maxAdventurerLevel;
|
||||
const withinCap
|
||||
= isMaxTier || adventurer.count < autoBuyCap;
|
||||
return (
|
||||
adventurer.unlocked
|
||||
&& next.resources.gold >= cost
|
||||
&& withinCap
|
||||
);
|
||||
}).
|
||||
sort((adventurerA, adventurerB) => {
|
||||
return adventurerB.combatPower - adventurerA.combatPower;
|
||||
return adventurerB.level - adventurerA.level;
|
||||
});
|
||||
if (bestAdventurer !== undefined) {
|
||||
const purchaseCost
|
||||
@@ -1346,6 +1360,13 @@ export const GameProvider = ({
|
||||
}
|
||||
return afterBoss;
|
||||
});
|
||||
|
||||
/*
|
||||
* Boss fight modifies server state; clear stale signature so
|
||||
* the next pre-save or auto-save does not send a mismatched one.
|
||||
*/
|
||||
signatureReference.current = null;
|
||||
localStorage.removeItem("elysium_save_signature");
|
||||
setAutoBossLastResult({
|
||||
at: Date.now(),
|
||||
bossName: bossName,
|
||||
|
||||
@@ -195,6 +195,138 @@ 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 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.
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line stylistic/no-mixed-operators -- prestige count * factor is clear
|
||||
const prestigeMultiplier = 1 + state.prestige.count * 0.1;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pure function — applies one game tick to the state.
|
||||
* DeltaSeconds: time elapsed since last tick.
|
||||
@@ -469,6 +601,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,
|
||||
|
||||
Reference in New Issue
Block a user