feat: add prestige system and skill upgrades with comprehensive documentation
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 5m57s

This commit is contained in:
Refactoring Agent
2026-05-01 15:18:09 +02:00
parent 3691aa4acc
commit 03815f27ee
52 changed files with 4056 additions and 873 deletions
+1
View File
@@ -15,4 +15,5 @@ export const PRESTIGE_DEF: Record<string, PrestigeDef> = {
guardianPact: { name: "Guardian Pact", desc: "+10% pact multiplier", max: 5, cost: 3500 },
quickStart: { name: "Quick Start", desc: "Start with 100 raw mana", max: 3, cost: 400 },
elemStart: { name: "Elem. Start", desc: "Start with 5 of each unlocked element", max: 3, cost: 800 },
unlockedManaTypeCapacity: { name: "Mana Type Capacity", desc: "+10 capacity for selected mana type", max: 5, cost: 1000 },
};
+66 -19
View File
@@ -48,20 +48,20 @@ export const SKILLS_DEF: Record<string, SkillDef> = {
researchEarthSpells: { name: "Earth Spell Research", desc: "Unlock Stone Bullet, Rock Spike spell enchantments", cat: "effectResearch", max: 1, base: 350, studyTime: 6, req: { enchanting: 2 }, cost: { type: 'element', element: 'earth', amount: 100 }, attunementReq: { enchanter: 1 } },
researchLightSpells: { name: "Light Spell Research", desc: "Unlock Light Lance, Radiance spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 }, cost: { type: 'element', element: 'light', amount: 100 }, attunementReq: { enchanter: 2 } },
researchDarkSpells: { name: "Dark Spell Research", desc: "Unlock Shadow Bolt, Dark Pulse spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 }, cost: { type: 'element', element: 'dark', amount: 100 }, attunementReq: { enchanter: 2 } },
researchLifeDeathSpells: { name: "Death Research", desc: "Unlock Drain spell enchantment", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 , cost: { type: 'element', element: 'death', amount: 100 }}, attunementReq: { enchanter: 2 } },
researchLifeDeathSpells: { name: "Death Research", desc: "Unlock Drain spell enchantment", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 }, cost: { type: 'element', element: 'death', amount: 100 }, attunementReq: { enchanter: 2 } },
// Tier 2 - Advanced Spell Effects - Require Enchanter 3
researchAdvancedFire: { name: "Advanced Fire Research", desc: "Unlock Inferno, Flame Wave spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchFireSpells: 1, enchanting: 4 , cost: { type: 'element', element: 'fire', amount: 100 }}, attunementReq: { enchanter: 3 } },
researchAdvancedWater: { name: "Advanced Water Research", desc: "Unlock Tidal Wave, Ice Storm spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchWaterSpells: 1, enchanting: 4 , cost: { type: 'element', element: 'water', amount: 100 }}, attunementReq: { enchanter: 3 } },
researchAdvancedAir: { name: "Advanced Air Research", desc: "Unlock Hurricane, Wind Blade spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchAirSpells: 1, enchanting: 4 , cost: { type: 'element', element: 'air', amount: 100 }}, attunementReq: { enchanter: 3 } },
researchAdvancedEarth: { name: "Advanced Earth Research", desc: "Unlock Earthquake, Stone Barrage spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchEarthSpells: 1, enchanting: 4 , cost: { type: 'element', element: 'earth', amount: 100 }}, attunementReq: { enchanter: 3 } },
researchAdvancedLight: { name: "Advanced Light Research", desc: "Unlock Solar Flare, Divine Smite spell enchantments", cat: "effectResearch", max: 1, base: 700, studyTime: 14, req: { researchLightSpells: 1, enchanting: 5 , cost: { type: 'element', element: 'light', amount: 100 }}, attunementReq: { enchanter: 4 } },
researchAdvancedDark: { name: "Advanced Dark Research", desc: "Unlock Void Rift, Shadow Storm spell enchantments", cat: "effectResearch", max: 1, base: 700, studyTime: 14, req: { researchDarkSpells: 1, enchanting: 5 , cost: { type: 'element', element: 'dark', amount: 100 }}, attunementReq: { enchanter: 4 } },
researchAdvancedFire: { name: "Advanced Fire Research", desc: "Unlock Inferno, Flame Wave spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchFireSpells: 1, enchanting: 4 }, cost: { type: 'element', element: 'fire', amount: 100 }, attunementReq: { enchanter: 3 } },
researchAdvancedWater: { name: "Advanced Water Research", desc: "Unlock Tidal Wave, Ice Storm spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchWaterSpells: 1, enchanting: 4 }, cost: { type: 'element', element: 'water', amount: 100 }, attunementReq: { enchanter: 3 } },
researchAdvancedAir: { name: "Advanced Air Research", desc: "Unlock Hurricane, Wind Blade spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchAirSpells: 1, enchanting: 4 }, cost: { type: 'element', element: 'air', amount: 100 }, attunementReq: { enchanter: 3 } },
researchAdvancedEarth: { name: "Advanced Earth Research", desc: "Unlock Earthquake, Stone Barrage spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchEarthSpells: 1, enchanting: 4 }, cost: { type: 'element', element: 'earth', amount: 100 }, attunementReq: { enchanter: 3 } },
researchAdvancedLight: { name: "Advanced Light Research", desc: "Unlock Solar Flare, Divine Smite spell enchantments", cat: "effectResearch", max: 1, base: 700, studyTime: 14, req: { researchLightSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'light', amount: 100 }, attunementReq: { enchanter: 4 } },
researchAdvancedDark: { name: "Advanced Dark Research", desc: "Unlock Void Rift, Shadow Storm spell enchantments", cat: "effectResearch", max: 1, base: 700, studyTime: 14, req: { researchDarkSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'dark', amount: 100 }, attunementReq: { enchanter: 4 } },
// Tier 3 - Master Spell Effects - Require Enchanter 5
researchMasterFire: { name: "Master Fire Research", desc: "Unlock Pyroclasm spell enchantment", cat: "effectResearch", max: 1, base: 1200, studyTime: 24, req: { researchAdvancedFire: 1, enchanting: 7 , cost: { type: 'element', element: 'fire', amount: 200 }}, attunementReq: { enchanter: 5 } },
researchMasterWater: { name: "Master Water Research", desc: "Unlock Tsunami spell enchantment", cat: "effectResearch", max: 1, base: 1200, studyTime: 24, req: { researchAdvancedWater: 1, enchanting: 7 , cost: { type: 'element', element: 'water', amount: 200 }}, attunementReq: { enchanter: 5 } },
researchMasterEarth: { name: "Master Earth Research", desc: "Unlock Meteor Strike spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedEarth: 1, enchanting: 8 , cost: { type: 'element', element: 'earth', amount: 200 }}, attunementReq: { enchanter: 5 } },
researchMasterFire: { name: "Master Fire Research", desc: "Unlock Pyroclasm spell enchantment", cat: "effectResearch", max: 1, base: 1200, studyTime: 24, req: { researchAdvancedFire: 1, enchanting: 7 }, cost: { type: 'element', element: 'fire', amount: 200 }, attunementReq: { enchanter: 5 } },
researchMasterWater: { name: "Master Water Research", desc: "Unlock Tsunami spell enchantment", cat: "effectResearch", max: 1, base: 1200, studyTime: 24, req: { researchAdvancedWater: 1, enchanting: 7 }, cost: { type: 'element', element: 'water', amount: 200 }, attunementReq: { enchanter: 5 } },
researchMasterEarth: { name: "Master Earth Research", desc: "Unlock Meteor Strike spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedEarth: 1, enchanting: 8 }, cost: { type: 'element', element: 'earth', amount: 200 }, attunementReq: { enchanter: 5 } },
// Combat Effect Research
researchDamageEffects: { name: "Damage Effect Research", desc: "Unlock Minor/Moderate Power, Amplification effects", cat: "effectResearch", max: 1, base: 250, studyTime: 5, req: { enchanting: 1 }, attunementReq: { enchanter: 1 } },
@@ -83,19 +83,19 @@ export const SKILLS_DEF: Record<string, SkillDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// Tier 1 - Basic Compound Spells
researchMetalSpells: { name: "Metal Spell Research", desc: "Unlock Metal Shard, Iron Fist spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 6, req: { researchFireSpells: 1, researchEarthSpells: 1, enchanting: 3 , cost: { type: 'element', element: 'metal', amount: 100 }}, attunementReq: { enchanter: 2 } },
researchSandSpells: { name: "Sand Spell Research", desc: "Unlock Sand Blast, Sandstorm spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 6, req: { researchEarthSpells: 1, researchWaterSpells: 1, enchanting: 3 , cost: { type: 'element', element: 'sand', amount: 100 }}, attunementReq: { enchanter: 2 } },
researchLightningSpells: { name: "Lightning Spell Research", desc: "Unlock Spark, Lightning Bolt spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 6, req: { researchFireSpells: 1, researchAirSpells: 1, enchanting: 3 , cost: { type: 'element', element: 'lightning', amount: 100 }}, attunementReq: { enchanter: 2 } },
researchMetalSpells: { name: "Metal Spell Research", desc: "Unlock Metal Shard, Iron Fist spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 6, req: { researchFireSpells: 1, researchEarthSpells: 1, enchanting: 3 }, cost: { type: 'element', element: 'metal', amount: 100 }, attunementReq: { enchanter: 2 } },
researchSandSpells: { name: "Sand Spell Research", desc: "Unlock Sand Blast, Sandstorm spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 6, req: { researchEarthSpells: 1, researchWaterSpells: 1, enchanting: 3 }, cost: { type: 'element', element: 'sand', amount: 100 }, attunementReq: { enchanter: 2 } },
researchLightningSpells: { name: "Lightning Spell Research", desc: "Unlock Spark, Lightning Bolt spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 6, req: { researchFireSpells: 1, researchAirSpells: 1, enchanting: 3 }, cost: { type: 'element', element: 'lightning', amount: 100 }, attunementReq: { enchanter: 2 } },
// Tier 2 - Advanced Compound Spells
researchAdvancedMetal: { name: "Advanced Metal Research", desc: "Unlock Steel Tempest spell enchantment", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchMetalSpells: 1, enchanting: 5 , cost: { type: 'element', element: 'metal', amount: 100 }}, attunementReq: { enchanter: 3 } },
researchAdvancedSand: { name: "Advanced Sand Research", desc: "Unlock Desert Wind spell enchantment", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchSandSpells: 1, enchanting: 5 , cost: { type: 'element', element: 'sand', amount: 100 }}, attunementReq: { enchanter: 3 } },
researchAdvancedLightning: { name: "Advanced Lightning Research", desc: "Unlock Chain Lightning, Storm Call spell enchantments", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchLightningSpells: 1, enchanting: 5 , cost: { type: 'element', element: 'lightning', amount: 100 }}, attunementReq: { enchanter: 3 } },
researchAdvancedMetal: { name: "Advanced Metal Research", desc: "Unlock Steel Tempest spell enchantment", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchMetalSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'metal', amount: 100 }, attunementReq: { enchanter: 3 } },
researchAdvancedSand: { name: "Advanced Sand Research", desc: "Unlock Desert Wind spell enchantment", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchSandSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'sand', amount: 100 }, attunementReq: { enchanter: 3 } },
researchAdvancedLightning: { name: "Advanced Lightning Research", desc: "Unlock Chain Lightning, Storm Call spell enchantments", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchLightningSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'lightning', amount: 100 }, attunementReq: { enchanter: 3 } },
// Tier 3 - Master Compound Spells
researchMasterMetal: { name: "Master Metal Research", desc: "Unlock Furnace Blast spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedMetal: 1, enchanting: 7 , cost: { type: 'element', element: 'metal', amount: 200 }}, attunementReq: { enchanter: 5 } },
researchMasterSand: { name: "Master Sand Research", desc: "Unlock Dune Collapse spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedSand: 1, enchanting: 7 , cost: { type: 'element', element: 'sand', amount: 200 }}, attunementReq: { enchanter: 5 } },
researchMasterLightning: { name: "Master Lightning Research", desc: "Unlock Thunder Strike spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedLightning: 1, enchanting: 7 , cost: { type: 'element', element: 'lightning', amount: 200 }}, attunementReq: { enchanter: 5 } },
researchMasterMetal: { name: "Master Metal Research", desc: "Unlock Furnace Blast spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedMetal: 1, enchanting: 7 }, cost: { type: 'element', element: 'metal', amount: 200 }, attunementReq: { enchanter: 5 } },
researchMasterSand: { name: "Master Sand Research", desc: "Unlock Dune Collapse spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedSand: 1, enchanting: 7 }, cost: { type: 'element', element: 'sand', amount: 200 }, attunementReq: { enchanter: 5 } },
researchMasterLightning: { name: "Master Lightning Research", desc: "Unlock Thunder Strike spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedLightning: 1, enchanting: 7 }, cost: { type: 'element', element: 'lightning', amount: 200 }, attunementReq: { enchanter: 5 } },
// ═══════════════════════════════════════════════════════════════════════════
// UTILITY MANA SPELL RESEARCH - Transference
@@ -292,6 +292,53 @@ export const EFFECT_RESEARCH_MAPPING: Record<string, string[]> = {
// Tier 3 - Master Utility Spells
researchMasterTransference: ['spell_soulTransfer'],
// ═══════════════════════════════════════════════════════════════════════════
// PER-ELEMENT CAPACITY RESEARCH - Unlocks per-element capacity effects
// ═══════════════════════════════════════════════════════════════════════════
// Basic Element Capacity Effects (Tier 1 - +10 per stack)
researchFireCapacity: ['fire_cap_10'],
researchWaterCapacity: ['water_cap_10'],
researchAirCapacity: ['air_cap_10'],
researchEarthCapacity: ['earth_cap_10'],
researchLightCapacity: ['light_cap_10'],
researchDarkCapacity: ['dark_cap_10'],
researchDeathCapacity: ['death_cap_10'],
// Advanced Element Capacity Effects (Tier 2 - +25 per stack)
researchAdvancedFireCap: ['fire_cap_25'],
researchAdvancedWaterCap: ['water_cap_25'],
researchAdvancedAirCap: ['air_cap_25'],
researchAdvancedEarthCap: ['earth_cap_25'],
researchAdvancedLightCap: ['light_cap_25'],
researchAdvancedDarkCap: ['dark_cap_25'],
researchAdvancedDeathCap: ['death_cap_25'],
// Master Element Capacity Effects (Tier 3 - +50 per stack)
researchMasterFireCap: ['fire_cap_50'],
researchMasterWaterCap: ['water_cap_50'],
researchMasterAirCap: ['air_cap_50'],
researchMasterEarthCap: ['earth_cap_50'],
researchMasterLightCap: ['light_cap_50'],
researchMasterDarkCap: ['dark_cap_50'],
researchMasterDeathCap: ['death_cap_50'],
// Composite Element Capacity Effects
researchMetalCapacity: ['metal_cap_10'],
researchAdvancedMetalCap: ['metal_cap_25', 'metal_cap_50'],
researchSandCapacity: ['sand_cap_10'],
researchAdvancedSandCap: ['sand_cap_25', 'sand_cap_50'],
researchLightningCapacity: ['lightning_cap_10'],
researchAdvancedLightningCap: ['lightning_cap_25', 'lightning_cap_50'],
// Exotic Element Capacity Effects
researchCrystalCapacity: ['crystal_cap_10'],
researchAdvancedCrystalCap: ['crystal_cap_25', 'crystal_cap_50'],
researchStellarCapacity: ['stellar_cap_10'],
researchAdvancedStellarCap: ['stellar_cap_25', 'stellar_cap_50'],
researchVoidCapacity: ['void_cap_10'],
researchAdvancedVoidCap: ['void_cap_25', 'void_cap_50'],
};
// Base effects unlocked when player gets enchanting skill level 1
+3 -1
View File
@@ -7,7 +7,9 @@ import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchant
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
import { SPELLS_DEF } from './constants';
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects';
import { computeEffects } from './upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
import type { ComputedEffects } from './upgrade-effects.types';
// ─── Helper Functions ─────────────────────────────────────────────────────────
@@ -1,6 +1,9 @@
// ─── Mana Enchantment Effects ────────────────────────────────────────────────
// All mana-related enchantment effects that can be applied to equipment
// Import ELEMENTS to get the list of elements for per-element capacity effects
import { ELEMENTS } from '../../constants';
import type { EquipmentCategory } from '../equipment'
import type { EnchantmentEffectDef } from '../enchantment-types'
@@ -149,4 +152,50 @@ export const MANA_EFFECTS: Record<string, EnchantmentEffectDef> = {
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 5 }
},
// ═══════════════════════════════════════════════════════════════════════════
// PER-ELEMENT CAPACITY EFFECTS - Boosts capacity for specific mana types
// ═══════════════════════════════════════════════════════════════════════════
// Helper to create per-element capacity effects for a given element
// Creates 3 tiers: +10 (5 stacks), +25 (3 stacks), +50 (2 stacks)
...Object.fromEntries(
Object.entries(ELEMENTS)
.filter(([, def]) => def.cat !== 'utility') // Skip utility elements like transference
.flatMap(([elemId, elemDef]) => {
const capName = elemId.charAt(0).toUpperCase() + elemId.slice(1);
return [
[`${elemId}_cap_10`, {
id: `${elemId}_cap_10`,
name: `${capName} Reservoir`,
description: `+10 ${elemDef.name} mana capacity`,
category: 'mana',
baseCapacityCost: 30,
maxStacks: 5,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: `elementCap_${elemId}`, value: 10 }
}],
[`${elemId}_cap_25`, {
id: `${elemId}_cap_25`,
name: `${capName} Basin`,
description: `+25 ${elemDef.name} mana capacity`,
category: 'mana',
baseCapacityCost: 60,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: `elementCap_${elemId}`, value: 25 }
}],
[`${elemId}_cap_50`, {
id: `${elemId}_cap_50`,
name: `${capName} Wellspring`,
description: `+50 ${elemDef.name} mana capacity`,
category: 'mana',
baseCapacityCost: 100,
maxStacks: 2,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: `elementCap_${elemId}`, value: 50 }
}],
];
})
),
};
+125
View File
@@ -0,0 +1,125 @@
// ─── Dynamic Computations ──────────────────────────────────────────────────
// Dynamic computation functions that depend on special effects
import type { ComputedEffects } from './upgrade-effects.types';
import { SPECIAL_EFFECTS, hasSpecial } from './special-effects';
/**
* 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 Waterfall: +0.25 regen per 100 max mana (upgraded cascade)
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_WATERFALL)) {
regen += Math.floor(maxMana / 100) * 0.25;
}
// Mana Torrent: +50% regen when above 75% mana
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TORRENT) && currentMana > maxMana * 0.75) {
regen *= 1.5;
}
// Desperate Wells / Despair Wells: +50% regen when below 25% mana
if ((hasSpecial(effects, SPECIAL_EFFECTS.DESPERATE_WELLS) || hasSpecial(effects, SPECIAL_EFFECTS.DESPAIR_WELLS)) && currentMana < maxMana * 0.25) {
regen *= 1.5;
}
// Panic Reserve: +100% regen when below 10% mana
if (hasSpecial(effects, SPECIAL_EFFECTS.PANIC_RESERVE) && currentMana < maxMana * 0.1) {
regen *= 2.0;
}
// Deep Reserve: +0.5 regen per 100 max mana
if (hasSpecial(effects, SPECIAL_EFFECTS.DEEP_RESERVE)) {
regen += Math.floor(maxMana / 100) * 0.5;
}
// Mana Core: 0.5% of max mana added as regen
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CORE)) {
regen += maxMana * 0.005;
}
// Mana Tide: Regen pulses ±50% (sinusoidal based on time)
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TIDE)) {
const pulseFactor = 0.5 + 0.5 * Math.sin(Date.now() / 10000);
regen *= (0.5 + pulseFactor * 0.5);
}
// Eternal Flow: Regen immune to ALL penalties
if (hasSpecial(effects, SPECIAL_EFFECTS.ETERNAL_FLOW)) {
return regen * effects.regenMultiplier;
}
// 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
// Mana Genesis: Generate 1% of max mana per hour passively
// This is handled in the game loop (store.ts), not here
// Mana Heart: +10% max mana per loop (permanent)
// This is applied during loop reset in store.ts
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;
}
+42 -12
View File
@@ -1,4 +1,4 @@
// ─── Unified Effect System ─────────────────────────────────────────────────────────
// ─── Unified Effect System ─────────────────────────────────────────────────
// This module consolidates ALL effect sources into a single computation:
// - Skill upgrade effects (from milestone upgrades)
// - Equipment enchantment effects (from enchanted gear)
@@ -6,19 +6,25 @@
import type { GameState, EquipmentInstance } from './types';
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects';
import { computeEffects } from './upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
import type { ComputedEffects } from './upgrade-effects.types';
// Re-export for convenience
export { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects };
export { computeEffects } from './upgrade-effects';
export { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
export type { ComputedEffects } from './upgrade-effects.types';
// ─── Equipment Effect Computation ────────────────────────────────────────────────
// ─── Equipment Effect Computation ────────────────────────────────────────────
/**
* Compute all effects from equipped enchantments
* @param enchantmentPowerMultiplier - Multiplier applied to all enchantment effect values (default 1.0)
*/
export function computeEquipmentEffects(
equipmentInstances: Record<string, EquipmentInstance>,
equippedInstances: Record<string, string | null>
equippedInstances: Record<string, string | null>,
enchantmentPowerMultiplier: number = 1.0
): {
bonuses: Record<string, number>;
multipliers: Record<string, number>;
@@ -43,17 +49,27 @@ export function computeEquipmentEffects(
if (effect.type === 'bonus' && effect.stat && effect.value) {
// Bonus effects add to the stat
bonuses[effect.stat] = (bonuses[effect.stat] || 0) + effect.value * ench.stacks;
// Apply enchantmentPowerMultiplier to the effect value
const adjustedValue = effect.value * enchantmentPowerMultiplier;
// Handle per-element capacity bonuses (stat format: elementCap_fire, elementCap_water, etc.)
if (effect.stat.startsWith('elementCap_')) {
const element = effect.stat.replace('elementCap_', '');
bonuses[`elementCap_${element}`] = (bonuses[`elementCap_${element}`] || 0) + adjustedValue * ench.stacks;
} else {
bonuses[effect.stat] = (bonuses[effect.stat] || 0) + adjustedValue * ench.stacks;
}
} else if (effect.type === 'multiplier' && effect.stat && effect.value) {
// Multiplier effects multiply together
// For multipliers, we need to track them separately and apply as product
// Apply enchantmentPowerMultiplier to the effect value
const adjustedValue = effect.value * enchantmentPowerMultiplier;
const key = effect.stat;
if (!multipliers[key]) {
multipliers[key] = 1;
}
// Each stack applies the multiplier
for (let i = 0; i < ench.stacks; i++) {
multipliers[key] *= effect.value;
multipliers[key] *= adjustedValue;
}
} else if (effect.type === 'special' && effect.specialId) {
specials.add(effect.specialId);
@@ -64,7 +80,7 @@ export function computeEquipmentEffects(
return { bonuses, multipliers, specials };
}
// ─── Unified Computed Effects ────────────────────────────────────────────────────
// ─── Unified Computed Effects ─────────────────────────────────────────────────
export interface UnifiedEffects extends ComputedEffects {
// Equipment bonuses
@@ -85,8 +101,21 @@ export function computeAllEffects(
// Get skill upgrade effects
const upgradeEffects = computeEffects(skillUpgrades, skillTiers);
// Get equipment effects
const equipmentEffects = computeEquipmentEffects(equipmentInstances, equippedInstances);
// Get equipment effects, applying the enchantment power multiplier
const equipmentEffects = computeEquipmentEffects(
equipmentInstances,
equippedInstances,
upgradeEffects.enchantmentPowerMultiplier
);
// Extract per-element capacity bonuses from equipment effects
const perElementCapBonus: Record<string, number> = { ...upgradeEffects.perElementCapBonus };
for (const [key, value] of Object.entries(equipmentEffects.bonuses)) {
if (key.startsWith('elementCap_')) {
const element = key.replace('elementCap_', '');
perElementCapBonus[element] = (perElementCapBonus[element] || 0) + value;
}
}
// Merge the effects
const merged: UnifiedEffects = {
@@ -97,6 +126,7 @@ export function computeAllEffects(
clickManaBonus: upgradeEffects.clickManaBonus + (equipmentEffects.bonuses.clickMana || 0),
baseDamageBonus: upgradeEffects.baseDamageBonus + (equipmentEffects.bonuses.baseDamage || 0),
elementCapBonus: upgradeEffects.elementCapBonus + (equipmentEffects.bonuses.elementCap || 0),
perElementCapBonus,
// Merge equipment multipliers with upgrade multipliers
maxManaMultiplier: upgradeEffects.maxManaMultiplier * (equipmentEffects.multipliers.maxMana || 1),
regenMultiplier: upgradeEffects.regenMultiplier * (equipmentEffects.multipliers.regen || 1),
@@ -105,7 +135,7 @@ export function computeAllEffects(
attackSpeedMultiplier: upgradeEffects.attackSpeedMultiplier * (equipmentEffects.multipliers.attackSpeed || 1),
elementCapMultiplier: upgradeEffects.elementCapMultiplier * (equipmentEffects.multipliers.elementCap || 1),
// Merge specials
specials: new Set([...upgradeEffects.specials, ...equipmentEffects.specials]),
specials: new Set([...Array.from(upgradeEffects.specials), ...Array.from(equipmentEffects.specials)]),
// Store equipment effects for reference
equipmentBonuses: equipmentEffects.bonuses,
equipmentMultipliers: equipmentEffects.multipliers,
@@ -147,7 +177,7 @@ export function getUnifiedEffects(state: Pick<GameState, 'skillUpgrades' | 'skil
);
}
// ─── Stat Computation with All Effects ───────────────────────────────────────────
// ─── Stat Computation with All Effects ───────────────────────────────────────
/**
* Compute max mana with all effect sources
+1 -1
View File
@@ -17,7 +17,7 @@ import {
getElementalBonus,
} from '../store/computed';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier, HOURS_PER_TICK, TICK_MS } from '../constants';
import { hasSpecial, SPECIAL_EFFECTS } from '../upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
/**
* Hook for all mana-related derived stats
@@ -0,0 +1,52 @@
// ─── Skill Upgrade Selection Hook ────────────────────────
// Hook for managing milestone upgrade selection state in SkillsTab
import { useState, useMemo, useCallback, SetStateAction, Dispatch } from 'react';
import type { SkillUpgradeChoice } from '@/lib/game/types';
export interface UseSkillUpgradeSelectionResult {
pendingSelections: string[];
setPendingSelections: Dispatch<SetStateAction<string[]>>;
toggleUpgrade: (upgradeId: string, available: SkillUpgradeChoice[], alreadySelected: string[]) => void;
handleConfirm: (upgradeDialogSkill: string | null, upgradeDialogMilestone: 5 | 10, commitSkillUpgrades: (skillId: string, selections: string[], milestone: 5 | 10) => void, onClose: () => void) => void;
handleCancel: (onClose: () => void) => void;
}
/**
* Hook for managing skill upgrade selection state in the SkillsTab milestone upgrade dialog.
* Manages pending selections across the dialog open/close cycle.
*/
export function useSkillUpgradeSelection(): UseSkillUpgradeSelectionResult {
const [pendingSelections, setPendingSelections] = useState<string[]>([]);
const toggleUpgrade = useCallback((upgradeId: string, available: SkillUpgradeChoice[], alreadySelected: string[]) => {
const currentSelections = pendingSelections.length > 0 ? pendingSelections : alreadySelected;
if (currentSelections.includes(upgradeId)) {
setPendingSelections(currentSelections.filter(id => id !== upgradeId));
} else if (currentSelections.length < 2) {
setPendingSelections([...currentSelections, upgradeId]);
}
}, [pendingSelections]);
const handleConfirm = useCallback((upgradeDialogSkill: string | null, upgradeDialogMilestone: 5 | 10, commitSkillUpgrades: (skillId: string, selections: string[], milestone: 5 | 10) => void, onClose: () => void) => {
const currentSelections = pendingSelections.length > 0 ? pendingSelections : [];
if (currentSelections.length === 2 && upgradeDialogSkill) {
commitSkillUpgrades(upgradeDialogSkill, currentSelections, upgradeDialogMilestone);
}
setPendingSelections([]);
onClose();
}, [pendingSelections]);
const handleCancel = useCallback((onClose: () => void) => {
setPendingSelections([]);
onClose();
}, []);
return useMemo(() => ({
pendingSelections,
setPendingSelections,
toggleUpgrade,
handleConfirm,
handleCancel,
}), [pendingSelections, toggleUpgrade, handleConfirm, handleCancel]);
}
+94
View File
@@ -0,0 +1,94 @@
// ─── Special Effect IDs ────────────────────────────────────────────────────────
// These are the IDs used in the 'specialId' field of special effects
import type { ComputedEffects } from './upgrade-effects.types';
export const SPECIAL_EFFECTS = {
// Mana Flow special effects
MANA_CASCADE: 'manaCascade',
STEADY_STREAM: 'steadyStream',
MANA_TORRENT: 'manaTorrent',
FLOW_SURGE: 'flowSurge',
MANA_OVERFLOW: 'manaOverflow',
MANA_WATERFALL: 'manaWaterfall',
ETERNAL_FLOW: 'eternalFlow',
// Mana Well special effects
DESPAIR_WELLS: 'despairWells',
DESPERATE_WELLS: 'desperateWells',
MANA_ECHO: 'manaEcho',
EMERGENCY_RESERVE: 'emergencyReserve',
MANA_THRESHOLD: 'manaThreshold',
MANA_CONVERSION: 'manaConversion',
PANIC_RESERVE: 'panicReserve',
MANA_CONDENSE: 'manaCondense',
DEEP_RESERVE: 'deepReserve',
MANA_TIDE: 'manaTide',
VOID_STORAGE: 'voidStorage',
MANA_CORE: 'manaCore',
MANA_HEART: 'manaHeart',
MANA_GENESIS: 'manaGenesis',
// Mana Overflow special effects
CLICK_SURGE: 'clickSurge',
MANA_FLOOD: 'manaFlood',
// Combat special effects
FIRST_STRIKE: 'firstStrike',
OVERPOWER: 'overpower',
BERSERKER: 'berserker',
EXECUTIONER: 'executioner',
COMBO_MASTER: 'comboMaster',
ADRENALINE_RUSH: 'adrenalineRush',
// Study special effects
QUICK_GRASP: 'quickGrasp',
DEEP_CONCENTRATION: 'deepConcentration',
QUICK_MASTERY: 'quickMastery',
PARALLEL_STUDY: 'parallelStudy',
STUDY_MOMENTUM: 'studyMomentum',
KNOWLEDGE_ECHO: 'knowledgeEcho',
KNOWLEDGE_TRANSFER: 'knowledgeTransfer',
MENTAL_CLARITY: 'mentalClarity',
STUDY_REFUND: 'studyRefund',
DEEP_UNDERSTANDING: 'deepUnderstanding',
STUDY_RUSH: 'studyRush',
CHAIN_STUDY: 'chainStudy',
// Element special effects
ELEMENTAL_AFFINITY: 'elementalAffinity',
EXOTIC_MASTERY: 'exoticMastery',
ELEMENTAL_RESONANCE: 'elementalResonance',
MANA_CONDUIT: 'manaConduit',
// Enchanting special effects
ENCHANT_MASTERY: 'enchantMastery',
ENCHANT_PRESERVATION: 'enchantPreservation',
THRIFTY_ENCHANTER: 'thriftyEnchanter',
OPTIMIZED_ENCHANTING: 'optimizedEnchanting',
HASTY_ENCHANTER: 'hastyEnchanter',
INSTANT_DESIGNS: 'instantDesigns',
PURE_ESSENCE: 'pureEssence',
// Crafting special effects
BATCH_CRAFTING: 'batchCrafting',
MASS_PRODUCTION: 'massProduction',
SCAVENGE: 'scavenge',
RECLAIM: 'reclaim',
// Golemancy special effects
GOLEM_FURY: 'golemFury',
GOLEM_RESONANCE: 'golemResonance',
RAPID_STRIKES: 'rapidStrikes',
BLITZ_ATTACK: 'blitzAttack',
// Ascension special effects
INSIGHT_BOUNTY: 'insightBounty',
} as const;
/**
* Check if a special effect is active
*/
export function hasSpecial(effects: ComputedEffects, specialId: string): boolean {
return effects?.specials?.has(specialId) ?? false;
}
+98 -11
View File
@@ -32,7 +32,9 @@ import {
SPEED_ROOM_CONFIG,
FLOOR_ARMOR_CONFIG,
} from './constants';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects';
import { computeEffects } from './upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
import type { ComputedEffects } from './upgrade-effects.types';
import {
computeAllEffects,
getUnifiedEffects,
@@ -76,11 +78,14 @@ const DEFAULT_EFFECTS: ComputedEffects = {
freeStudyChance: 0,
elementCapMultiplier: 1,
elementCapBonus: 0,
perElementCapBonus: {},
conversionCostMultiplier: 1,
doubleCraftChance: 0,
permanentRegenBonus: 0,
specials: new Set(),
activeUpgrades: [],
skillLevelMultiplier: 1,
enchantmentPowerMultiplier: 1,
};
// ─── Helper Functions ─────────────────────────────────────────────────────────
@@ -166,6 +171,38 @@ export function getDodgeChance(floor: number): number {
);
}
// Get health regen for an enemy (0-1 as percentage of max HP per tick)
export function getEnemyHealthRegen(floor: number, element: string): number {
// Higher floors have a chance for enemies with health regen
if (floor < 15) return 0;
// Health regen becomes more common on higher floors
const regenChance = Math.min(0.3, (floor - 15) * 0.005); // Max 30% chance
if (Math.random() > regenChance) return 0;
// Scale regen with floor (0.5% to 3% of max HP per tick)
const floorProgress = Math.min(1, (floor - 15) / 85);
return 0.005 + floorProgress * 0.025;
}
// Get barrier for an enemy (0-1 as percentage of max HP)
export function getEnemyBarrier(floor: number, element: string): number {
// Barrier appears on higher floors, more common with certain elements
if (floor < 20) return 0;
// Barrier chance based on element - light/water/earth more likely
const barrierElements = ['light', 'water', 'earth'];
const baseChance = barrierElements.includes(element) ? 0.15 : 0.08;
const floorBonus = Math.min(0.25, (floor - 20) * 0.003); // Max 25% additional chance
const barrierChance = Math.min(0.4, baseChance + floorBonus);
if (Math.random() > barrierChance) return 0;
// Barrier is 10% to 30% of max HP
const floorProgress = Math.min(1, (floor - 20) / 80);
return 0.1 + floorProgress * 0.2;
}
// ─── Enemy Naming System ───────────────────────────────────────────────
// Generate enemy names based on element and floor tier
const ENEMY_NAMES_BY_ELEMENT: Record<string, string[]> = {
@@ -211,6 +248,8 @@ export function generateSwarmEnemies(floor: number): EnemyState[] {
maxHP: Math.floor(baseHP * SWARM_CONFIG.hpMultiplier),
armor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
dodgeChance: 0,
healthRegen: getEnemyHealthRegen(floor, element),
barrier: getEnemyBarrier(floor, element),
element,
});
}
@@ -235,6 +274,8 @@ export function generateFloorState(floor: number): FloorState {
maxHP: guardian.hp,
armor: guardian.armor || 0,
dodgeChance: 0,
healthRegen: 0.01, // Guardians have 1% HP regen per tick
barrier: 0, // Guardians don't have barrier by default (could be added later)
element: guardian.element,
}],
};
@@ -256,6 +297,8 @@ export function generateFloorState(floor: number): FloorState {
maxHP: baseHP,
armor: getFloorArmor(floor),
dodgeChance: getDodgeChance(floor),
healthRegen: getEnemyHealthRegen(floor, element),
barrier: getEnemyBarrier(floor, element),
element,
}],
};
@@ -287,6 +330,8 @@ export function generateFloorState(floor: number): FloorState {
maxHP: baseHP,
armor: getFloorArmor(floor),
dodgeChance: 0,
healthRegen: getEnemyHealthRegen(floor, element),
barrier: getEnemyBarrier(floor, element),
element,
}],
};
@@ -370,17 +415,37 @@ export function computeMaxMana(
}
export function computeElementMax(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'unlockedManaTypeUpgrades'>,
effects?: ComputedEffects | UnifiedEffects,
element?: string
): number {
const pu = state.prestigeUpgrades;
const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25;
// Apply unlockedManaTypeCapacity bonus for specific element (always apply)
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);
}
// Apply upgrade effects if provided
if (effects) {
return Math.floor((base + effects.elementCapBonus) * effects.elementCapMultiplier);
let bonus = effects.elementCapBonus || 0; // Global bonus
// Add per-element bonus if element is specified and available
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 base;
return adjustedBase;
}
export function computeRegen(
@@ -436,7 +501,7 @@ export function computeEffectiveRegenForDisplay(
* Compute regen with dynamic special effects (needs current mana, max mana, incursion)
*/
export function computeEffectiveRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers'>,
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
effects?: ComputedEffects
): number {
// Base regen from existing function
@@ -627,8 +692,9 @@ function deductSpellCost(
function makeInitial(overrides: Partial<GameState> = {}): GameState {
const pu = overrides.prestigeUpgrades || {};
const startFloor = 1 + (pu.spireKey || 0) * 2;
const elemMax = computeElementMax({ skills: overrides.skills || {}, prestigeUpgrades: pu });
const effects = overrides.skillUpgrades ? computeEffects(overrides.skillUpgrades || {}, overrides.skillTiers || {}) : undefined;
const manaHeartBonus = overrides.manaHeartBonus || 0;
const unlockedManaTypeUpgrades = overrides.unlockedManaTypeUpgrades || [];
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
Object.keys(ELEMENTS).forEach((k) => {
@@ -640,9 +706,18 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
startAmount = pu.elemStart * 5;
}
// Calculate per-element max capacity including unlockedManaTypeCapacity upgrades
const baseElemMax = computeElementMax({
skills: overrides.skills || {},
prestigeUpgrades: pu,
skillUpgrades: overrides.skillUpgrades || {},
skillTiers: overrides.skillTiers || {},
unlockedManaTypeUpgrades
}, effects, k);
elements[k] = {
current: overrides.elements?.[k]?.current ?? startAmount,
max: elemMax,
max: baseElemMax,
unlocked: isUnlocked,
};
});
@@ -821,6 +896,9 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
// Activity Log (for Spire Mode UI)
activityLog: [],
// Track selected mana types for unlockedManaTypeCapacity upgrade
unlockedManaTypeUpgrades: unlockedManaTypeUpgrades,
};
}
@@ -868,7 +946,7 @@ export interface GameStore extends GameState, CraftingActions {
convertMana: (element: string, amount: number) => void;
unlockElement: (element: string) => void;
craftComposite: (target: string) => void;
doPrestige: (id: string) => void;
doPrestige: (id: string, selectedManaType?: string) => void;
startNewLoop: () => void;
togglePause: () => void;
resetGame: () => void;
@@ -2164,7 +2242,7 @@ export const useGameStore = create<GameStore>()(
const craftBonus = 1 + (state.skills.elemCrafting || 0) * 0.25;
const outputAmount = Math.floor(craftBonus);
const effects = getUnifiedEffects(state);
const effects = getUnifiedEffects(state.skillUpgrades, state.skillTiers, state.equipmentInstances, state.equippedInstances);
const elemMax = computeElementMax(state, effects);
newElems[target] = {
...(newElems[target] || { current: 0, max: elemMax, unlocked: false }),
@@ -2179,7 +2257,7 @@ export const useGameStore = create<GameStore>()(
});
},
doPrestige: (id: string) => {
doPrestige: (id: string, selectedManaType?: string) => {
const state = get();
const pd = PRESTIGE_DEF[id];
if (!pd) return;
@@ -2188,10 +2266,18 @@ export const useGameStore = create<GameStore>()(
if (lvl >= pd.max || state.insight < pd.cost) return;
const newPU = { ...state.prestigeUpgrades, [id]: lvl + 1 };
// For unlockedManaTypeCapacity, track the selected mana type
let newUnlockedManaTypeUpgrades = state.unlockedManaTypeUpgrades || [];
if (id === 'unlockedManaTypeCapacity' && selectedManaType) {
newUnlockedManaTypeUpgrades = [...newUnlockedManaTypeUpgrades, { typeId: selectedManaType, level: 1 }];
}
set({
insight: state.insight - pd.cost,
prestigeUpgrades: newPU,
memorySlots: id === 'deepMemory' ? state.memorySlots + 1 : state.memorySlots,
unlockedManaTypeUpgrades: newUnlockedManaTypeUpgrades,
log: [`${pd.name} upgraded to Lv.${lvl + 1}!`, ...state.log.slice(0, 49)],
});
},
@@ -2231,6 +2317,7 @@ export const useGameStore = create<GameStore>()(
memories: state.memories,
skills: state.skills, // Keep skills through temporal memory for now
manaHeartBonus: newHeartBonus,
unlockedManaTypeUpgrades: state.unlockedManaTypeUpgrades || [],
});
// Set the kept mana from EMERGENCY_RESERVE
+2 -1
View File
@@ -5,7 +5,8 @@ import type { StateCreator } from 'zustand';
import type { GameState, GameAction, SpellCost } from '../types';
import { GUARDIANS, SPELLS_DEF, ELEMENTS, ELEMENT_OPPOSITES } from '../constants';
import { getFloorMaxHP, getFloorElement, calcDamage, computePactMultiplier, canAffordSpellCost, deductSpellCost } from './computed';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '../upgrade-effects';
import { computeEffects } from '../upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
export interface CombatSlice {
// State
+5 -14
View File
@@ -3,6 +3,8 @@
import type { GameState } from '../types';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, ELEMENT_OPPOSITES } from '../constants';
import { computeEffects } from '../upgrade-effects';
import type { ComputedEffects } from '../upgrade-effects.types';
import type { UnifiedEffects } from '../effects';
import { getTierMultiplier } from '../skill-evolution';
// Helper to get effective skill level accounting for tiers
@@ -40,20 +42,9 @@ export function computeMaxMana(
return Math.floor((base + computedEffects.maxManaBonus) * computedEffects.maxManaMultiplier * heartMultiplier);
}
export function computeElementMax(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ReturnType<typeof computeEffects>
): number {
const pu = state.prestigeUpgrades;
const skillTiers = state.skillTiers || {};
const skillUpgrades = state.skillUpgrades || {};
const elemAttuneLevel = getEffectiveSkillLevel(state.skills, 'elemAttune', skillTiers);
const base = 10 + elemAttuneLevel.level * 50 * elemAttuneLevel.tierMultiplier + (pu.elementalAttune || 0) * 25;
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
return Math.floor((base + computedEffects.elementCapBonus) * computedEffects.elementCapMultiplier);
}
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
// This file no longer exports computeElementMax to avoid duplicate export issues
// Import computeElementMax from '../store' instead
export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
+4 -2
View File
@@ -4,8 +4,10 @@
import type { StateCreator } from 'zustand';
import type { GameState, ElementState, SpellCost } from '../types';
import { ELEMENTS, MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants';
import { computeMaxMana, computeElementMax, computeClickMana, canAffordSpellCost, getMeditationBonus } from './computed';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '../upgrade-effects';
import { computeMaxMana, computeClickMana, canAffordSpellCost, getMeditationBonus } from './computed';
import { computeElementMax } from '../store';
import { computeEffects } from '../upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
export interface ManaSlice {
// State
+2 -1
View File
@@ -5,7 +5,8 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { TICK_MS, HOURS_PER_TICK, MAX_DAY, SPELLS_DEF, GUARDIANS, getStudySpeedMultiplier } from '../constants';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '../upgrade-effects';
import { computeEffects } from '../upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
import {
computeMaxMana,
computeRegen,
+2 -1
View File
@@ -3,7 +3,8 @@
import type { GameState } from './types';
import { SKILLS_DEF, SPELLS_DEF, getStudyCostMultiplier } from './constants';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from './upgrade-effects';
import { computeEffects } from './upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
// ─── Study Actions Interface ──────────────────────────────────────────────────
+4
View File
@@ -43,6 +43,8 @@ export interface EnemyState {
maxHP: number;
armor: number; // Damage reduction (0-1)
dodgeChance: number; // For speed rooms (0-1)
healthRegen?: number; // HP regenerated per tick (0-1 as percentage of max HP)
barrier?: number; // Shield that absorbs damage before HP (0-1 as percentage of max HP)
element: string;
}
@@ -227,6 +229,8 @@ export interface GameState {
prestigeUpgrades: Record<string, number>;
memorySlots: number;
memories: string[];
// Track selected mana types for unlockedManaTypeCapacity upgrade
unlockedManaTypeUpgrades: Array<{ typeId: string; level: number }>;
// Mana Well Effects (Phase 4)
manaHeartBonus: number; // Cumulative +10% max mana per loop from MANA_HEART
+11 -276
View File
@@ -1,153 +1,13 @@
// ─── Upgrade Effect System ─────────────────────────────────────────────────────
// ─── 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';
import type { ActiveUpgradeEffect, ComputedEffects } from './upgrade-effects.types';
import { SPECIAL_EFFECTS, hasSpecial } from './special-effects';
import { computeDynamicRegen, computeDynamicClickMana, computeDynamicDamage } from './dynamic-compute';
// ─── 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[];
// DEEP_UNDERSTANDING: +10% bonus from all skill levels
skillLevelMultiplier: number;
}
// ─── 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_WATERFALL: 'manaWaterfall', // +0.25 regen per 100 max mana (upgraded cascade)
ETERNAL_FLOW: 'eternalFlow', // Regen immune to all penalties
// Mana Well special effects
DESPAIR_WELLS: 'despairWells', // +50% regen when below 25% mana (task name)
DESPERATE_WELLS: 'desperateWells', // +50% regen when below 25% mana (legacy name)
MANA_ECHO: 'manaEcho', // 10% chance double mana from clicks
EMERGENCY_RESERVE: 'emergencyReserve', // Keep 10% mana on new loop
MANA_THRESHOLD: 'manaThreshold', // +30% max mana, -10% regen trade-off
MANA_CONVERSION: 'manaConversion', // Convert 5% max mana to click bonus
PANIC_RESERVE: 'panicReserve', // +100% regen when below 10% mana
MANA_CONDENSE: 'manaCondense', // +1% max mana per 1000 gathered
DEEP_RESERVE: 'deepReserve', // +0.5 regen per 100 max mana
MANA_TIDE: 'manaTide', // Regen pulses ±50%
VOID_STORAGE: 'voidStorage', // Store 150% max temporarily
MANA_CORE: 'manaCore', // 0.5% max mana as regen
MANA_HEART: 'manaHeart', // +10% max mana per loop
MANA_GENESIS: 'manaGenesis', // Generate 1% max mana per hour
// Mana Overflow special effects
CLICK_SURGE: 'clickSurge', // +50% click mana above 90% mana
MANA_FLOOD: 'manaFlood', // +75% click mana above 75% mana
// 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
EXECUTIONER: 'executioner', // +50% damage when enemy below 25% HP
COMBO_MASTER: 'comboMaster', // Every 5th attack deals 3x damage
ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana
// 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
// Enchanting special effects
ENCHANT_MASTERY: 'enchantMastery', // 2 enchantment designs in progress
ENCHANT_PRESERVATION: 'enchantPreservation', // 25% chance free enchant
THRIFTY_ENCHANTER: 'thriftyEnchanter', // +10% chance free enchantment
OPTIMIZED_ENCHANTING: 'optimizedEnchanting', // +25% chance free enchantment
HASTY_ENCHANTER: 'hastyEnchanter', // +25% speed for repeat designs
INSTANT_DESIGNS: 'instantDesigns', // 10% instant design completion
PURE_ESSENCE: 'pureEssence', // +25% power for tier 1 enchants
// Crafting special effects
BATCH_CRAFTING: 'batchCrafting', // Craft 2 items at 75% speed each
MASS_PRODUCTION: 'massProduction', // Craft 3 items at full speed
SCAVENGE: 'scavenge', // Recover 10% materials from broken
RECLAIM: 'reclaim', // Recover 25% materials from broken
// Golemancy special effects
GOLEM_FURY: 'golemFury', // +50% attack speed for first 2 floors
GOLEM_RESONANCE: 'golemResonance', // Golems share 10% damage
RAPID_STRIKES: 'rapidStrikes', // +25% attack speed for first 3 floors
BLITZ_ATTACK: 'blitzAttack', // +50% attack speed for first 5 floors
// Ascension special effects
INSIGHT_BOUNTY: 'insightBounty', // +25% insight from guardians
} as const;
// ─── Upgrade Definition Cache ─────────────────────────────────────────────────
// ─── Upgrade Definition Cache ───────────────────────────
// Cache all upgrades by ID for quick lookup
const upgradeDefinitionsById: Map<string, SkillUpgradeChoice> = new Map();
@@ -165,7 +25,7 @@ function buildUpgradeCache(): void {
}
}
// ─── Helper Functions ─────────────────────────────────────────────────────────
// ─── Helper Functions ──────────────────────────────
/**
* Get all selected upgrades with their full effect definitions
@@ -229,12 +89,14 @@ export function computeEffects(
freeStudyChance: 0,
elementCapMultiplier: 1,
elementCapBonus: 0,
perElementCapBonus: {},
conversionCostMultiplier: 1,
doubleCraftChance: 0,
permanentRegenBonus: 0,
specials: new Set<string>(),
activeUpgrades,
skillLevelMultiplier: 1,
enchantmentPowerMultiplier: 1,
};
// Apply DEEP_UNDERSTANDING: +10% bonus from all skill levels
@@ -286,10 +148,11 @@ export function computeEffects(
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;
case 'enchantPower':
effects.enchantmentPowerMultiplier *= effect.value;
break;
}
} else if (effect.type === 'bonus' && effect.stat && effect.value !== undefined) {
// Bonus effects (add to the stat)
@@ -314,7 +177,6 @@ export function computeEffects(
break;
}
} else if (effect.type === 'special' && effect.specialId) {
// Special effects - add to the set for game logic to check
effects.specials.add(effect.specialId);
}
}
@@ -327,130 +189,3 @@ export function computeEffects(
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 Waterfall: +0.25 regen per 100 max mana (upgraded cascade)
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_WATERFALL)) {
regen += Math.floor(maxMana / 100) * 0.25;
}
// Mana Torrent: +50% regen when above 75% mana
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TORRENT) && currentMana > maxMana * 0.75) {
regen *= 1.5;
}
// Desperate Wells / Despair Wells: +50% regen when below 25% mana
if ((hasSpecial(effects, SPECIAL_EFFECTS.DESPERATE_WELLS) || hasSpecial(effects, SPECIAL_EFFECTS.DESPAIR_WELLS)) && currentMana < maxMana * 0.25) {
regen *= 1.5;
}
// Panic Reserve: +100% regen when below 10% mana
if (hasSpecial(effects, SPECIAL_EFFECTS.PANIC_RESERVE) && currentMana < maxMana * 0.1) {
regen *= 2.0;
}
// Deep Reserve: +0.5 regen per 100 max mana
if (hasSpecial(effects, SPECIAL_EFFECTS.DEEP_RESERVE)) {
regen += Math.floor(maxMana / 100) * 0.5;
}
// Mana Core: 0.5% of max mana added as regen
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CORE)) {
regen += maxMana * 0.005;
}
// Mana Tide: Regen pulses ±50% (sinusoidal based on time)
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TIDE)) {
const pulseFactor = 0.5 + 0.5 * Math.sin(Date.now() / 10000); // 10 second cycles
regen *= (0.5 + pulseFactor * 0.5); // Range: 0.5x to 1.0x
}
// Eternal Flow: Regen immune to ALL penalties (stronger than Steady Stream)
if (hasSpecial(effects, SPECIAL_EFFECTS.ETERNAL_FLOW)) {
return regen * effects.regenMultiplier;
}
// 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
// Mana Genesis: Generate 1% of max mana per hour passively
// This is handled in the game loop (store.ts), not here
// Mana Heart: +10% max mana per loop (permanent)
// This is applied during loop reset in store.ts
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;
}
+63
View File
@@ -0,0 +1,63 @@
// ─── Upgrade Effect Types ────────────────────────────────────────────────────
// Type interfaces for upgrade effects
import type { SkillUpgradeChoice, SkillUpgradeEffect } from './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;
perElementCapBonus: Record<string, number>;
conversionCostMultiplier: number;
doubleCraftChance: number;
// Special values
permanentRegenBonus: number;
// Special effect flags
specials: Set<string>;
// All active upgrades for display
activeUpgrades: ActiveUpgradeEffect[];
// DEEP_UNDERSTANDING: +10% bonus from all skill levels
skillLevelMultiplier: number;
// Enchantment Power
enchantmentPowerMultiplier: number;
}
+1 -1
View File
@@ -5,13 +5,13 @@ export { fmt, fmtDec } from './formatting';
export { getFloorMaxHP, getFloorElement } from './floor-utils';
export {
computeMaxMana,
computeElementMax,
computeRegen,
computeEffectiveRegen,
computeEffectiveRegenForDisplay,
computeClickMana,
getMeditationBonus
} from './mana-utils';
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
export {
getElementalBonus,
getBoonBonuses,
+5 -15
View File
@@ -1,7 +1,7 @@
// ─── Mana & Regen Utilities ──────────────────────────────────────────────────
import type { GameState } from '../types';
import type { ComputedEffects } from '../upgrade-effects';
import type { ComputedEffects } from '../upgrade-effects.types';
import { HOURS_PER_TICK } from '../constants';
import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
@@ -22,19 +22,9 @@ export function computeMaxMana(
return base;
}
export function computeElementMax(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
): number {
const pu = state.prestigeUpgrades;
const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25;
// Apply upgrade effects if provided
if (effects) {
return Math.floor((base + effects.elementCapBonus) * effects.elementCapMultiplier);
}
return base;
}
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
// This file no longer exports computeElementMax to avoid duplicate export issues
// Import computeElementMax from '../store' instead
export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
@@ -83,7 +73,7 @@ export function computeEffectiveRegenForDisplay(
* Compute regen with dynamic special effects (needs current mana, max mana, incursion)
*/
export function computeEffectiveRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers'>,
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
effects?: ComputedEffects
): number {
// Base regen from existing function