All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s
363 lines
12 KiB
TypeScript
Executable File
363 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
|
|
|
|
// 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
|
|
} 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;
|
|
}
|