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
+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