Initial commit
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s
This commit is contained in:
362
src/lib/game/upgrade-effects.ts
Executable file
362
src/lib/game/upgrade-effects.ts
Executable file
@@ -0,0 +1,362 @@
|
||||
// ─── 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;
|
||||
}
|
||||
Reference in New Issue
Block a user