Files
Mana-Loop/src/lib/game/upgrade-effects.ts
Z User 40c2b383ff
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m59s
Implement guardian unique perks and fix EXECUTIONER
CRITICAL FIXES:
- Add EXECUTIONER to SPECIAL_EFFECTS constant (was used but not defined)
- Implement all 10 guardian unique perks when pacts are signed:
  - Floor 10: Fire spells cast 10% faster
  - Floor 20: Water spells have 10% lifesteal
  - Floor 30: Air spells have 15% crit chance
  - Floor 40: Earth spells +25% damage to guardians
  - Floor 50: Light spells +20% damage
  - Floor 60: Dark spells have 20% lifesteal
  - Floor 70: Life spells heal 30% of damage
  - Floor 80: Death spells execute below 20% HP
  - Floor 90: Void spells ignore 30% resistance
  - Floor 100: All spells +50% damage and cast 25% faster

- Add getGuardianPerks() function to compute active perks from signed pacts
- Apply guardian perks in combat calculations (damage, lifesteal, crit, execute)
- Import getGuardianPerks in store.ts

🤖 Generated with [Claude Code](https://claude.ai/code)
2026-03-28 12:58:18 +00:00

367 lines
12 KiB
TypeScript
Executable File

// ─── Upgrade Effect System ─────────────────────────────────────────────────────
// This module handles applying skill upgrade effects to game stats
import type { SkillUpgradeChoice, SkillUpgradeEffect } from './types';
import { getUpgradesForSkillAtMilestone, SKILL_EVOLUTION_PATHS } from './skill-evolution';
// ─── Types ───────────────────────────────────────────────────────────────────
export interface ActiveUpgradeEffect {
upgradeId: string;
skillId: string;
milestone: 5 | 10;
effect: SkillUpgradeEffect;
name: string;
desc: string;
}
export interface ComputedEffects {
// Mana effects
maxManaMultiplier: number;
maxManaBonus: number;
regenMultiplier: number;
regenBonus: number;
clickManaMultiplier: number;
clickManaBonus: number;
meditationEfficiency: number;
spellCostMultiplier: number;
conversionEfficiency: number;
// Combat effects
baseDamageMultiplier: number;
baseDamageBonus: number;
attackSpeedMultiplier: number;
critChanceBonus: number;
critDamageMultiplier: number;
elementalDamageMultiplier: number;
// Study effects
studySpeedMultiplier: number;
studyCostMultiplier: number;
progressRetention: number;
instantStudyChance: number;
freeStudyChance: number;
// Element effects
elementCapMultiplier: number;
elementCapBonus: number;
conversionCostMultiplier: number;
doubleCraftChance: number;
// Special values
permanentRegenBonus: number;
// Special effect flags (for game logic to check)
specials: Set<string>;
// All active upgrades for display
activeUpgrades: ActiveUpgradeEffect[];
}
// ─── Special Effect IDs ────────────────────────────────────────────────────────
// These are the IDs used in the 'specialId' field of special effects
export const SPECIAL_EFFECTS = {
// Mana Flow special effects
MANA_CASCADE: 'manaCascade', // +0.1 regen per 100 max mana
STEADY_STREAM: 'steadyStream', // Regen immune to incursion
MANA_TORRENT: 'manaTorrent', // +50% regen when above 75% mana
FLOW_SURGE: 'flowSurge', // Clicks restore 2x regen for 1 hour
MANA_OVERFLOW: 'manaOverflow', // Raw mana can exceed max by 20%
// Mana Well special effects
DESPERATE_WELLS: 'desperateWells', // +50% regen when below 25% mana
MANA_ECHO: 'manaEcho', // 10% chance double mana from clicks
EMERGENCY_RESERVE: 'emergencyReserve', // Keep 10% mana on new loop
// Combat special effects
FIRST_STRIKE: 'firstStrike', // +15% damage on first attack each floor
OVERPOWER: 'overpower', // +50% damage when mana above 80%
BERSERKER: 'berserker', // +50% damage when below 50% mana
COMBO_MASTER: 'comboMaster', // Every 5th attack deals 3x damage
ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana
EXECUTIONER: 'executioner', // Instant kill enemies below 25% HP
// Study special effects
QUICK_GRASP: 'quickGrasp', // 5% chance double study progress per hour
DEEP_CONCENTRATION: 'deepConcentration', // +20% study speed when mana > 90%
QUICK_MASTERY: 'quickMastery', // -20% study time for final 3 levels
PARALLEL_STUDY: 'parallelStudy', // Study 2 things at 50% speed
STUDY_MOMENTUM: 'studyMomentum', // +5% study speed per consecutive hour
KNOWLEDGE_ECHO: 'knowledgeEcho', // 10% chance instant study
KNOWLEDGE_TRANSFER: 'knowledgeTransfer', // New skills start at 10% progress
MENTAL_CLARITY: 'mentalClarity', // +10% study speed when mana > 75%
STUDY_REFUND: 'studyRefund', // 25% mana back on study complete
DEEP_UNDERSTANDING: 'deepUnderstanding', // +10% bonus from all skill levels
STUDY_RUSH: 'studyRush', // First hour of study is 2x speed
CHAIN_STUDY: 'chainStudy', // -5% cost per maxed skill
// Element special effects
ELEMENTAL_AFFINITY: 'elementalAffinity', // New elements start with 10 capacity
EXOTIC_MASTERY: 'exoticMastery', // +20% exotic element damage
ELEMENTAL_RESONANCE: 'elementalResonance', // Using element spells restores 1 of that element
MANA_CONDUIT: 'manaConduit', // Meditation regenerates elemental mana
// Movement special effects (for legs equipment)
EXPEDITIOUS_RETREAT: 'expeditiousRetreat', // Teleport down 5 floors when exiting spire
} as const;
// ─── Upgrade Definition Cache ─────────────────────────────────────────────────
// Cache all upgrades by ID for quick lookup
const upgradeDefinitionsById: Map<string, SkillUpgradeChoice> = new Map();
// Build the cache on first access
function buildUpgradeCache(): void {
if (upgradeDefinitionsById.size > 0) return;
for (const [baseSkillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
for (const tierDef of path.tiers) {
for (const upgrade of tierDef.upgrades) {
upgradeDefinitionsById.set(upgrade.id, upgrade);
}
}
}
}
// ─── Helper Functions ─────────────────────────────────────────────────────────
/**
* Get all selected upgrades with their full effect definitions
*/
export function getActiveUpgrades(
skillUpgrades: Record<string, string[]>,
skillTiers: Record<string, number>
): ActiveUpgradeEffect[] {
buildUpgradeCache();
const result: ActiveUpgradeEffect[] = [];
for (const [skillId, upgradeIds] of Object.entries(skillUpgrades)) {
for (const upgradeId of upgradeIds) {
const upgradeDef = upgradeDefinitionsById.get(upgradeId);
if (upgradeDef) {
result.push({
upgradeId,
skillId,
milestone: upgradeDef.milestone,
effect: upgradeDef.effect,
name: upgradeDef.name,
desc: upgradeDef.desc,
});
}
}
}
return result;
}
/**
* Compute all active effects from selected upgrades
*/
export function computeEffects(
skillUpgrades: Record<string, string[]>,
skillTiers: Record<string, number>
): ComputedEffects {
const activeUpgrades = getActiveUpgrades(skillUpgrades, skillTiers);
// Start with base values
const effects: ComputedEffects = {
maxManaMultiplier: 1,
maxManaBonus: 0,
regenMultiplier: 1,
regenBonus: 0,
clickManaMultiplier: 1,
clickManaBonus: 0,
meditationEfficiency: 1,
spellCostMultiplier: 1,
conversionEfficiency: 1,
baseDamageMultiplier: 1,
baseDamageBonus: 0,
attackSpeedMultiplier: 1,
critChanceBonus: 0,
critDamageMultiplier: 1.5,
elementalDamageMultiplier: 1,
studySpeedMultiplier: 1,
studyCostMultiplier: 1,
progressRetention: 0,
instantStudyChance: 0,
freeStudyChance: 0,
elementCapMultiplier: 1,
elementCapBonus: 0,
conversionCostMultiplier: 1,
doubleCraftChance: 0,
permanentRegenBonus: 0,
specials: new Set<string>(),
activeUpgrades,
};
// Apply each upgrade effect
for (const upgrade of activeUpgrades) {
const { effect } = upgrade;
if (effect.type === 'multiplier' && effect.stat && effect.value !== undefined) {
// Multiplier effects (multiply the stat)
switch (effect.stat) {
case 'maxMana':
effects.maxManaMultiplier *= effect.value;
break;
case 'regen':
effects.regenMultiplier *= effect.value;
break;
case 'clickMana':
effects.clickManaMultiplier *= effect.value;
break;
case 'meditationEfficiency':
effects.meditationEfficiency *= effect.value;
break;
case 'spellCost':
effects.spellCostMultiplier *= effect.value;
break;
case 'conversionEfficiency':
effects.conversionEfficiency *= effect.value;
break;
case 'baseDamage':
effects.baseDamageMultiplier *= effect.value;
break;
case 'attackSpeed':
effects.attackSpeedMultiplier *= effect.value;
break;
case 'elementalDamage':
effects.elementalDamageMultiplier *= effect.value;
break;
case 'studySpeed':
effects.studySpeedMultiplier *= effect.value;
break;
case 'elementCap':
effects.elementCapMultiplier *= effect.value;
break;
case 'conversionCost':
effects.conversionCostMultiplier *= effect.value;
break;
case 'costReduction':
// For cost reduction, higher is better (less cost)
// This is a multiplier on the reduction effectiveness
effects.studyCostMultiplier /= effect.value;
break;
}
} else if (effect.type === 'bonus' && effect.stat && effect.value !== undefined) {
// Bonus effects (add to the stat)
switch (effect.stat) {
case 'maxMana':
effects.maxManaBonus += effect.value;
break;
case 'regen':
effects.regenBonus += effect.value;
break;
case 'clickMana':
effects.clickManaBonus += effect.value;
break;
case 'baseDamage':
effects.baseDamageBonus += effect.value;
break;
case 'elementCap':
effects.elementCapBonus += effect.value;
break;
case 'permanentRegen':
effects.permanentRegenBonus += effect.value;
break;
}
} else if (effect.type === 'special' && effect.specialId) {
// Special effects - add to the set for game logic to check
effects.specials.add(effect.specialId);
}
}
return effects;
}
/**
* Check if a special effect is active
*/
export function hasSpecial(effects: ComputedEffects, specialId: string): boolean {
return effects?.specials?.has(specialId) ?? false;
}
/**
* Compute regen with special effects that depend on dynamic values
*/
export function computeDynamicRegen(
effects: ComputedEffects,
baseRegen: number,
maxMana: number,
currentMana: number,
incursionStrength: number
): number {
let regen = baseRegen;
// Mana Cascade: +0.1 regen per 100 max mana
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CASCADE)) {
regen += Math.floor(maxMana / 100) * 0.1;
}
// Mana Torrent: +50% regen when above 75% mana
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TORRENT) && currentMana > maxMana * 0.75) {
regen *= 1.5;
}
// Desperate Wells: +50% regen when below 25% mana
if (hasSpecial(effects, SPECIAL_EFFECTS.DESPERATE_WELLS) && currentMana < maxMana * 0.25) {
regen *= 1.5;
}
// Steady Stream: Regen immune to incursion
if (hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)) {
return regen * effects.regenMultiplier;
}
// Apply incursion penalty
regen *= (1 - incursionStrength);
return regen * effects.regenMultiplier;
}
/**
* Compute click mana with special effects
*/
export function computeDynamicClickMana(
effects: ComputedEffects,
baseClickMana: number
): number {
let clickMana = baseClickMana;
// Mana Echo: 10% chance to gain double mana from clicks
// Note: The chance is handled in the click handler, this just returns the base
// The click handler should check hasSpecial and apply the 10% chance
return Math.floor((clickMana + effects.clickManaBonus) * effects.clickManaMultiplier);
}
/**
* Compute damage with special effects
*/
export function computeDynamicDamage(
effects: ComputedEffects,
baseDamage: number,
floorHPPct: number,
currentMana: number,
maxMana: number
): number {
let damage = baseDamage * effects.baseDamageMultiplier;
// Overpower: +50% damage when mana above 80%
if (hasSpecial(effects, SPECIAL_EFFECTS.OVERPOWER) && currentMana >= maxMana * 0.8) {
damage *= 1.5;
}
// Berserker: +50% damage when below 50% mana
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && currentMana < maxMana * 0.5) {
damage *= 1.5;
}
// Combo Master: Every 5th attack deals 3x damage
// Note: The hit counter is tracked in game state, this just returns the multiplier
// The combat handler should check hasSpecial and the hit count
return damage + effects.baseDamageBonus;
}