d2d28887b1
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m9s
- Refactored page.tsx (613→252 lines) with GameOverScreen and LeftPanel extracted - Refactored StatsTab.tsx (584→92 lines) with section components - Refactored SkillsTab.tsx (434→54 lines) with sub-components - Created modular structure for GameContext, LootInventory, and other components - All extracted components organized into feature directories
234 lines
8.8 KiB
TypeScript
234 lines
8.8 KiB
TypeScript
// ─── Computed Stats Functions ─────────────────────────────────────────
|
|
// Extracted from store.ts (lines 362-689)
|
|
// Full implementations with UnifiedEffects support
|
|
|
|
import type { GameState, SpellCost, StudyTarget } from '../types';
|
|
import type { ComputedEffects } from '../upgrade-effects.types';
|
|
import type { UnifiedEffects } from '../effects';
|
|
import { SPELLS_DEF, GUARDIANS, ELEMENT_OPPOSITES, SKILLS_DEF, HOURS_PER_TICK, TICK_MS, INCURSION_START_DAY, MAX_DAY, ELEMENTS } from '../constants';
|
|
import { getUnifiedEffects } from '../effects';
|
|
import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
|
|
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
|
|
|
// Helper to get effective skill level accounting for tiers
|
|
function getEffectiveSkillLevel(
|
|
skills: Record<string, number>,
|
|
baseSkillId: string,
|
|
skillTiers: Record<string, number> = {}
|
|
): { level: number; tier: number; tierMultiplier: number } {
|
|
const currentTier = skillTiers[baseSkillId] || 1;
|
|
const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
|
|
const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
|
|
const tierMultiplier = Math.pow(10, currentTier - 1);
|
|
return { level, tier: currentTier, tierMultiplier };
|
|
}
|
|
|
|
export function computeMaxMana(
|
|
state: GameState,
|
|
effects?: ComputedEffects | UnifiedEffects
|
|
): number {
|
|
const pu = state.prestigeUpgrades;
|
|
const skillMult = (effects as any)?.skillLevelMultiplier || 1;
|
|
const base = 100 + (state.skills.manaWell || 0) * 100 * skillMult + (pu.manaWell || 0) * 500;
|
|
|
|
// Check if we need to compute effects from equipment
|
|
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
|
effects = getUnifiedEffects(state as any);
|
|
}
|
|
|
|
let maxMana: number;
|
|
if (effects) {
|
|
maxMana = Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
|
|
} else {
|
|
maxMana = base;
|
|
}
|
|
|
|
if (effects && hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDENSE)) {
|
|
const totalGathered = state.totalManaGathered || 0;
|
|
const condensesBonus = Math.floor(totalGathered / 1000);
|
|
maxMana = Math.floor(maxMana * (1 + condensesBonus * 0.01));
|
|
}
|
|
|
|
return maxMana;
|
|
}
|
|
|
|
export function computeElementMax(
|
|
state: GameState,
|
|
effects?: ComputedEffects | UnifiedEffects,
|
|
element?: string
|
|
): number {
|
|
const pu = state.prestigeUpgrades;
|
|
const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25;
|
|
|
|
let adjustedBase = base;
|
|
if (element && state.unlockedManaTypeUpgrades) {
|
|
const typeUpgrades = state.unlockedManaTypeUpgrades.filter(u => u.typeId === element);
|
|
const totalLevels = typeUpgrades.reduce((sum, u) => sum + u.level, 0);
|
|
adjustedBase = base + (totalLevels * 10);
|
|
}
|
|
|
|
if (effects) {
|
|
let bonus = effects.elementCapBonus || 0;
|
|
if (element && (effects as UnifiedEffects).perElementCapBonus) {
|
|
const perElementBonus = (effects as UnifiedEffects).perElementCapBonus[element];
|
|
if (perElementBonus) {
|
|
bonus += perElementBonus;
|
|
}
|
|
}
|
|
return Math.floor((adjustedBase + bonus) * (effects.elementCapMultiplier || 1));
|
|
}
|
|
return adjustedBase;
|
|
}
|
|
|
|
export function computeRegen(
|
|
state: GameState,
|
|
effects?: ComputedEffects | UnifiedEffects
|
|
): number {
|
|
const pu = state.prestigeUpgrades;
|
|
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
|
const skillMult = (effects as any)?.skillLevelMultiplier || 1;
|
|
const base = 2 + (state.skills.manaFlow || 0) * 1 * skillMult + (state.skills.manaSpring || 0) * 2 * skillMult + (pu.manaFlow || 0) * 0.5;
|
|
|
|
let regen = base * temporalBonus;
|
|
const attunementRegen = getTotalAttunementRegen(state.attunements || {});
|
|
regen += attunementRegen;
|
|
|
|
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
|
effects = getUnifiedEffects(state as any);
|
|
}
|
|
|
|
if (effects) {
|
|
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
|
|
}
|
|
return regen;
|
|
}
|
|
|
|
export function computeEffectiveRegenForDisplay(
|
|
state: GameState,
|
|
effects?: ComputedEffects | UnifiedEffects
|
|
): { rawRegen: number; conversionDrain: number; effectiveRegen: number } {
|
|
const rawRegen = computeRegen(state, effects);
|
|
const conversionDrain = getTotalAttunementConversionDrain(state.attunements || {});
|
|
const effectiveRegen = Math.max(0, rawRegen - conversionDrain);
|
|
return { rawRegen, conversionDrain, effectiveRegen };
|
|
}
|
|
|
|
export function computeEffectiveRegen(
|
|
state: GameState,
|
|
effects?: ComputedEffects
|
|
): number {
|
|
let regen = computeRegen(state, effects);
|
|
const incursionStrength = state.incursionStrength || 0;
|
|
regen *= (1 - incursionStrength);
|
|
return regen;
|
|
}
|
|
|
|
export function computeClickMana(
|
|
state: GameState,
|
|
effects?: ComputedEffects | UnifiedEffects
|
|
): number {
|
|
const skillMult = (effects as any)?.skillLevelMultiplier || 1;
|
|
const base = 1 + (state.skills.manaTap || 0) * 1 * skillMult + (state.skills.manaSurge || 0) * 3 * skillMult;
|
|
|
|
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
|
effects = getUnifiedEffects(state as any);
|
|
}
|
|
|
|
if (effects) {
|
|
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
|
|
}
|
|
return base;
|
|
}
|
|
|
|
function getElementalBonus(spellElem: string, floorElem: string): number {
|
|
if (spellElem === 'raw') return 1.0;
|
|
if (spellElem === floorElem) return 1.25;
|
|
if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5;
|
|
if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75;
|
|
return 1.0;
|
|
}
|
|
|
|
export function calcDamage(
|
|
state: Pick<GameState, 'skills' | 'signedPacts'>,
|
|
spellId: string,
|
|
floorElem?: string,
|
|
effects?: ComputedEffects | UnifiedEffects
|
|
): number {
|
|
const sp = SPELLS_DEF[spellId];
|
|
if (!sp) return 5;
|
|
const skills = state.skills;
|
|
const skillMult = (effects as any)?.skillLevelMultiplier || 1;
|
|
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5 * skillMult;
|
|
const pct = 1 + (skills.arcaneFury || 0) * 0.1 * skillMult;
|
|
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15 * skillMult;
|
|
const critChance = (skills.precision || 0) * 0.05;
|
|
const pactMult = state.signedPacts.reduce((m, f) => m * ((GUARDIANS as any)[f]?.pact || 1), 1);
|
|
|
|
let damage = baseDmg * pct * pactMult * elemMasteryBonus;
|
|
if (floorElem) {
|
|
damage *= getElementalBonus(sp.elem, floorElem);
|
|
}
|
|
if (Math.random() < critChance) {
|
|
damage *= 1.5;
|
|
}
|
|
return damage;
|
|
}
|
|
|
|
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
|
|
const pu = state.prestigeUpgrades;
|
|
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1;
|
|
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus;
|
|
return Math.floor((state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult);
|
|
}
|
|
|
|
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number {
|
|
const hasMeditation = skills.meditation === 1;
|
|
const hasDeepTrance = skills.deepTrance === 1;
|
|
const hasVoidMeditation = skills.voidMeditation === 1;
|
|
const hours = meditateTicks * HOURS_PER_TICK;
|
|
let bonus = 1 + Math.min(hours / 4, 0.5);
|
|
if (hasMeditation && hours >= 4) bonus = 2.5;
|
|
if (hasDeepTrance && hours >= 6) bonus = 3.0;
|
|
if (hasVoidMeditation && hours >= 8) bonus = 5.0;
|
|
bonus *= meditationEfficiency;
|
|
return bonus;
|
|
}
|
|
|
|
export function getIncursionStrength(day: number, hour: number): number {
|
|
if (day < INCURSION_START_DAY) return 0;
|
|
const totalHours = (day - INCURSION_START_DAY) * 24 + hour;
|
|
const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24;
|
|
return Math.min(0.95, (totalHours / maxHours) * 0.95);
|
|
}
|
|
|
|
export function canAffordSpellCost(
|
|
cost: SpellCost,
|
|
rawMana: number,
|
|
elements: Record<string, { current: number; max: number; unlocked: boolean }>
|
|
): boolean {
|
|
if (cost.type === 'raw') {
|
|
return rawMana >= cost.amount;
|
|
} else {
|
|
const elem = elements[cost.element || ''];
|
|
return elem && elem.unlocked && elem.current >= cost.amount;
|
|
}
|
|
}
|
|
|
|
export function deductSpellCost(
|
|
cost: SpellCost,
|
|
rawMana: number,
|
|
elements: Record<string, { current: number; max: number; unlocked: boolean }>
|
|
): { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> } {
|
|
const newElements = { ...elements };
|
|
if (cost.type === 'raw') {
|
|
const deductedAmount = Math.min(rawMana, cost.amount);
|
|
return { rawMana: rawMana - deductedAmount, elements: newElements };
|
|
} else if (cost.element && newElements[cost.element]) {
|
|
const elem = newElements[cost.element];
|
|
const deductedAmount = Math.min(elem.current, cost.amount);
|
|
newElements[cost.element] = { ...elem, current: elem.current - deductedAmount };
|
|
return { rawMana, elements: newElements };
|
|
}
|
|
return { rawMana, elements: newElements };
|
|
}
|