feat: add prestige system and skill upgrades with comprehensive documentation
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 5m57s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 5m57s
This commit is contained in:
+98
-11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user