generated from nhcarrigan/template
ad4fcc2811
Closes #147: patch functions now detect actual changes before incrementing the patched counter, preventing inflated sync reports. Closes #149: computeEssencePerSecond exported from tick engine and shown in the resource bar dropdown alongside Gold/s. Closes #150: auto-buy now sorts adventurers by level descending for semantic clarity, ensuring highest-tier units are purchased first.
635 lines
20 KiB
TypeScript
635 lines
20 KiB
TypeScript
/**
|
|
* @file Game tick engine for Elysium.
|
|
* @copyright nhcarrigan
|
|
* @license Naomi's Public License
|
|
* @author Naomi Carrigan
|
|
*/
|
|
/* eslint-disable @typescript-eslint/naming-convention -- Snake_case IDs and SCREAMING_SNAKE are conventional for game data */
|
|
/* eslint-disable complexity -- Tick engine is inherently complex with many game systems */
|
|
/* eslint-disable max-lines-per-function -- Tick engine processes many systems in one pass for performance */
|
|
/* eslint-disable max-statements -- Tick engine requires many state variables across all game systems */
|
|
/* 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,
|
|
type Equipment,
|
|
type GameState,
|
|
computeSetBonuses,
|
|
getActiveCompanionBonus,
|
|
} from "@elysium/types";
|
|
import { EQUIPMENT_SETS } from "../data/equipmentSets.js";
|
|
import { updateChallengeProgress } from "../utils/dailyChallenges.js";
|
|
|
|
/**
|
|
* Checks all achievements against the current game state and returns an updated
|
|
* achievements array, marking newly-met conditions with the current timestamp.
|
|
* @param state - The current game state to check achievements against.
|
|
* @returns Updated achievements array with newly unlocked achievements timestamped.
|
|
*/
|
|
const checkAchievements = (state: GameState): Array<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((boss) => {
|
|
return boss.status === "defeated";
|
|
}).length >= condition.amount;
|
|
break;
|
|
case "questsCompleted":
|
|
met
|
|
= state.quests.filter((quest) => {
|
|
return quest.status === "completed";
|
|
}).length >= condition.amount;
|
|
break;
|
|
case "adventurerTotal":
|
|
met
|
|
= state.adventurers.reduce((sum, adventurer) => {
|
|
return sum + adventurer.count;
|
|
}, 0) >= condition.amount;
|
|
break;
|
|
case "prestigeCount":
|
|
met = state.prestige.count >= condition.amount;
|
|
break;
|
|
case "equipmentOwned":
|
|
met
|
|
= state.equipment.filter((item) => {
|
|
return item.owned;
|
|
}).length >= condition.amount;
|
|
break;
|
|
default:
|
|
/* V8 ignore next -- @preserve */ break;
|
|
}
|
|
|
|
return met
|
|
? { ...achievement, unlockedAt: now }
|
|
: achievement;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Maximum value any resource can accumulate to. Beyond this JS floats lose all useful precision.
|
|
*/
|
|
export const RESOURCE_CAP = 1e300;
|
|
|
|
/**
|
|
* Probability of quest failure per zone — scales from 10% (early game) to 40% (end game).
|
|
* On failure the quest resets to "available" with no rewards; the player must wait the
|
|
* full duration again on their next attempt.
|
|
*/
|
|
export const zoneFailureChance: Record<string, number> = {
|
|
abyssal_trench: 0.24,
|
|
astral_void: 0.2,
|
|
celestial_reaches: 0.22,
|
|
cosmic_maelstrom: 0.4,
|
|
crystalline_spire: 0.28,
|
|
eternal_throne: 0.32,
|
|
frozen_peaks: 0.14,
|
|
infernal_court: 0.26,
|
|
infinite_expanse: 0.36,
|
|
primeval_sanctum: 0.4,
|
|
primordial_chaos: 0.34,
|
|
reality_forge: 0.38,
|
|
shadow_marshes: 0.16,
|
|
shattered_ruins: 0.12,
|
|
the_absolute: 0.4,
|
|
verdant_vale: 0.1,
|
|
void_sanctum: 0.3,
|
|
volcanic_depths: 0.18,
|
|
};
|
|
|
|
/**
|
|
* Caps a resource value at RESOURCE_CAP.
|
|
* @param value - The resource value to cap.
|
|
* @returns The capped value.
|
|
*/
|
|
const capResource = (value: number): number => {
|
|
return Math.min(value, RESOURCE_CAP);
|
|
};
|
|
|
|
/**
|
|
* Pure function — applies one game tick to the state.
|
|
* DeltaSeconds: time elapsed since last tick.
|
|
* Returns a new GameState (does not mutate the original).
|
|
* @param state - The current game state.
|
|
* @param deltaSeconds - Time elapsed since last tick in seconds.
|
|
* @returns A new GameState with the tick applied.
|
|
*/
|
|
/**
|
|
* Computes the effective gold earned per second across all adventurers,
|
|
* including all active multipliers (upgrades, prestige, equipment, etc.).
|
|
* @param state - The current game state.
|
|
* @returns Gold per second as a number.
|
|
*/
|
|
export const computeGoldPerSecond = (state: GameState): number => {
|
|
const equippedItems: Array<Equipment> = state.equipment.filter((item) => {
|
|
return item.equipped;
|
|
});
|
|
const equipmentGoldMultiplier = equippedItems.reduce((mult, item) => {
|
|
return mult * (item.bonus.goldMultiplier ?? 1);
|
|
}, 1);
|
|
const setGoldMultiplier = computeSetBonuses(
|
|
equippedItems.map((item) => {
|
|
return item.id;
|
|
}),
|
|
EQUIPMENT_SETS,
|
|
).goldMultiplier;
|
|
|
|
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
|
|
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
|
const craftedGoldMultiplier = state.exploration?.craftedGoldMultiplier ?? 1;
|
|
const companionBonus = getActiveCompanionBonus(
|
|
state.companions?.activeCompanionId,
|
|
state.companions?.unlockedCompanionIds ?? [],
|
|
);
|
|
const companionGoldMult
|
|
= companionBonus?.type === "passiveGold"
|
|
? 1 + companionBonus.value
|
|
: 1;
|
|
|
|
let goldPerSecond = 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.goldPerSecond
|
|
* adventurer.count
|
|
* upgradeMultiplier
|
|
* state.prestige.productionMultiplier
|
|
* runestonesIncome
|
|
* echoIncome
|
|
* equipmentGoldMultiplier
|
|
* setGoldMultiplier
|
|
* craftedGoldMultiplier
|
|
* companionGoldMult;
|
|
goldPerSecond = goldPerSecond + contribution;
|
|
}
|
|
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;
|
|
};
|
|
|
|
/**
|
|
* Pure function — applies one game tick to the state.
|
|
* DeltaSeconds: time elapsed since last tick.
|
|
* Returns a new GameState (does not mutate the original).
|
|
* @param state - The current game state.
|
|
* @param deltaSeconds - Time elapsed since last tick in seconds.
|
|
* @returns A new GameState with the tick applied.
|
|
*/
|
|
export const applyTick = (
|
|
state: GameState,
|
|
deltaSeconds: number,
|
|
): GameState => {
|
|
const equippedItems: Array<Equipment> = state.equipment.filter((item) => {
|
|
return item.equipped;
|
|
});
|
|
const equipmentGoldMultiplier = equippedItems.reduce((mult, item) => {
|
|
return mult * (item.bonus.goldMultiplier ?? 1);
|
|
}, 1);
|
|
const setGoldMultiplier = computeSetBonuses(
|
|
equippedItems.map((item) => {
|
|
return item.id;
|
|
}),
|
|
EQUIPMENT_SETS,
|
|
).goldMultiplier;
|
|
|
|
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
|
|
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
|
|
const runestonesCrystal = state.prestige.runestonesCrystalMultiplier ?? 1;
|
|
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
|
const craftedGoldMultiplier = state.exploration?.craftedGoldMultiplier ?? 1;
|
|
const craftedEssenceMultiplier
|
|
= state.exploration?.craftedEssenceMultiplier ?? 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 companionQuestTimeReduction
|
|
= companionBonus?.type === "questTime"
|
|
? companionBonus.value
|
|
: 0;
|
|
|
|
let goldGained = 0;
|
|
let essenceGained = 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 prestige = state.prestige.productionMultiplier;
|
|
|
|
const goldPerTick
|
|
= adventurer.goldPerSecond
|
|
* adventurer.count
|
|
* upgradeMultiplier
|
|
* prestige
|
|
* runestonesIncome
|
|
* echoIncome
|
|
* equipmentGoldMultiplier
|
|
* setGoldMultiplier
|
|
* craftedGoldMultiplier
|
|
* companionGoldMult
|
|
* deltaSeconds;
|
|
goldGained = goldGained + goldPerTick;
|
|
|
|
const essencePerTick
|
|
= adventurer.essencePerSecond
|
|
* adventurer.count
|
|
* upgradeMultiplier
|
|
* prestige
|
|
* runestonesEssence
|
|
* craftedEssenceMultiplier
|
|
* companionEssenceMult
|
|
* deltaSeconds;
|
|
essenceGained = essenceGained + essencePerTick;
|
|
}
|
|
|
|
// 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 updatedEquipmentReference = state.equipment;
|
|
|
|
const updatedQuests = state.quests.map((quest) => {
|
|
const effectiveQuestMs
|
|
= quest.durationSeconds * (1 - companionQuestTimeReduction) * 1000;
|
|
if (
|
|
quest.status !== "active"
|
|
|| quest.startedAt === undefined
|
|
|| now < quest.startedAt + effectiveQuestMs
|
|
) {
|
|
return quest;
|
|
}
|
|
|
|
const failureChance = zoneFailureChance[quest.zoneId] ?? 0.2;
|
|
if (Math.random() < failureChance) {
|
|
const { startedAt: _dropped, ...questWithoutStartedAt } = quest;
|
|
return {
|
|
...questWithoutStartedAt,
|
|
lastFailedAt: now,
|
|
status: "available" as const,
|
|
};
|
|
}
|
|
|
|
for (const reward of quest.rewards) {
|
|
if (reward.type === "gold" && reward.amount !== undefined) {
|
|
questGold = questGold + reward.amount;
|
|
} else if (reward.type === "essence" && reward.amount !== undefined) {
|
|
questEssence = questEssence + reward.amount;
|
|
} else if (reward.type === "crystals" && reward.amount !== undefined) {
|
|
const crystalAmount = reward.amount;
|
|
const crystalGain = crystalAmount * runestonesCrystal;
|
|
questCrystals = questCrystals + crystalGain;
|
|
} else if (reward.type === "upgrade" && reward.targetId !== undefined) {
|
|
updatedUpgrades = updatedUpgrades.map((upgrade) => {
|
|
return upgrade.id === reward.targetId
|
|
? { ...upgrade, unlocked: true }
|
|
: upgrade;
|
|
});
|
|
} else if (
|
|
reward.type === "adventurer"
|
|
&& reward.targetId !== undefined
|
|
) {
|
|
updatedAdventurers = updatedAdventurers.map((adventurer) => {
|
|
return adventurer.id === reward.targetId
|
|
? { ...adventurer, unlocked: true }
|
|
: adventurer;
|
|
});
|
|
} else if (reward.type === "equipment" && reward.targetId !== undefined) {
|
|
const rewardTargetId = reward.targetId;
|
|
const currentEquipment = updatedEquipmentReference;
|
|
updatedEquipmentReference = currentEquipment.map((item) => {
|
|
if (item.id !== rewardTargetId) {
|
|
return item;
|
|
}
|
|
const slotEmpty = !currentEquipment.some((other) => {
|
|
return other.type === item.type && other.equipped;
|
|
});
|
|
return {
|
|
...item,
|
|
equipped: slotEmpty || item.equipped,
|
|
owned: true,
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
return { ...quest, status: "completed" as const };
|
|
});
|
|
|
|
// Unlock quests whose prerequisites are now all completed and whose zone is unlocked
|
|
const completedIds = new Set(
|
|
updatedQuests.
|
|
filter((quest) => {
|
|
return quest.status === "completed";
|
|
}).
|
|
map((quest) => {
|
|
return quest.id;
|
|
}),
|
|
);
|
|
const fullyUpdatedQuests = updatedQuests.map((quest) => {
|
|
if (quest.status !== "locked") {
|
|
return quest;
|
|
}
|
|
const questZone = state.zones.find((searchZone) => {
|
|
return searchZone.id === quest.zoneId;
|
|
});
|
|
if (questZone?.status === "locked") {
|
|
return quest;
|
|
}
|
|
if (
|
|
quest.prerequisiteIds.every((id) => {
|
|
return completedIds.has(id);
|
|
})
|
|
) {
|
|
return { ...quest, status: "available" as const };
|
|
}
|
|
return quest;
|
|
});
|
|
|
|
/*
|
|
* Unlock zones whose both conditions are now satisfied after quest completion:
|
|
* (1) the gate boss has been defeated, (2) the gate quest is now completed
|
|
*/
|
|
const updatedZones = state.zones.map((zone) => {
|
|
if (zone.status === "unlocked") {
|
|
return zone;
|
|
}
|
|
const bossOk
|
|
= zone.unlockBossId === null
|
|
|| state.bosses.some((boss) => {
|
|
return boss.id === zone.unlockBossId && boss.status === "defeated";
|
|
});
|
|
const questOk
|
|
= zone.unlockQuestId === null || completedIds.has(zone.unlockQuestId);
|
|
if (bossOk && questOk) {
|
|
return { ...zone, status: "unlocked" as const };
|
|
}
|
|
return zone;
|
|
});
|
|
|
|
// Activate the first boss in any zone that just became unlocked this tick
|
|
const newlyUnlockedZoneIds = new Set(
|
|
updatedZones.
|
|
filter((zone) => {
|
|
const wasLocked
|
|
= state.zones.find((originalZone) => {
|
|
return originalZone.id === zone.id;
|
|
})?.status === "locked";
|
|
return zone.status === "unlocked" && wasLocked;
|
|
}).
|
|
map((zone) => {
|
|
return zone.id;
|
|
}),
|
|
);
|
|
let updatedBosses = state.bosses;
|
|
if (newlyUnlockedZoneIds.size > 0) {
|
|
updatedBosses = state.bosses.map((boss) => {
|
|
if (newlyUnlockedZoneIds.has(boss.zoneId)) {
|
|
const zoneBosses = state.bosses.filter((zoneBoss) => {
|
|
return zoneBoss.zoneId === boss.zoneId;
|
|
});
|
|
const [ firstBoss ] = zoneBosses;
|
|
if (firstBoss?.id === boss.id && boss.status === "locked") {
|
|
return { ...boss, status: "available" as const };
|
|
}
|
|
}
|
|
return boss;
|
|
});
|
|
}
|
|
|
|
// Count quests newly completed this tick and update daily challenge progress
|
|
const newlyCompletedQuestCount = updatedQuests.filter((quest, index) => {
|
|
const wasNotCompleted = state.quests[index]?.status !== "completed";
|
|
return quest.status === "completed" && wasNotCompleted;
|
|
}).length;
|
|
|
|
let updatedDailyChallenges = state.dailyChallenges;
|
|
let challengeCrystals = 0;
|
|
if (updatedDailyChallenges !== undefined && newlyCompletedQuestCount > 0) {
|
|
const result = updateChallengeProgress(
|
|
updatedDailyChallenges,
|
|
"questsCompleted",
|
|
newlyCompletedQuestCount,
|
|
);
|
|
updatedDailyChallenges = result.updatedChallenges;
|
|
challengeCrystals = result.crystalsAwarded;
|
|
}
|
|
|
|
const goldValue = capResource(state.resources.gold + goldGained + questGold);
|
|
const essenceValue = capResource(
|
|
state.resources.essence + essenceGained + questEssence,
|
|
);
|
|
const totalGoldEarnedValue
|
|
= state.player.totalGoldEarned + goldGained + questGold;
|
|
|
|
const partialState: GameState = {
|
|
...state,
|
|
resources: {
|
|
...state.resources,
|
|
crystals: capResource(
|
|
state.resources.crystals + questCrystals + challengeCrystals,
|
|
),
|
|
essence: essenceValue,
|
|
gold: goldValue,
|
|
},
|
|
...updatedDailyChallenges === undefined
|
|
? {}
|
|
: { dailyChallenges: updatedDailyChallenges },
|
|
adventurers: updatedAdventurers,
|
|
bosses: updatedBosses,
|
|
equipment: updatedEquipmentReference,
|
|
lastTickAt: now,
|
|
player: {
|
|
...state.player,
|
|
totalGoldEarned: totalGoldEarnedValue,
|
|
},
|
|
quests: fullyUpdatedQuests,
|
|
upgrades: updatedUpgrades,
|
|
zones: updatedZones,
|
|
};
|
|
|
|
// Check achievements and apply crystal 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,
|
|
);
|
|
|
|
return {
|
|
...partialState,
|
|
achievements: updatedAchievements,
|
|
resources: {
|
|
...partialState.resources,
|
|
crystals: capResource(
|
|
partialState.resources.crystals + crystalsFromAchievements,
|
|
),
|
|
},
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Calculates the effective click power, including upgrades and equipped trinkets.
|
|
* @param state - The current game state.
|
|
* @returns The calculated click power value.
|
|
*/
|
|
export const calculateClickPower = (state: GameState): number => {
|
|
const clickMultiplier = state.upgrades.
|
|
filter((upgrade) => {
|
|
return upgrade.purchased && upgrade.target === "click";
|
|
}).
|
|
reduce((mult, upgrade) => {
|
|
return mult * upgrade.multiplier;
|
|
}, 1);
|
|
|
|
const equippedItems = state.equipment.filter((item) => {
|
|
return item.equipped;
|
|
});
|
|
const equipmentClickMultiplier = equippedItems.
|
|
filter((item) => {
|
|
return item.bonus.clickMultiplier !== undefined;
|
|
}).
|
|
reduce((mult, item) => {
|
|
return mult * (item.bonus.clickMultiplier ?? 1);
|
|
}, 1);
|
|
const setClickMultiplier = computeSetBonuses(
|
|
equippedItems.map((item) => {
|
|
return item.id;
|
|
}),
|
|
EQUIPMENT_SETS,
|
|
).clickMultiplier;
|
|
|
|
const runestonesClick = state.prestige.runestonesClickMultiplier ?? 1;
|
|
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
|
|
const craftedClickMultiplier = state.exploration?.craftedClickMultiplier ?? 1;
|
|
|
|
const companionClickBonus = getActiveCompanionBonus(
|
|
state.companions?.activeCompanionId,
|
|
state.companions?.unlockedCompanionIds ?? [],
|
|
);
|
|
const companionClickMult
|
|
= companionClickBonus?.type === "clickGold"
|
|
? 1 + companionClickBonus.value
|
|
: 1;
|
|
|
|
return (
|
|
state.baseClickPower
|
|
* clickMultiplier
|
|
* state.prestige.productionMultiplier
|
|
* runestonesClick
|
|
* echoIncome
|
|
* equipmentClickMultiplier
|
|
* setClickMultiplier
|
|
* craftedClickMultiplier
|
|
* companionClickMult
|
|
);
|
|
};
|