Initial commit
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s

This commit is contained in:
Z User
2026-03-28 15:00:03 +00:00
commit 22dfdb5910
134 changed files with 27218 additions and 0 deletions

View File

@@ -0,0 +1,272 @@
import { describe, it, expect } from 'vitest';
import {
fmt,
fmtDec,
getFloorMaxHP,
getFloorElement,
canAffordSpellCost,
deductSpellCost,
computeMaxMana,
computeRegen,
computeClickMana,
getMeditationBonus,
getIncursionStrength,
} from '../computed-stats';
import { MAX_DAY, INCURSION_START_DAY, HOURS_PER_TICK } from '../constants';
describe('fmt', () => {
it('should format numbers < 1000 as integers', () => {
expect(fmt(500)).toBe('500');
expect(fmt(0)).toBe('0');
expect(fmt(999)).toBe('999');
});
it('should format thousands with K suffix', () => {
expect(fmt(1500)).toBe('1.5K');
expect(fmt(1000)).toBe('1.0K');
expect(fmt(9999)).toBe('10.0K');
});
it('should format millions with M suffix', () => {
expect(fmt(1500000)).toBe('1.50M');
expect(fmt(1000000)).toBe('1.00M');
});
it('should format billions with B suffix', () => {
expect(fmt(1500000000)).toBe('1.50B');
expect(fmt(1000000000)).toBe('1.00B');
});
it('should handle edge cases', () => {
expect(fmt(NaN)).toBe('0');
expect(fmt(Infinity)).toBe('0');
expect(fmt(-100)).toBe('-100');
});
});
describe('fmtDec', () => {
it('should format decimal numbers', () => {
expect(fmtDec(1.5, 1)).toBe('1.5');
expect(fmtDec(1.234, 2)).toBe('1.23');
expect(fmtDec(1000.5, 1)).toBe('1000.5');
});
});
describe('getFloorMaxHP', () => {
it('should return base HP for floor 1', () => {
const hp = getFloorMaxHP(1);
expect(hp).toBeGreaterThan(0);
});
it('should scale HP with floor number', () => {
const hp1 = getFloorMaxHP(1);
const hp5 = getFloorMaxHP(5);
const hp10 = getFloorMaxHP(10);
expect(hp5).toBeGreaterThan(hp1);
expect(hp10).toBeGreaterThan(hp5);
});
it('should return higher HP for guardian floors', () => {
// Floor 10 is Ignis Prime guardian
const hp10 = getFloorMaxHP(10);
const hp9 = getFloorMaxHP(9);
expect(hp10).toBeGreaterThan(hp9 * 2); // Guardians have much more HP
});
});
describe('getFloorElement', () => {
it('should return an element string for valid floors', () => {
const elem = getFloorElement(1);
expect(typeof elem).toBe('string');
expect(['fire', 'water', 'earth', 'air', 'raw']).toContain(elem);
});
it('should cycle through elements based on floor number', () => {
// Check that floors have different elements
const elem1 = getFloorElement(1);
const elem7 = getFloorElement(7);
// Since it cycles every 5 floors, floor 1 and 7 might have same element
const elem2 = getFloorElement(2);
// Floor 1 and 2 should have different elements (if cycle allows)
});
});
describe('canAffordSpellCost', () => {
it('should return true for raw mana cost when enough mana', () => {
const result = canAffordSpellCost({ type: 'raw', amount: 10 }, 50, {});
expect(result).toBe(true);
});
it('should return false for raw mana cost when not enough mana', () => {
const result = canAffordSpellCost({ type: 'raw', amount: 100 }, 50, {});
expect(result).toBe(false);
});
it('should handle zero cost', () => {
const result = canAffordSpellCost({ type: 'raw', amount: 0 }, 0, {});
expect(result).toBe(true);
});
it('should handle elemental costs', () => {
const elements = {
fire: { current: 10, max: 50, unlocked: true },
};
const result = canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
expect(result).toBe(true);
});
it('should return false for elemental cost when not enough', () => {
const elements = {
fire: { current: 3, max: 50, unlocked: true },
};
const result = canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
expect(result).toBe(false);
});
it('should return false for locked element', () => {
const elements = {
fire: { current: 10, max: 50, unlocked: false },
};
const result = canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
expect(result).toBe(false);
});
});
describe('deductSpellCost', () => {
it('should deduct raw mana correctly', () => {
const result = deductSpellCost({ type: 'raw', amount: 10 }, 50, {});
expect(result.rawMana).toBe(40);
});
it('should not go below zero', () => {
const result = deductSpellCost({ type: 'raw', amount: 100 }, 50, {});
expect(result.rawMana).toBe(0);
});
it('should deduct elemental mana correctly', () => {
const elements = {
fire: { current: 10, max: 50, unlocked: true },
};
const result = deductSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
expect(result.elements.fire.current).toBe(5);
});
it('should return same values when cost is zero', () => {
const elements = {
fire: { current: 10, max: 50, unlocked: true },
};
const result = deductSpellCost({ type: 'raw', amount: 0 }, 50, elements);
expect(result.rawMana).toBe(50);
expect(result.elements.fire.current).toBe(10);
});
});
describe('computeMaxMana', () => {
it('should return base 100 with no skills or upgrades', () => {
const state = {
skills: {},
prestigeUpgrades: {},
skillUpgrades: {},
skillTiers: {},
};
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 };
const result = computeMaxMana(state, effects);
expect(result).toBe(100);
});
it('should include manaWell prestige upgrade', () => {
const state = {
skills: {},
prestigeUpgrades: { manaWell: 5 },
skillUpgrades: {},
skillTiers: {},
};
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 };
const result = computeMaxMana(state, effects);
expect(result).toBe(100 + 5 * 500); // Base + 500 per level
});
it('should apply multiplier from effects', () => {
const state = {
skills: {},
prestigeUpgrades: {},
skillUpgrades: {},
skillTiers: {},
};
const effects = { maxManaBonus: 0, maxManaMultiplier: 1.5 };
const result = computeMaxMana(state, effects);
expect(result).toBe(150); // 100 * 1.5
});
it('should apply bonus from effects', () => {
const state = {
skills: {},
prestigeUpgrades: {},
skillUpgrades: {},
skillTiers: {},
};
const effects = { maxManaBonus: 50, maxManaMultiplier: 1 };
const result = computeMaxMana(state, effects);
expect(result).toBe(150); // 100 + 50
});
});
describe('computeRegen', () => {
it('should return base regen with no skills', () => {
const state = {
skills: {},
prestigeUpgrades: {},
skillUpgrades: {},
skillTiers: {},
};
const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 };
const result = computeRegen(state, effects);
expect(result).toBe(2); // Base regen
});
});
describe('computeClickMana', () => {
it('should return base click mana with no skills', () => {
const state = {
skills: {},
prestigeUpgrades: {},
skillUpgrades: {},
skillTiers: {},
};
const effects = { clickManaBonus: 0, clickManaMultiplier: 1 };
const result = computeClickMana(state, effects);
expect(result).toBeGreaterThanOrEqual(1);
});
});
describe('getMeditationBonus', () => {
it('should return 1.0 with zero ticks', () => {
const result = getMeditationBonus(0, {});
expect(result).toBe(1.0);
});
it('should increase with more ticks', () => {
const result1 = getMeditationBonus(10, {});
const result2 = getMeditationBonus(100, {});
expect(result2).toBeGreaterThan(result1);
});
});
describe('getIncursionStrength', () => {
it('should return 0 before incursion start day', () => {
const result = getIncursionStrength(1, 12);
expect(result).toBe(0);
});
it('should return positive value during incursion', () => {
// After incursion start day
const result = getIncursionStrength(INCURSION_START_DAY, 12);
expect(result).toBeGreaterThan(0);
});
it('should increase with later days', () => {
const result1 = getIncursionStrength(INCURSION_START_DAY, 12);
const result2 = getIncursionStrength(MAX_DAY, 12);
expect(result2).toBeGreaterThan(result1);
});
});

567
src/lib/game/attunements.ts Executable file
View File

@@ -0,0 +1,567 @@
// ─── Attunement System ─────────────────────────────────────────────────────────
// Attunements are powerful magical bonds tied to specific body locations
// Each grants a unique capability, primary mana type, and skill tree
import type { SkillDef } from './types';
// ─── Body Slots ───────────────────────────────────────────────────────────────
export type AttunementSlot =
| 'rightHand'
| 'leftHand'
| 'head'
| 'back'
| 'chest'
| 'leftLeg'
| 'rightLeg';
export const ATTUNEMENT_SLOTS: AttunementSlot[] = [
'rightHand',
'leftHand',
'head',
'back',
'chest',
'leftLeg',
'rightLeg',
];
// Slot display names
export const ATTUNEMENT_SLOT_NAMES: Record<AttunementSlot, string> = {
rightHand: 'Right Hand',
leftHand: 'Left Hand',
head: 'Head',
back: 'Back',
chest: 'Heart',
leftLeg: 'Left Leg',
rightLeg: 'Right Leg',
};
// ─── Mana Types ───────────────────────────────────────────────────────────────
export type ManaType =
// Primary mana types from attunements
| 'transference' // Enchanter - moving/enchanting
| 'form' // Caster - shaping spells
| 'vision' // Seer - perception/revelation
| 'barrier' // Warden - protection/defense
| 'flow' // Strider - movement/swiftness
| 'stability' // Anchor - grounding/endurance
// Guardian pact types (Invoker)
| 'fire'
| 'water'
| 'earth'
| 'air'
| 'light'
| 'dark'
| 'life'
| 'death'
// Raw mana
| 'raw';
// ─── Attunement Types ─────────────────────────────────────────────────────────
export type AttunementType =
| 'enchanter'
| 'caster'
| 'seer'
| 'warden'
| 'invoker'
| 'strider'
| 'anchor';
// ─── Attunement Definition ────────────────────────────────────────────────────
export interface AttunementDef {
id: AttunementType;
name: string;
slot: AttunementSlot;
description: string;
capability: string; // What this attunement unlocks
primaryManaType: ManaType | null; // null for Invoker (uses guardian types)
rawManaRegen: number; // Base raw mana regen bonus
autoConvertRate: number; // Raw mana -> primary mana per hour
skills: Record<string, SkillDef>; // Attunement-specific skills
icon: string; // Lucide icon name
color: string; // Theme color
}
// ─── Attunement State ─────────────────────────────────────────────────────────
export interface AttunementState {
unlocked: boolean;
level: number; // Attunement level (from challenges)
manaPool: number; // Current primary mana
maxMana: number; // Max primary mana pool
}
// ─── Attunement Definitions ───────────────────────────────────────────────────
export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// ENCHANTER - Right Hand
// The starting attunement. Grants access to enchanting and transference magic.
// ═══════════════════════════════════════════════════════════════════════════
enchanter: {
id: 'enchanter',
name: 'Enchanter',
slot: 'rightHand',
description: 'Channel mana through your right hand to imbue equipment with magical properties.',
capability: 'Unlock enchanting. Apply enchantments using transference mana.',
primaryManaType: 'transference',
rawManaRegen: 0.5,
autoConvertRate: 0.2, // 0.2 transference per hour per raw regen
icon: 'Wand2',
color: '#8B5CF6', // Purple
skills: {
// Core enchanting skills
enchanting: {
name: 'Enchanting',
desc: 'Apply magical effects to equipment',
cat: 'enchanter',
max: 10,
base: 100,
studyTime: 8,
},
efficientEnchant: {
name: 'Efficient Enchanting',
desc: 'Reduce enchantment mana costs',
cat: 'enchanter',
max: 5,
base: 200,
studyTime: 12,
req: { enchanting: 3 },
},
disenchanting: {
name: 'Disenchanting',
desc: 'Remove enchantments and recover some mana',
cat: 'enchanter',
max: 5,
base: 150,
studyTime: 10,
req: { enchanting: 2 },
},
enchantSpeed: {
name: 'Swift Enchanting',
desc: 'Faster enchantment application',
cat: 'enchanter',
max: 5,
base: 175,
studyTime: 10,
req: { enchanting: 2 },
},
transferenceMastery: {
name: 'Transference Mastery',
desc: 'Increased transference mana pool and regen',
cat: 'enchanter',
max: 10,
base: 250,
studyTime: 15,
},
},
},
// ═══════════════════════════════════════════════════════════════════════════
// CASTER - Left Hand
// Shapes raw mana into spell patterns. Enhanced spell damage.
// ═══════════════════════════════════════════════════════════════════════════
caster: {
id: 'caster',
name: 'Caster',
slot: 'leftHand',
description: 'Shape mana into devastating spell patterns through your left hand.',
capability: 'Form mana shaping. +25% spell damage bonus.',
primaryManaType: 'form',
rawManaRegen: 0.3,
autoConvertRate: 0.15,
icon: 'Hand',
color: '#3B82F6', // Blue
skills: {
spellShaping: {
name: 'Spell Shaping',
desc: 'Increase spell damage and efficiency',
cat: 'caster',
max: 10,
base: 100,
studyTime: 8,
},
quickCast: {
name: 'Quick Cast',
desc: 'Faster spell casting speed',
cat: 'caster',
max: 10,
base: 120,
studyTime: 8,
},
spellEcho: {
name: 'Spell Echo',
desc: 'Chance to cast spells twice',
cat: 'caster',
max: 5,
base: 300,
studyTime: 15,
req: { spellShaping: 5 },
},
formMastery: {
name: 'Form Mastery',
desc: 'Increased form mana pool and regen',
cat: 'caster',
max: 10,
base: 250,
studyTime: 15,
},
},
},
// ═══════════════════════════════════════════════════════════════════════════
// SEER - Head
// Perception and revelation. Critical hit bonus and weakness detection.
// ═══════════════════════════════════════════════════════════════════════════
seer: {
id: 'seer',
name: 'Seer',
slot: 'head',
description: 'See beyond the veil. Reveal hidden truths and enemy weaknesses.',
capability: 'Reveal floor weaknesses. +20% critical hit chance.',
primaryManaType: 'vision',
rawManaRegen: 0.2,
autoConvertRate: 0.1,
icon: 'Eye',
color: '#F59E0B', // Amber
skills: {
insight: {
name: 'Insight',
desc: 'Increased critical hit chance',
cat: 'seer',
max: 10,
base: 100,
studyTime: 8,
},
revealWeakness: {
name: 'Reveal Weakness',
desc: 'Show enemy elemental weaknesses',
cat: 'seer',
max: 5,
base: 200,
studyTime: 12,
},
foresight: {
name: 'Foresight',
desc: 'Chance to anticipate and dodge attacks',
cat: 'seer',
max: 5,
base: 250,
studyTime: 15,
req: { insight: 5 },
},
visionMastery: {
name: 'Vision Mastery',
desc: 'Increased vision mana pool and regen',
cat: 'seer',
max: 10,
base: 250,
studyTime: 15,
},
},
},
// ═══════════════════════════════════════════════════════════════════════════
// WARDEN - Back
// Protection and defense. Damage reduction and shields.
// ═══════════════════════════════════════════════════════════════════════════
warden: {
id: 'warden',
name: 'Warden',
slot: 'back',
description: 'Shield yourself with protective wards and barriers.',
capability: 'Generate protective shields. -10% damage taken.',
primaryManaType: 'barrier',
rawManaRegen: 0.25,
autoConvertRate: 0.12,
icon: 'Shield',
color: '#10B981', // Green
skills: {
warding: {
name: 'Warding',
desc: 'Generate protective shields',
cat: 'warden',
max: 10,
base: 100,
studyTime: 8,
},
fortitude: {
name: 'Fortitude',
desc: 'Reduce damage taken',
cat: 'warden',
max: 10,
base: 150,
studyTime: 10,
},
reflection: {
name: 'Reflection',
desc: 'Chance to reflect damage to attacker',
cat: 'warden',
max: 5,
base: 300,
studyTime: 15,
req: { warding: 5 },
},
barrierMastery: {
name: 'Barrier Mastery',
desc: 'Increased barrier mana pool and regen',
cat: 'warden',
max: 10,
base: 250,
studyTime: 15,
},
},
},
// ═══════════════════════════════════════════════════════════════════════════
// INVOKER - Chest/Heart
// Pact with guardians. No primary mana - uses guardian elemental types.
// ═══════════════════════════════════════════════════════════════════════════
invoker: {
id: 'invoker',
name: 'Invoker',
slot: 'chest',
description: 'Form pacts with spire guardians and channel their elemental power.',
capability: 'Pact with guardians. Gain mana types from pacted guardians.',
primaryManaType: null, // Uses guardian types instead
rawManaRegen: 0.4,
autoConvertRate: 0, // No auto-convert; mana comes from guardian pacts
icon: 'Heart',
color: '#EF4444', // Red
skills: {
pactMaking: {
name: 'Pact Making',
desc: 'Form stronger pacts with guardians',
cat: 'invoker',
max: 10,
base: 100,
studyTime: 8,
},
guardianChannel: {
name: 'Guardian Channeling',
desc: 'Channel guardian powers more effectively',
cat: 'invoker',
max: 10,
base: 150,
studyTime: 10,
},
elementalBurst: {
name: 'Elemental Burst',
desc: 'Unleash stored guardian energy',
cat: 'invoker',
max: 5,
base: 300,
studyTime: 15,
req: { pactMaking: 5, guardianChannel: 3 },
},
soulResonance: {
name: 'Soul Resonance',
desc: 'Deep bond with pacted guardians',
cat: 'invoker',
max: 5,
base: 400,
studyTime: 20,
req: { pactMaking: 8 },
},
},
},
// ═══════════════════════════════════════════════════════════════════════════
// STRIDER - Left Leg
// Movement and swiftness. Attack speed and mobility.
// ═══════════════════════════════════════════════════════════════════════════
strider: {
id: 'strider',
name: 'Strider',
slot: 'leftLeg',
description: 'Move with supernatural speed and grace.',
capability: 'Enhanced mobility. +15% attack speed.',
primaryManaType: 'flow',
rawManaRegen: 0.3,
autoConvertRate: 0.15,
icon: 'Zap',
color: '#06B6D4', // Cyan
skills: {
swiftness: {
name: 'Swiftness',
desc: 'Increased attack and movement speed',
cat: 'strider',
max: 10,
base: 100,
studyTime: 8,
},
evasive: {
name: 'Evasive',
desc: 'Chance to avoid damage',
cat: 'strider',
max: 5,
base: 200,
studyTime: 12,
},
momentum: {
name: 'Momentum',
desc: 'Build speed over consecutive attacks',
cat: 'strider',
max: 5,
base: 250,
studyTime: 15,
req: { swiftness: 5 },
},
flowMastery: {
name: 'Flow Mastery',
desc: 'Increased flow mana pool and regen',
cat: 'strider',
max: 10,
base: 250,
studyTime: 15,
},
},
},
// ═══════════════════════════════════════════════════════════════════════════
// ANCHOR - Right Leg
// Stability and endurance. Max mana and knockback resistance.
// ═══════════════════════════════════════════════════════════════════════════
anchor: {
id: 'anchor',
name: 'Anchor',
slot: 'rightLeg',
description: 'Stand firm against any force. Your foundation is unshakeable.',
capability: 'Increased stability. +100 max mana.',
primaryManaType: 'stability',
rawManaRegen: 0.35,
autoConvertRate: 0.18,
icon: 'Mountain',
color: '#78716C', // Stone gray
skills: {
grounding: {
name: 'Grounding',
desc: 'Increased max mana and stability',
cat: 'anchor',
max: 10,
base: 100,
studyTime: 8,
},
endurance: {
name: 'Endurance',
desc: 'Reduced mana costs when below 50% mana',
cat: 'anchor',
max: 5,
base: 200,
studyTime: 12,
},
ironWill: {
name: 'Iron Will',
desc: 'Prevent mana drain effects',
cat: 'anchor',
max: 5,
base: 250,
studyTime: 15,
req: { grounding: 5 },
},
stabilityMastery: {
name: 'Stability Mastery',
desc: 'Increased stability mana pool and regen',
cat: 'anchor',
max: 10,
base: 250,
studyTime: 15,
},
},
},
};
// ─── Helper Functions ─────────────────────────────────────────────────────────
/**
* Get the attunement for a specific body slot
*/
export function getAttunementForSlot(slot: AttunementSlot): AttunementDef | undefined {
return Object.values(ATTUNEMENTS).find(a => a.slot === slot);
}
/**
* Get the starting attunement (Enchanter - right hand)
*/
export function getStartingAttunement(): AttunementDef {
return ATTUNEMENTS.enchanter;
}
/**
* Check if an attunement is unlocked for the player
*/
export function isAttunementUnlocked(
attunementStates: Record<AttunementType, AttunementState>,
attunementType: AttunementType
): boolean {
return attunementStates[attunementType]?.unlocked ?? false;
}
/**
* Get total raw mana regen from all unlocked attunements
*/
export function getTotalAttunementRegen(
attunementStates: Record<AttunementType, AttunementState>
): number {
let total = 0;
for (const [type, state] of Object.entries(attunementStates)) {
if (state.unlocked) {
const def = ATTUNEMENTS[type as AttunementType];
if (def) {
total += def.rawManaRegen * (1 + state.level * 0.1); // +10% per level
}
}
}
return total;
}
/**
* Get mana type display name
*/
export function getManaTypeName(type: ManaType): string {
const names: Record<ManaType, string> = {
raw: 'Raw Mana',
transference: 'Transference',
form: 'Form',
vision: 'Vision',
barrier: 'Barrier',
flow: 'Flow',
stability: 'Stability',
fire: 'Fire',
water: 'Water',
earth: 'Earth',
air: 'Air',
light: 'Light',
dark: 'Dark',
life: 'Life',
death: 'Death',
};
return names[type] || type;
}
/**
* Get mana type color
*/
export function getManaTypeColor(type: ManaType): string {
const colors: Record<ManaType, string> = {
raw: '#A78BFA', // Light purple
transference: '#8B5CF6', // Purple
form: '#3B82F6', // Blue
vision: '#F59E0B', // Amber
barrier: '#10B981', // Green
flow: '#06B6D4', // Cyan
stability: '#78716C', // Stone
fire: '#EF4444', // Red
water: '#3B82F6', // Blue
earth: '#A16207', // Brown
air: '#94A3B8', // Slate
light: '#FCD34D', // Yellow
dark: '#6B7280', // Gray
life: '#22C55E', // Green
death: '#7C3AED', // Violet
};
return colors[type] || '#A78BFA';
}

492
src/lib/game/computed-stats.ts Executable file
View File

@@ -0,0 +1,492 @@
// ─── Computed Stats and Utility Functions ───────────────────────────────────────
// This module contains all computed stat functions and utility helpers
// extracted from the main store for better organization
import type { GameState, SpellCost, EquipmentInstance } from './types';
import {
GUARDIANS,
SPELLS_DEF,
FLOOR_ELEM_CYCLE,
HOURS_PER_TICK,
MAX_DAY,
INCURSION_START_DAY,
ELEMENT_OPPOSITES,
ELEMENTS,
TICK_MS,
} from './constants';
import type { ComputedEffects } from './upgrade-effects';
import { getUnifiedEffects, type UnifiedEffects } from './effects';
import { EQUIPMENT_TYPES } from './data/equipment';
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
// ─── Default Effects Constant ───────────────────────────────────────────────────
// Default empty effects for when effects aren't provided
export const DEFAULT_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(),
activeUpgrades: [],
};
// ─── Number Formatting Functions ────────────────────────────────────────────────
export function fmt(n: number): string {
if (!isFinite(n) || isNaN(n)) return '0';
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
return Math.floor(n).toString();
}
export function fmtDec(n: number, d: number = 1): string {
return isFinite(n) ? n.toFixed(d) : '0';
}
// ─── Floor Functions ────────────────────────────────────────────────────────────
export function getFloorMaxHP(floor: number): number {
if (GUARDIANS[floor]) return GUARDIANS[floor].hp;
// Improved scaling: slower early game, faster late game
const baseHP = 100;
const floorScaling = floor * 50;
const exponentialScaling = Math.pow(floor, 1.7);
return Math.floor(baseHP + floorScaling + exponentialScaling);
}
export function getFloorElement(floor: number): string {
return FLOOR_ELEM_CYCLE[(floor - 1) % 8];
}
// ─── Equipment Spell Helper ─────────────────────────────────────────────────────
// Get all spells from equipped caster weapons (staves, wands, etc.)
// Returns array of { spellId, equipmentInstanceId }
export function getActiveEquipmentSpells(
equippedInstances: Record<string, string | null>,
equipmentInstances: Record<string, EquipmentInstance>
): Array<{ spellId: string; equipmentId: string }> {
const spells: Array<{ spellId: string; equipmentId: string }> = [];
// Check main hand and off hand for caster equipment
const weaponSlots = ['mainHand', 'offHand'] as const;
for (const slot of weaponSlots) {
const instanceId = equippedInstances[slot];
if (!instanceId) continue;
const instance = equipmentInstances[instanceId];
if (!instance) continue;
// Check if this is a caster-type equipment
const equipType = EQUIPMENT_TYPES[instance.typeId];
if (!equipType || equipType.category !== 'caster') continue;
// Get spells from enchantments
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
spells.push({
spellId: effectDef.effect.spellId,
equipmentId: instanceId,
});
}
}
}
return spells;
}
// ─── Skill Level Helper ─────────────────────────────────────────────────────────
// Helper to get effective skill level accounting for tiers
export function getEffectiveSkillLevel(
skills: Record<string, number>,
baseSkillId: string,
skillTiers: Record<string, number> = {}
): { level: number; tier: number; tierMultiplier: number } {
// Find the highest tier the player has for this base skill
const currentTier = skillTiers[baseSkillId] || 1;
// Look for the tiered skill ID (e.g., manaFlow_t2)
const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
// Tier multiplier: each tier is 10x more powerful
const tierMultiplier = Math.pow(10, currentTier - 1);
return { level, tier: currentTier, tierMultiplier };
}
// ─── Computed Stat Functions ────────────────────────────────────────────────────
export function computeMaxMana(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
effects?: ComputedEffects | UnifiedEffects
): number {
const pu = state.prestigeUpgrades;
const base =
100 +
(state.skills.manaWell || 0) * 100 +
(pu.manaWell || 0) * 500;
// If effects not provided, compute unified effects (includes equipment)
if (!effects && state.equipmentInstances && state.equippedInstances) {
effects = getUnifiedEffects(state as any);
}
// Apply effects if available (now includes equipment bonuses)
if (effects) {
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
}
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;
}
export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
effects?: ComputedEffects | UnifiedEffects
): number {
const pu = state.prestigeUpgrades;
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
const base =
2 +
(state.skills.manaFlow || 0) * 1 +
(state.skills.manaSpring || 0) * 2 +
(pu.manaFlow || 0) * 0.5;
let regen = base * temporalBonus;
// If effects not provided, compute unified effects (includes equipment)
if (!effects && state.equipmentInstances && state.equippedInstances) {
effects = getUnifiedEffects(state as any);
}
// Apply effects if available (now includes equipment bonuses)
if (effects) {
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
}
return regen;
}
/**
* Compute regen with dynamic special effects (needs current mana, max mana, incursion)
*/
export function computeEffectiveRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
effects?: ComputedEffects | UnifiedEffects
): number {
// Base regen from existing function
let regen = computeRegen(state, effects);
const maxMana = computeMaxMana(state, effects);
const currentMana = state.rawMana;
const incursionStrength = state.incursionStrength || 0;
// Apply incursion penalty
regen *= (1 - incursionStrength);
return regen;
}
export function computeClickMana(
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
effects?: ComputedEffects | UnifiedEffects
): number {
const base =
1 +
(state.skills.manaTap || 0) * 1 +
(state.skills.manaSurge || 0) * 3;
// If effects not provided, compute unified effects (includes equipment)
if (!effects && state.equipmentInstances && state.equippedInstances) {
effects = getUnifiedEffects(state as any);
}
// Apply effects if available (now includes equipment bonuses)
if (effects) {
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
}
return base;
}
// ─── Damage Calculation Helpers ─────────────────────────────────────────────────
// Elemental damage bonus: +50% if spell element opposes floor element (super effective)
// -25% if spell element matches its own opposite (weak)
export function getElementalBonus(spellElem: string, floorElem: string): number {
if (spellElem === 'raw') return 1.0; // Raw mana has no elemental bonus
if (spellElem === floorElem) return 1.25; // Same element: +25% damage
// Check for super effective first: spell is the opposite of floor
// e.g., casting water (opposite of fire) at fire floor = super effective
if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective: +50% damage
// Check for weak: spell's opposite matches floor
// e.g., casting fire (whose opposite is water) at water floor = weak
if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak: -25% damage
return 1.0; // Neutral
}
export function calcDamage(
state: Pick<GameState, 'skills' | 'signedPacts'>,
spellId: string,
floorElem?: string
): number {
const sp = SPELLS_DEF[spellId];
if (!sp) return 5;
const skills = state.skills;
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5;
const pct = 1 + (skills.arcaneFury || 0) * 0.1;
// Elemental mastery bonus
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
// Guardian bane bonus
const guardianBonus = floorElem && GUARDIANS[Object.values(GUARDIANS).find(g => g.element === floorElem)?.hp ? 0 : 0]
? 1 + (skills.guardianBane || 0) * 0.2
: 1;
const critChance = (skills.precision || 0) * 0.05;
const pactMult = state.signedPacts.reduce(
(m, f) => m * (GUARDIANS[f]?.pact || 1),
1
);
let damage = baseDmg * pct * pactMult * elemMasteryBonus;
// Apply elemental bonus if floor element provided
if (floorElem) {
damage *= getElementalBonus(sp.elem, floorElem);
}
// Apply crit
if (Math.random() < critChance) {
damage *= 1.5;
}
return damage;
}
// ─── Insight Calculation ────────────────────────────────────────────────────────
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
const pu = state.prestigeUpgrades;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1;
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus;
return Math.floor(
(state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult
);
}
// ─── Meditation Bonus ───────────────────────────────────────────────────────────
// Meditation bonus now affects regen rate directly
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number {
const hasMeditation = skills.meditation === 1;
const hasDeepTrance = skills.deepTrance === 1;
const hasVoidMeditation = skills.voidMeditation === 1;
const hours = meditateTicks * HOURS_PER_TICK;
// Base meditation: ramps up over 4 hours to 1.5x
let bonus = 1 + Math.min(hours / 4, 0.5);
// With Meditation Focus: up to 2.5x after 4 hours
if (hasMeditation && hours >= 4) {
bonus = 2.5;
}
// With Deep Trance: up to 3.0x after 6 hours
if (hasDeepTrance && hours >= 6) {
bonus = 3.0;
}
// With Void Meditation: up to 5.0x after 8 hours
if (hasVoidMeditation && hours >= 8) {
bonus = 5.0;
}
// Apply meditation efficiency from upgrades (Deep Wellspring, etc.)
bonus *= meditationEfficiency;
return bonus;
}
// ─── Incursion Strength ─────────────────────────────────────────────────────────
export function getIncursionStrength(day: number, hour: number): number {
if (day < INCURSION_START_DAY) return 0;
const totalHours = (day - INCURSION_START_DAY) * 24 + hour;
const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24;
return Math.min(0.95, (totalHours / maxHours) * 0.95);
}
// ─── Spell Cost Helpers ─────────────────────────────────────────────────────────
// Check if player can afford spell cost
export function canAffordSpellCost(
cost: SpellCost,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>
): boolean {
if (cost.type === 'raw') {
return rawMana >= cost.amount;
} else {
const elem = elements[cost.element || ''];
return elem && elem.unlocked && elem.current >= cost.amount;
}
}
// Deduct spell cost from appropriate mana pool
export function deductSpellCost(
cost: SpellCost,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>
): { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> } {
const newElements = { ...elements };
if (cost.type === 'raw') {
// Clamp to 0 to prevent negative mana
return { rawMana: Math.max(0, rawMana - cost.amount), elements: newElements };
} else if (cost.element && newElements[cost.element]) {
newElements[cost.element] = {
...newElements[cost.element],
current: Math.max(0, newElements[cost.element].current - cost.amount)
};
return { rawMana, elements: newElements };
}
return { rawMana, elements: newElements };
}
// ─── Damage Breakdown Helper ───────────────────────────────────────────────────
export interface DamageBreakdown {
base: number;
combatTrainBonus: number;
arcaneFuryMult: number;
elemMasteryMult: number;
guardianBaneMult: number;
pactMult: number;
precisionChance: number;
elemBonus: number;
elemBonusText: string;
total: number;
}
export function getDamageBreakdown(
state: Pick<GameState, 'skills' | 'signedPacts'>,
activeSpellId: string,
floorElem: string,
isGuardianFloor: boolean
): DamageBreakdown | null {
const spell = SPELLS_DEF[activeSpellId];
if (!spell) return null;
const baseDmg = spell.dmg;
const combatTrainBonus = (state.skills.combatTrain || 0) * 5;
const arcaneFuryMult = 1 + (state.skills.arcaneFury || 0) * 0.1;
const elemMasteryMult = 1 + (state.skills.elementalMastery || 0) * 0.15;
const guardianBaneMult = isGuardianFloor ? (1 + (state.skills.guardianBane || 0) * 0.2) : 1;
const pactMult = state.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1);
const precisionChance = (state.skills.precision || 0) * 0.05;
// Elemental bonus
let elemBonus = 1.0;
let elemBonusText = '';
if (spell.elem !== 'raw' && floorElem) {
if (spell.elem === floorElem) {
elemBonus = 1.25;
elemBonusText = '+25% same element';
} else if (ELEMENTS[spell.elem] && ELEMENT_OPPOSITES[floorElem] === spell.elem) {
elemBonus = 1.5;
elemBonusText = '+50% super effective';
}
}
return {
base: baseDmg,
combatTrainBonus,
arcaneFuryMult,
elemMasteryMult,
guardianBaneMult,
pactMult,
precisionChance,
elemBonus,
elemBonusText,
total: calcDamage(state, activeSpellId, floorElem)
};
}
// ─── Total DPS Calculation ─────────────────────────────────────────────────────
export function getTotalDPS(
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances'>,
upgradeEffects: { attackSpeedMultiplier: number },
floorElem: string
): number {
const quickCastBonus = 1 + (state.skills.quickCast || 0) * 0.05;
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
const castsPerSecondMult = HOURS_PER_TICK / (TICK_MS / 1000);
const activeEquipmentSpells = getActiveEquipmentSpells(
state.equippedInstances,
state.equipmentInstances
);
let totalDPS = 0;
for (const { spellId } of activeEquipmentSpells) {
const spell = SPELLS_DEF[spellId];
if (!spell) continue;
const spellCastSpeed = spell.castSpeed || 1;
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
const damagePerCast = calcDamage(state, spellId, floorElem);
const castsPerSecond = totalCastSpeed * castsPerSecondMult;
totalDPS += damagePerCast * castsPerSecond;
}
return totalDPS;
}

876
src/lib/game/constants.ts Executable file
View File

@@ -0,0 +1,876 @@
// ─── Game Constants ───────────────────────────────────────────────────────────
import type { ElementDef, GuardianDef, SpellDef, SkillDef, PrestigeDef, SpellCost } from './types';
// Time constants
export const TICK_MS = 200;
export const HOURS_PER_TICK = 0.04; // 200ms / 5000ms per in-game hour
export const MAX_DAY = 30;
export const INCURSION_START_DAY = 20;
// Mana constants
export const MANA_PER_ELEMENT = 100;
// Helper function for creating raw mana cost
export function rawCost(amount: number): SpellCost {
return { type: 'raw', amount };
}
// Helper function for creating elemental mana cost
export function elemCost(element: string, amount: number): SpellCost {
return { type: 'element', element, amount };
}
// ─── Element Definitions ─────────────────────────────────────────────────────
export const ELEMENTS: Record<string, ElementDef> = {
// Base Elements
fire: { name: "Fire", sym: "🔥", color: "#FF6B35", glow: "#FF6B3540", cat: "base" },
water: { name: "Water", sym: "💧", color: "#4ECDC4", glow: "#4ECDC440", cat: "base" },
air: { name: "Air", sym: "🌬️", color: "#00D4FF", glow: "#00D4FF40", cat: "base" },
earth: { name: "Earth", sym: "⛰️", color: "#F4A261", glow: "#F4A26140", cat: "base" },
light: { name: "Light", sym: "☀️", color: "#FFD700", glow: "#FFD70040", cat: "base" },
dark: { name: "Dark", sym: "🌑", color: "#9B59B6", glow: "#9B59B640", cat: "base" },
life: { name: "Life", sym: "🌿", color: "#2ECC71", glow: "#2ECC7140", cat: "base" },
death: { name: "Death", sym: "💀", color: "#778CA3", glow: "#778CA340", cat: "base" },
// Utility Elements
mental: { name: "Mental", sym: "🧠", color: "#E91E8C", glow: "#E91E8C40", cat: "utility" },
transference: { name: "Transference", sym: "🔗", color: "#1ABC9C", glow: "#1ABC9C40", cat: "utility" },
force: { name: "Force", sym: "💨", color: "#E74C3C", glow: "#E74C3C40", cat: "utility" },
// Composite Elements
blood: { name: "Blood", sym: "🩸", color: "#C0392B", glow: "#C0392B40", cat: "composite", recipe: ["life", "water"] },
metal: { name: "Metal", sym: "⚙️", color: "#BDC3C7", glow: "#BDC3C740", cat: "composite", recipe: ["fire", "earth"] },
wood: { name: "Wood", sym: "🪵", color: "#8B5E3C", glow: "#8B5E3C40", cat: "composite", recipe: ["life", "earth"] },
sand: { name: "Sand", sym: "⏳", color: "#D4AC0D", glow: "#D4AC0D40", cat: "composite", recipe: ["earth", "water"] },
// Exotic Elements
crystal: { name: "Crystal", sym: "💎", color: "#85C1E9", glow: "#85C1E940", cat: "exotic", recipe: ["sand", "sand", "mental"] },
stellar: { name: "Stellar", sym: "⭐", color: "#F0E68C", glow: "#F0E68C40", cat: "exotic", recipe: ["fire", "fire", "light"] },
void: { name: "Void", sym: "🕳️", color: "#4A235A", glow: "#4A235A40", cat: "exotic", recipe: ["dark", "dark", "death"] },
};
export const FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "life", "death"];
// ─── Guardians ────────────────────────────────────────────────────────────────
export const GUARDIANS: Record<number, GuardianDef> = {
10: {
name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35",
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Fire damage' },
{ type: 'maxMana', value: 50, desc: '+50 max mana' },
],
pactCost: 500,
pactTime: 2,
uniquePerk: "Fire spells cast 10% faster"
},
20: {
name: "Aqua Regia", element: "water", hp: 15000, pact: 1.75, color: "#4ECDC4",
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Water damage' },
{ type: 'manaRegen', value: 0.5, desc: '+0.5 mana regen' },
],
pactCost: 1000,
pactTime: 4,
uniquePerk: "Water spells have 10% lifesteal"
},
30: {
name: "Ventus Rex", element: "air", hp: 30000, pact: 2.0, color: "#00D4FF",
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Air damage' },
{ type: 'castingSpeed', value: 5, desc: '+5% cast speed' },
],
pactCost: 2000,
pactTime: 6,
uniquePerk: "Air spells have 15% crit chance"
},
40: {
name: "Terra Firma", element: "earth", hp: 50000, pact: 2.25, color: "#F4A261",
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Earth damage' },
{ type: 'maxMana', value: 100, desc: '+100 max mana' },
],
pactCost: 4000,
pactTime: 8,
uniquePerk: "Earth spells deal +25% damage to guardians"
},
50: {
name: "Lux Aeterna", element: "light", hp: 80000, pact: 2.5, color: "#FFD700",
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Light damage' },
{ type: 'insightGain', value: 10, desc: '+10% insight gain' },
],
pactCost: 8000,
pactTime: 10,
uniquePerk: "Light spells reveal enemy weaknesses (+20% damage)"
},
60: {
name: "Umbra Mortis", element: "dark", hp: 120000, pact: 2.75, color: "#9B59B6",
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Dark damage' },
{ type: 'critDamage', value: 15, desc: '+15% crit damage' },
],
pactCost: 15000,
pactTime: 12,
uniquePerk: "Dark spells have 20% lifesteal"
},
70: {
name: "Vita Sempiterna", element: "life", hp: 180000, pact: 3.0, color: "#2ECC71",
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Life damage' },
{ type: 'manaRegen', value: 1, desc: '+1 mana regen' },
],
pactCost: 25000,
pactTime: 14,
uniquePerk: "Life spells heal for 30% of damage dealt"
},
80: {
name: "Mors Ultima", element: "death", hp: 250000, pact: 3.25, color: "#778CA3",
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Death damage' },
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' },
],
pactCost: 40000,
pactTime: 16,
uniquePerk: "Death spells execute enemies below 20% HP"
},
90: {
name: "Primordialis", element: "void", hp: 400000, pact: 4.0, color: "#4A235A",
boons: [
{ type: 'elementalDamage', value: 15, desc: '+15% Void damage' },
{ type: 'maxMana', value: 200, desc: '+200 max mana' },
{ type: 'manaRegen', value: 1, desc: '+1 mana regen' },
],
pactCost: 75000,
pactTime: 20,
uniquePerk: "Void spells ignore 30% of enemy resistance"
},
100: {
name: "The Awakened One", element: "stellar", hp: 1000000, pact: 5.0, color: "#F0E68C",
boons: [
{ type: 'elementalDamage', value: 20, desc: '+20% Stellar damage' },
{ type: 'maxMana', value: 500, desc: '+500 max mana' },
{ type: 'manaRegen', value: 2, desc: '+2 mana regen' },
{ type: 'insightGain', value: 25, desc: '+25% insight gain' },
],
pactCost: 150000,
pactTime: 24,
uniquePerk: "All spells deal +50% damage and cast 25% faster"
},
};
// ─── Spells ───────────────────────────────────────────────────────────────────
export const SPELLS_DEF: Record<string, SpellDef> = {
// Tier 0 - Basic Raw Mana Spells (fast, costs raw mana)
manaBolt: {
name: "Mana Bolt",
elem: "raw",
dmg: 5,
cost: rawCost(3),
tier: 0,
castSpeed: 3,
unlock: 0,
studyTime: 0,
desc: "A weak bolt of pure mana. Costs raw mana instead of elemental."
},
manaStrike: {
name: "Mana Strike",
elem: "raw",
dmg: 8,
cost: rawCost(5),
tier: 0,
castSpeed: 2.5,
unlock: 50,
studyTime: 1,
desc: "A concentrated strike of raw mana. Slightly stronger than Mana Bolt."
},
// Tier 1 - Basic Elemental Spells (2-4 hours study)
fireball: {
name: "Fireball",
elem: "fire",
dmg: 15,
cost: elemCost("fire", 2),
tier: 1,
castSpeed: 2,
unlock: 100,
studyTime: 2,
desc: "Hurl a ball of fire at your enemy."
},
emberShot: {
name: "Ember Shot",
elem: "fire",
dmg: 10,
cost: elemCost("fire", 1),
tier: 1,
castSpeed: 3,
unlock: 75,
studyTime: 1,
desc: "A quick shot of embers. Efficient fire damage."
},
waterJet: {
name: "Water Jet",
elem: "water",
dmg: 12,
cost: elemCost("water", 2),
tier: 1,
castSpeed: 2,
unlock: 100,
studyTime: 2,
desc: "A high-pressure jet of water."
},
iceShard: {
name: "Ice Shard",
elem: "water",
dmg: 14,
cost: elemCost("water", 2),
tier: 1,
castSpeed: 2,
unlock: 120,
studyTime: 2,
desc: "Launch a sharp shard of ice."
},
gust: {
name: "Gust",
elem: "air",
dmg: 10,
cost: elemCost("air", 2),
tier: 1,
castSpeed: 3,
unlock: 100,
studyTime: 2,
desc: "A powerful gust of wind."
},
windSlash: {
name: "Wind Slash",
elem: "air",
dmg: 12,
cost: elemCost("air", 2),
tier: 1,
castSpeed: 2.5,
unlock: 110,
studyTime: 2,
desc: "A cutting blade of wind."
},
stoneBullet: {
name: "Stone Bullet",
elem: "earth",
dmg: 16,
cost: elemCost("earth", 2),
tier: 1,
castSpeed: 2,
unlock: 150,
studyTime: 3,
desc: "Launch a bullet of solid stone."
},
rockSpike: {
name: "Rock Spike",
elem: "earth",
dmg: 18,
cost: elemCost("earth", 3),
tier: 1,
castSpeed: 1.5,
unlock: 180,
studyTime: 3,
desc: "Summon a spike of rock from below."
},
lightLance: {
name: "Light Lance",
elem: "light",
dmg: 18,
cost: elemCost("light", 2),
tier: 1,
castSpeed: 2,
unlock: 200,
studyTime: 4,
desc: "A piercing lance of pure light."
},
radiance: {
name: "Radiance",
elem: "light",
dmg: 14,
cost: elemCost("light", 2),
tier: 1,
castSpeed: 2.5,
unlock: 180,
studyTime: 3,
desc: "Burst of radiant energy."
},
shadowBolt: {
name: "Shadow Bolt",
elem: "dark",
dmg: 16,
cost: elemCost("dark", 2),
tier: 1,
castSpeed: 2,
unlock: 200,
studyTime: 4,
desc: "A bolt of shadowy energy."
},
darkPulse: {
name: "Dark Pulse",
elem: "dark",
dmg: 12,
cost: elemCost("dark", 1),
tier: 1,
castSpeed: 3,
unlock: 150,
studyTime: 2,
desc: "A quick pulse of darkness."
},
drain: {
name: "Drain",
elem: "death",
dmg: 10,
cost: elemCost("death", 2),
tier: 1,
castSpeed: 2,
unlock: 150,
studyTime: 3,
desc: "Drain life force from your enemy.",
effects: [{ type: 'lifesteal', value: 0.2 }]
},
rotTouch: {
name: "Rot Touch",
elem: "death",
dmg: 14,
cost: elemCost("death", 2),
tier: 1,
castSpeed: 2,
unlock: 170,
studyTime: 3,
desc: "Touch of decay and rot."
},
lifeTap: {
name: "Life Tap",
elem: "life",
dmg: 8,
cost: elemCost("life", 1),
tier: 1,
castSpeed: 3,
unlock: 100,
studyTime: 2,
desc: "Tap into life energy for a weak attack.",
effects: [{ type: 'lifesteal', value: 0.3 }]
},
thornWhip: {
name: "Thorn Whip",
elem: "life",
dmg: 12,
cost: elemCost("life", 2),
tier: 1,
castSpeed: 2.5,
unlock: 130,
studyTime: 2,
desc: "A whip of living thorns."
},
// Tier 2 - Advanced Spells (8-12 hours study)
inferno: {
name: "Inferno",
elem: "fire",
dmg: 60,
cost: elemCost("fire", 8),
tier: 2,
castSpeed: 1,
unlock: 1000,
studyTime: 8,
desc: "Engulf your enemy in flames."
},
flameWave: {
name: "Flame Wave",
elem: "fire",
dmg: 45,
cost: elemCost("fire", 6),
tier: 2,
castSpeed: 1.5,
unlock: 800,
studyTime: 6,
desc: "A wave of fire sweeps across the battlefield."
},
tidalWave: {
name: "Tidal Wave",
elem: "water",
dmg: 55,
cost: elemCost("water", 8),
tier: 2,
castSpeed: 1,
unlock: 1000,
studyTime: 8,
desc: "A massive wave crashes down."
},
iceStorm: {
name: "Ice Storm",
elem: "water",
dmg: 50,
cost: elemCost("water", 7),
tier: 2,
castSpeed: 1.2,
unlock: 900,
studyTime: 7,
desc: "A storm of ice shards."
},
earthquake: {
name: "Earthquake",
elem: "earth",
dmg: 70,
cost: elemCost("earth", 10),
tier: 2,
castSpeed: 0.8,
unlock: 1200,
studyTime: 10,
desc: "Shake the very foundation."
},
stoneBarrage: {
name: "Stone Barrage",
elem: "earth",
dmg: 55,
cost: elemCost("earth", 7),
tier: 2,
castSpeed: 1.2,
unlock: 1000,
studyTime: 8,
desc: "Multiple stone projectiles."
},
hurricane: {
name: "Hurricane",
elem: "air",
dmg: 50,
cost: elemCost("air", 8),
tier: 2,
castSpeed: 1,
unlock: 1000,
studyTime: 8,
desc: "A devastating hurricane."
},
windBlade: {
name: "Wind Blade",
elem: "air",
dmg: 40,
cost: elemCost("air", 5),
tier: 2,
castSpeed: 1.8,
unlock: 700,
studyTime: 6,
desc: "A blade of cutting wind."
},
solarFlare: {
name: "Solar Flare",
elem: "light",
dmg: 65,
cost: elemCost("light", 9),
tier: 2,
castSpeed: 0.9,
unlock: 1100,
studyTime: 9,
desc: "A blinding flare of solar energy."
},
divineSmite: {
name: "Divine Smite",
elem: "light",
dmg: 55,
cost: elemCost("light", 7),
tier: 2,
castSpeed: 1.2,
unlock: 900,
studyTime: 7,
desc: "A smite of divine power."
},
voidRift: {
name: "Void Rift",
elem: "dark",
dmg: 55,
cost: elemCost("dark", 8),
tier: 2,
castSpeed: 1,
unlock: 1000,
studyTime: 8,
desc: "Open a rift to the void."
},
shadowStorm: {
name: "Shadow Storm",
elem: "dark",
dmg: 48,
cost: elemCost("dark", 6),
tier: 2,
castSpeed: 1.3,
unlock: 800,
studyTime: 6,
desc: "A storm of shadows."
},
soulRend: {
name: "Soul Rend",
elem: "death",
dmg: 50,
cost: elemCost("death", 7),
tier: 2,
castSpeed: 1.1,
unlock: 1100,
studyTime: 9,
desc: "Tear at the enemy's soul.",
effects: [{ type: 'lifesteal', value: 0.25 }]
},
entangle: {
name: "Entangle",
elem: "life",
dmg: 35,
cost: elemCost("life", 5),
tier: 2,
castSpeed: 1.5,
unlock: 700,
studyTime: 6,
desc: "Vines entangle and crush."
},
// Tier 3 - Master Spells (20-30 hours study)
pyroclasm: {
name: "Pyroclasm",
elem: "fire",
dmg: 250,
cost: elemCost("fire", 25),
tier: 3,
castSpeed: 0.6,
unlock: 10000,
studyTime: 24,
desc: "An eruption of volcanic fury."
},
tsunami: {
name: "Tsunami",
elem: "water",
dmg: 220,
cost: elemCost("water", 22),
tier: 3,
castSpeed: 0.65,
unlock: 10000,
studyTime: 24,
desc: "A towering wall of water."
},
meteorStrike: {
name: "Meteor Strike",
elem: "earth",
dmg: 280,
cost: elemCost("earth", 28),
tier: 3,
castSpeed: 0.5,
unlock: 12000,
studyTime: 28,
desc: "Call down a meteor from the heavens."
},
cosmicStorm: {
name: "Cosmic Storm",
elem: "air",
dmg: 200,
cost: elemCost("air", 20),
tier: 3,
castSpeed: 0.7,
unlock: 10000,
studyTime: 24,
desc: "A storm of cosmic proportions."
},
heavenLight: {
name: "Heaven's Light",
elem: "light",
dmg: 240,
cost: elemCost("light", 24),
tier: 3,
castSpeed: 0.6,
unlock: 11000,
studyTime: 26,
desc: "The light of heaven itself."
},
oblivion: {
name: "Oblivion",
elem: "dark",
dmg: 230,
cost: elemCost("dark", 23),
tier: 3,
castSpeed: 0.6,
unlock: 10500,
studyTime: 25,
desc: "Consign to oblivion."
},
deathMark: {
name: "Death Mark",
elem: "death",
dmg: 200,
cost: elemCost("death", 20),
tier: 3,
castSpeed: 0.7,
unlock: 10000,
studyTime: 24,
desc: "Mark for death.",
effects: [{ type: 'lifesteal', value: 0.35 }]
},
worldTree: {
name: "World Tree",
elem: "life",
dmg: 180,
cost: elemCost("life", 18),
tier: 3,
castSpeed: 0.75,
unlock: 9000,
studyTime: 22,
desc: "Power of the world tree itself.",
effects: [{ type: 'lifesteal', value: 0.4 }]
},
// Tier 4 - Legendary Spells (40-60 hours study, require exotic elements)
stellarNova: {
name: "Stellar Nova",
elem: "stellar",
dmg: 500,
cost: elemCost("stellar", 15),
tier: 4,
castSpeed: 0.4,
unlock: 50000,
studyTime: 48,
desc: "A nova of stellar energy."
},
voidCollapse: {
name: "Void Collapse",
elem: "void",
dmg: 450,
cost: elemCost("void", 12),
tier: 4,
castSpeed: 0.45,
unlock: 40000,
studyTime: 42,
desc: "Collapse the void upon your enemy."
},
crystalShatter: {
name: "Crystal Shatter",
elem: "crystal",
dmg: 400,
cost: elemCost("crystal", 10),
tier: 4,
castSpeed: 0.5,
unlock: 35000,
studyTime: 36,
desc: "Shatter crystalline energy."
},
};
// ─── Skills ───────────────────────────────────────────────────────────────────
export const SKILLS_DEF: Record<string, SkillDef> = {
// Mana Skills (4-8 hours study)
manaWell: { name: "Mana Well", desc: "+100 max mana", cat: "mana", max: 10, base: 100, studyTime: 4 },
manaFlow: { name: "Mana Flow", desc: "+1 regen/hr", cat: "mana", max: 10, base: 150, studyTime: 5 },
elemAttune: { name: "Elem. Attunement", desc: "+50 elem mana cap", cat: "mana", max: 10, base: 200, studyTime: 4 },
manaOverflow: { name: "Mana Overflow", desc: "+25% mana from clicks", cat: "mana", max: 5, base: 400, req: { manaWell: 3 }, studyTime: 6 },
// Study Skills (3-6 hours study)
quickLearner: { name: "Quick Learner", desc: "+10% study speed", cat: "study", max: 10, base: 250, studyTime: 4 },
focusedMind: { name: "Focused Mind", desc: "-5% study mana cost", cat: "study", max: 10, base: 300, studyTime: 5 },
meditation: { name: "Meditation Focus", desc: "Up to 2.5x regen after 4hrs meditating", cat: "study", max: 1, base: 400, studyTime: 6 },
knowledgeRetention: { name: "Knowledge Retention", desc: "+20% study progress saved on cancel", cat: "study", max: 3, base: 350, studyTime: 5 },
// Enchanting Skills (4-8 hours study) - Core crafting system
enchanting: { name: "Enchanting", desc: "Unlocks enchantment design", cat: "enchant", max: 10, base: 200, studyTime: 5 },
efficientEnchant:{ name: "Efficient Enchant", desc: "-5% enchantment capacity cost", cat: "enchant", max: 5, base: 350, studyTime: 6, req: { enchanting: 3 } },
disenchanting: { name: "Disenchanting", desc: "Recover 20% mana from removed enchantments", cat: "enchant", max: 3, base: 400, studyTime: 6, req: { enchanting: 2 } },
enchantSpeed: { name: "Enchant Speed", desc: "-10% enchantment time", cat: "enchant", max: 5, base: 300, studyTime: 4, req: { enchanting: 2 } },
scrollCrafting: { name: "Scroll Crafting", desc: "Create scrolls to store enchantment designs", cat: "enchant", max: 3, base: 500, studyTime: 8, req: { enchanting: 5 } },
essenceRefining: { name: "Essence Refining", desc: "+10% enchantment effect power", cat: "enchant", max: 5, base: 450, studyTime: 7, req: { enchanting: 4 } },
// Crafting Skills (4-6 hours study)
effCrafting: { name: "Eff. Crafting", desc: "-10% craft time", cat: "craft", max: 5, base: 300, studyTime: 4 },
fieldRepair: { name: "Field Repair", desc: "+15% repair efficiency", cat: "craft", max: 5, base: 350, studyTime: 4 },
elemCrafting: { name: "Elem. Crafting", desc: "+25% craft output", cat: "craft", max: 3, base: 500, req: { effCrafting: 3 }, studyTime: 8 },
// Effect Research Skills (unlock enchantment effects for designing)
// Tier 1 - Basic Spell Effects
researchManaSpells: { name: "Mana Spell Research", desc: "Unlock Mana Strike spell enchantment", cat: "effectResearch", max: 1, base: 200, studyTime: 4, req: { enchanting: 1 } },
researchFireSpells: { name: "Fire Spell Research", desc: "Unlock Ember Shot, Fireball spell enchantments", cat: "effectResearch", max: 1, base: 300, studyTime: 6, req: { enchanting: 2 } },
researchWaterSpells: { name: "Water Spell Research", desc: "Unlock Water Jet, Ice Shard spell enchantments", cat: "effectResearch", max: 1, base: 300, studyTime: 6, req: { enchanting: 2 } },
researchAirSpells: { name: "Air Spell Research", desc: "Unlock Gust, Wind Slash spell enchantments", cat: "effectResearch", max: 1, base: 300, studyTime: 6, req: { enchanting: 2 } },
researchEarthSpells: { name: "Earth Spell Research", desc: "Unlock Stone Bullet, Rock Spike spell enchantments", cat: "effectResearch", max: 1, base: 350, studyTime: 6, req: { enchanting: 2 } },
researchLightSpells: { name: "Light Spell Research", desc: "Unlock Light Lance, Radiance spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 } },
researchDarkSpells: { name: "Dark Spell Research", desc: "Unlock Shadow Bolt, Dark Pulse spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 } },
researchLifeDeathSpells: { name: "Life & Death Research", desc: "Unlock Drain, Life Tap spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 } },
// Tier 2 - Advanced Spell Effects
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 } },
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 } },
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 } },
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 } },
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 } },
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 } },
// Tier 3 - Master Spell Effects
researchMasterFire: { name: "Master Fire Research", desc: "Unlock Pyroclasm spell enchantment", cat: "effectResearch", max: 1, base: 1200, studyTime: 24, req: { researchAdvancedFire: 1, enchanting: 7 } },
researchMasterWater: { name: "Master Water Research", desc: "Unlock Tsunami spell enchantment", cat: "effectResearch", max: 1, base: 1200, studyTime: 24, req: { researchAdvancedWater: 1, enchanting: 7 } },
researchMasterEarth: { name: "Master Earth Research", desc: "Unlock Meteor Strike spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedEarth: 1, enchanting: 8 } },
// 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 } },
researchCombatEffects: { name: "Combat Effect Research", desc: "Unlock Sharp Edge, Swift Casting effects", cat: "effectResearch", max: 1, base: 350, studyTime: 6, req: { researchDamageEffects: 1, enchanting: 3 } },
// Mana Effect Research
researchManaEffects: { name: "Mana Effect Research", desc: "Unlock Mana Reserve, Trickle, Mana Tap effects", cat: "effectResearch", max: 1, base: 200, studyTime: 4, req: { enchanting: 1 } },
researchAdvancedManaEffects: { name: "Advanced Mana Research", desc: "Unlock Mana Reservoir, Stream, River, Mana Surge effects", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { researchManaEffects: 1, enchanting: 3 } },
// Utility Effect Research
researchUtilityEffects: { name: "Utility Effect Research", desc: "Unlock Meditative Focus, Quick Study, Insightful effects", cat: "effectResearch", max: 1, base: 300, studyTime: 6, req: { enchanting: 2 } },
// Special Effect Research
researchSpecialEffects: { name: "Special Effect Research", desc: "Unlock Echo Chamber, Siphoning, Bane effects", cat: "effectResearch", max: 1, base: 500, studyTime: 10, req: { enchanting: 4 } },
researchOverpower: { name: "Overpower Research", desc: "Unlock Overpower effect", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchSpecialEffects: 1, enchanting: 5 } },
// Research Skills (longer study times: 12-72 hours)
manaTap: { name: "Mana Tap", desc: "+1 mana/click", cat: "research", max: 1, base: 300, studyTime: 12 },
manaSurge: { name: "Mana Surge", desc: "+3 mana/click", cat: "research", max: 1, base: 800, studyTime: 36, req: { manaTap: 1 } },
manaSpring: { name: "Mana Spring", desc: "+2 mana regen", cat: "research", max: 1, base: 600, studyTime: 24 },
deepTrance: { name: "Deep Trance", desc: "Extend meditation bonus to 6hrs for 3x", cat: "research", max: 1, base: 900, studyTime: 48, req: { meditation: 1 } },
voidMeditation:{ name: "Void Meditation", desc: "Extend meditation bonus to 8hrs for 5x", cat: "research", max: 1, base: 1500, studyTime: 72, req: { deepTrance: 1 } },
// Ascension Skills (very long study, powerful effects)
insightHarvest: { name: "Insight Harvest", desc: "+10% insight gain", cat: "ascension", max: 5, base: 1000, studyTime: 20 },
temporalMemory: { name: "Temporal Memory", desc: "Keep 1 spell learned across loops", cat: "ascension", max: 3, base: 2000, studyTime: 36 },
guardianBane: { name: "Guardian Bane", desc: "+20% dmg vs guardians", cat: "ascension", max: 3, base: 1500, studyTime: 30 },
};
// ─── Prestige Upgrades ────────────────────────────────────────────────────────
export const PRESTIGE_DEF: Record<string, PrestigeDef> = {
manaWell: { name: "Mana Well", desc: "+500 starting max mana", max: 5, cost: 500 },
manaFlow: { name: "Mana Flow", desc: "+0.5 regen/sec permanently", max: 10, cost: 750 },
deepMemory: { name: "Deep Memory", desc: "+1 memory slot", max: 5, cost: 1000 },
insightAmp: { name: "Insight Amp", desc: "+25% insight gain", max: 4, cost: 1500 },
spireKey: { name: "Spire Key", desc: "Start at floor +2", max: 5, cost: 4000 },
temporalEcho: { name: "Temporal Echo", desc: "+10% mana generation", max: 5, cost: 3000 },
steadyHand: { name: "Steady Hand", desc: "-15% durability loss", max: 5, cost: 1200 },
ancientKnowledge: { name: "Ancient Knowledge", desc: "Start with blueprint discovered", max: 5, cost: 2000 },
elementalAttune: { name: "Elemental Attunement", desc: "+25 elemental mana cap", max: 10, cost: 600 },
spellMemory: { name: "Spell Memory", desc: "Start with random spell learned", max: 3, cost: 2500 },
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 },
};
// ─── Skill Categories ─────────────────────────────────────────────────────────
// Skills are now organized by attunement - each attunement grants access to specific skill categories
export const SKILL_CATEGORIES = [
// Core categories (always available)
{ id: 'mana', name: 'Mana', icon: '💧', attunement: null },
{ id: 'study', name: 'Study', icon: '📚', attunement: null },
{ id: 'research', name: 'Research', icon: '🔮', attunement: null },
{ id: 'ascension', name: 'Ascension', icon: '⭐', attunement: null },
// Enchanter attunement (Right Hand)
{ id: 'enchant', name: 'Enchanting', icon: '✨', attunement: 'enchanter' },
{ id: 'effectResearch', name: 'Effect Research', icon: '🔬', attunement: 'enchanter' },
// Invoker attunement (Chest)
{ id: 'invocation', name: 'Invocation', icon: '💜', attunement: 'invoker' },
{ id: 'pact', name: 'Pact Mastery', icon: '🤝', attunement: 'invoker' },
// Fabricator attunement (Left Hand)
{ id: 'fabrication', name: 'Fabrication', icon: '⚒️', attunement: 'fabricator' },
{ id: 'golemancy', name: 'Golemancy', icon: '🗿', attunement: 'fabricator' },
// Legacy category (for backward compatibility)
{ id: 'craft', name: 'Crafting', icon: '🔧', attunement: null },
];
// ─── Rarity Colors ───────────────────────────────────────────────────────────
export const RARITY_COLORS: Record<string, string> = {
common: '#9CA3AF',
uncommon: '#22C55E',
rare: '#3B82F6',
epic: '#A855F7',
legendary: '#F97316',
mythic: '#EF4444',
};
// ─── Effect Research Mapping ───────────────────────────────────────────────────
// Maps research skill IDs to the effect IDs they unlock
export const EFFECT_RESEARCH_MAPPING: Record<string, string[]> = {
// Tier 1 - Basic Spell Effects
researchManaSpells: ['spell_manaStrike'],
researchFireSpells: ['spell_emberShot', 'spell_fireball'],
researchWaterSpells: ['spell_waterJet', 'spell_iceShard'],
researchAirSpells: ['spell_gust', 'spell_windSlash'],
researchEarthSpells: ['spell_stoneBullet', 'spell_rockSpike'],
researchLightSpells: ['spell_lightLance', 'spell_radiance'],
researchDarkSpells: ['spell_shadowBolt', 'spell_darkPulse'],
researchLifeDeathSpells: ['spell_drain', 'spell_lifeTap'],
// Tier 2 - Advanced Spell Effects
researchAdvancedFire: ['spell_inferno', 'spell_flameWave'],
researchAdvancedWater: ['spell_tidalWave', 'spell_iceStorm'],
researchAdvancedAir: ['spell_hurricane', 'spell_windBlade'],
researchAdvancedEarth: ['spell_earthquake', 'spell_stoneBarrage'],
researchAdvancedLight: ['spell_solarFlare', 'spell_divineSmite'],
researchAdvancedDark: ['spell_voidRift', 'spell_shadowStorm'],
// Tier 3 - Master Spell Effects
researchMasterFire: ['spell_pyroclasm'],
researchMasterWater: ['spell_tsunami'],
researchMasterEarth: ['spell_meteorStrike'],
// Combat Effect Research
researchDamageEffects: ['damage_5', 'damage_10', 'damage_pct_10'],
researchCombatEffects: ['crit_5', 'attack_speed_10'],
// Mana Effect Research
researchManaEffects: ['mana_cap_50', 'mana_regen_1', 'click_mana_1'],
researchAdvancedManaEffects: ['mana_cap_100', 'mana_regen_2', 'mana_regen_5', 'click_mana_3'],
// Utility Effect Research
researchUtilityEffects: ['meditate_10', 'study_10', 'insight_5'],
// Special Effect Research
researchSpecialEffects: ['spell_echo_10', 'lifesteal_5', 'guardian_dmg_10'],
researchOverpower: ['overpower_80'],
};
// Base effects unlocked when player gets enchanting skill level 1
export const BASE_UNLOCKED_EFFECTS: string[] = []; // No effects at game start
// Effects that unlock when getting enchanting skill level 1
export const ENCHANTING_UNLOCK_EFFECTS = ['spell_manaBolt'];
// ─── Base Unlocked Elements ───────────────────────────────────────────────────
export const BASE_UNLOCKED_ELEMENTS = ['fire', 'water', 'air', 'earth'];
// ─── Study Speed Formula ─────────────────────────────────────────────────────
export function getStudySpeedMultiplier(skills: Record<string, number>): number {
return 1 + (skills.quickLearner || 0) * 0.1;
}
// ─── Study Cost Formula ───────────────────────────────────────────────────────
export function getStudyCostMultiplier(skills: Record<string, number>): number {
return 1 - (skills.focusedMind || 0) * 0.05;
}
// ─── Element Opposites for Damage Calculation ────────────────────────────────
export const ELEMENT_OPPOSITES: Record<string, string> = {
fire: 'water', water: 'fire',
air: 'earth', earth: 'air',
light: 'dark', dark: 'light',
life: 'death', death: 'life',
};
// ─── Element Icon Mapping (Lucide Icons) ──────────────────────────────────────
// Note: These are string identifiers for dynamic icon loading
// The actual Lucide icons are imported in the components
export const ELEMENT_ICON_NAMES: Record<string, string> = {
fire: 'Flame',
water: 'Droplet',
air: 'Wind',
earth: 'Mountain',
light: 'Sun',
dark: 'Moon',
life: 'Leaf',
death: 'Skull',
mental: 'Brain',
transference: 'Link',
force: 'Wind',
blood: 'Droplets',
metal: 'Target',
wood: 'TreeDeciduous',
sand: 'Hourglass',
crystal: 'Gem',
stellar: 'Star',
void: 'CircleDot',
raw: 'Circle',
};

827
src/lib/game/crafting-slice.ts Executable file
View File

@@ -0,0 +1,827 @@
// ─── Crafting Store Slice ─────────────────────────────────────────────────────────
// Handles equipment and enchantment system: design, prepare, apply stages
import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentSlot, EquipmentCraftingProgress, LootInventory, AttunementState } from './types';
import { EQUIPMENT_TYPES, type EquipmentCategory } from './data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
import { SPELLS_DEF } from './constants';
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
// ─── Helper Functions ─────────────────────────────────────────────────────────
function generateInstanceId(): string {
return `equip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Get equipment category from type
function getEquipmentCategory(typeId: string): EquipmentCategory | null {
const type = EQUIPMENT_TYPES[typeId];
return type?.category || null;
}
// Calculate total capacity cost for a design
function calculateDesignCapacityCost(effects: DesignEffect[], efficiencyBonus: number = 0): number {
return effects.reduce((total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus), 0);
}
// Calculate design time based on number and complexity of effects
function calculateDesignTime(effects: DesignEffect[]): number {
// Base 1 hour + 0.5 hours per effect stack
let time = 1;
for (const eff of effects) {
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
if (effectDef) {
time += 0.5 * eff.stacks;
}
}
return time;
}
// Calculate preparation time based on equipment capacity
function calculatePrepTime(equipmentCapacity: number): number {
// Base 2 hours + 1 hour per 50 capacity
return 2 + Math.floor(equipmentCapacity / 50);
}
// Calculate preparation mana cost
function calculatePrepManaCost(equipmentCapacity: number): number {
// 10 mana per capacity point
return equipmentCapacity * 10;
}
// Calculate application time based on design complexity
function calculateApplicationTime(design: EnchantmentDesign): number {
// 2 hours base + 1 hour per effect stack
return 2 + design.effects.reduce((total, eff) => total + eff.stacks, 0);
}
// Calculate application mana cost per hour
function calculateApplicationManaPerHour(design: EnchantmentDesign): number {
// 20 mana per hour base + 5 per effect stack
return 20 + design.effects.reduce((total, eff) => total + eff.stacks * 5, 0);
}
// ─── Crafting Actions Interface ───────────────────────────────────────────────
export interface CraftingActions {
// Equipment management
createEquipmentInstance: (typeId: string) => string | null;
equipItem: (instanceId: string, slot: EquipmentSlot) => boolean;
unequipItem: (slot: EquipmentSlot) => void;
deleteEquipmentInstance: (instanceId: string) => void;
// Enchantment design
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean;
cancelDesign: () => void;
saveDesign: (design: EnchantmentDesign) => void;
deleteDesign: (designId: string) => void;
// Enchantment preparation
startPreparing: (equipmentInstanceId: string) => boolean;
cancelPreparation: () => void;
// Enchantment application
startApplying: (equipmentInstanceId: string, designId: string) => boolean;
pauseApplication: () => void;
resumeApplication: () => void;
cancelApplication: () => void;
// Disenchanting
disenchantEquipment: (instanceId: string) => void;
// Equipment Crafting (from blueprints)
startCraftingEquipment: (blueprintId: string) => boolean;
cancelEquipmentCrafting: () => void;
deleteMaterial: (materialId: string, amount: number) => void;
// Computed getters
getEquipmentSpells: () => string[];
getEquipmentEffects: () => Record<string, number>;
getAvailableCapacity: (instanceId: string) => number;
}
// ─── Initial Equipment Setup ───────────────────────────────────────────────────
export function createStartingEquipment(): {
equippedInstances: Record<string, string | null>;
equipmentInstances: Record<string, EquipmentInstance>;
} {
// Create starting staff with Mana Bolt enchantment
const staffId = generateInstanceId();
const staffInstance: EquipmentInstance = {
instanceId: staffId,
typeId: 'basicStaff',
name: 'Basic Staff',
enchantments: [
{ effectId: 'spell_manaBolt', stacks: 1, actualCost: 50 }
],
usedCapacity: 50,
totalCapacity: 50,
rarity: 'common',
quality: 100,
};
// Create starting clothes
const shirtId = generateInstanceId();
const shirtInstance: EquipmentInstance = {
instanceId: shirtId,
typeId: 'civilianShirt',
name: 'Civilian Shirt',
enchantments: [],
usedCapacity: 0,
totalCapacity: 30,
rarity: 'common',
quality: 100,
};
const shoesId = generateInstanceId();
const shoesInstance: EquipmentInstance = {
instanceId: shoesId,
typeId: 'civilianShoes',
name: 'Civilian Shoes',
enchantments: [],
usedCapacity: 0,
totalCapacity: 15,
rarity: 'common',
quality: 100,
};
return {
equippedInstances: {
mainHand: staffId,
offHand: null,
head: null,
body: shirtId,
hands: null,
feet: shoesId,
accessory1: null,
accessory2: null,
},
equipmentInstances: {
[staffId]: staffInstance,
[shirtId]: shirtInstance,
[shoesId]: shoesInstance,
},
};
}
// ─── Crafting Store Extensions ─────────────────────────────────────────────────
export function createCraftingSlice(
set: (fn: (state: GameState) => Partial<GameState>) => void,
get: () => GameState & CraftingActions
): CraftingActions {
return {
// ─── Equipment Management ─────────────────────────────────────────────────
createEquipmentInstance: (typeId: string) => {
const type = EQUIPMENT_TYPES[typeId];
if (!type) return null;
const instanceId = generateInstanceId();
const instance: EquipmentInstance = {
instanceId,
typeId,
name: type.name,
enchantments: [],
usedCapacity: 0,
totalCapacity: type.baseCapacity,
rarity: 'common',
quality: 100,
};
set((state) => ({
equipmentInstances: {
...state.equipmentInstances,
[instanceId]: instance,
},
}));
return instanceId;
},
equipItem: (instanceId: string, slot: EquipmentSlot) => {
const state = get();
const instance = state.equipmentInstances[instanceId];
if (!instance) return false;
const type = EQUIPMENT_TYPES[instance.typeId];
if (!type) return false;
// Check if equipment can go in this slot
const validSlots = type.category === 'accessory'
? ['accessory1', 'accessory2']
: [type.slot];
if (!validSlots.includes(slot)) return false;
// Check if slot is occupied
const currentEquipped = state.equippedInstances[slot];
if (currentEquipped === instanceId) return true; // Already equipped here
// If this item is equipped elsewhere, unequip it first
let newEquipped = { ...state.equippedInstances };
for (const [s, id] of Object.entries(newEquipped)) {
if (id === instanceId) {
newEquipped[s as EquipmentSlot] = null;
}
}
// Equip to new slot
newEquipped[slot] = instanceId;
set(() => ({ equippedInstances: newEquipped }));
return true;
},
unequipItem: (slot: EquipmentSlot) => {
set((state) => ({
equippedInstances: {
...state.equippedInstances,
[slot]: null,
},
}));
},
deleteEquipmentInstance: (instanceId: string) => {
set((state) => {
// First unequip if equipped
let newEquipped = { ...state.equippedInstances };
for (const [slot, id] of Object.entries(newEquipped)) {
if (id === instanceId) {
newEquipped[slot as EquipmentSlot] = null;
}
}
// Remove from instances
const newInstances = { ...state.equipmentInstances };
delete newInstances[instanceId];
return {
equippedInstances: newEquipped,
equipmentInstances: newInstances,
};
});
},
// ─── Enchantment Design ─────────────────────────────────────────────────
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => {
const state = get();
// Check if player has enchanting skill
const enchantingLevel = state.skills.enchanting || 0;
if (enchantingLevel < 1) return false;
// Validate effects for equipment category
const category = getEquipmentCategory(equipmentTypeId);
if (!category) return false;
for (const eff of effects) {
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
if (!effectDef) return false;
if (!effectDef.allowedEquipmentCategories.includes(category)) return false;
if (eff.stacks > effectDef.maxStacks) return false;
}
// Calculate capacity cost
const efficiencyBonus = (state.skills.efficientEnchant || 0) * 0.05;
const totalCapacityCost = calculateDesignCapacityCost(effects, efficiencyBonus);
// Create design
const designTime = calculateDesignTime(effects);
const design: EnchantmentDesign = {
id: `design_${Date.now()}`,
name,
equipmentType: equipmentTypeId,
effects,
totalCapacityUsed: totalCapacityCost,
designTime,
created: Date.now(),
};
set(() => ({
currentAction: 'design',
designProgress: {
designId: design.id,
progress: 0,
required: designTime,
},
}));
return true;
},
cancelDesign: () => {
set(() => ({
currentAction: 'meditate',
designProgress: null,
}));
},
saveDesign: (design: EnchantmentDesign) => {
set((state) => ({
enchantmentDesigns: [...state.enchantmentDesigns, design],
designProgress: null,
currentAction: 'meditate',
}));
},
deleteDesign: (designId: string) => {
set((state) => ({
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
}));
},
// ─── Enchantment Preparation ─────────────────────────────────────────────
startPreparing: (equipmentInstanceId: string) => {
const state = get();
const instance = state.equipmentInstances[equipmentInstanceId];
if (!instance) return false;
const prepTime = calculatePrepTime(instance.totalCapacity);
const manaCost = calculatePrepManaCost(instance.totalCapacity);
if (state.rawMana < manaCost) return false;
set(() => ({
currentAction: 'prepare',
preparationProgress: {
equipmentInstanceId,
progress: 0,
required: prepTime,
manaCostPaid: 0,
},
}));
return true;
},
cancelPreparation: () => {
set(() => ({
currentAction: 'meditate',
preparationProgress: null,
}));
},
// ─── Enchantment Application ─────────────────────────────────────────────
startApplying: (equipmentInstanceId: string, designId: string) => {
const state = get();
const instance = state.equipmentInstances[equipmentInstanceId];
const design = state.enchantmentDesigns.find(d => d.id === designId);
if (!instance || !design) return false;
// Check capacity
if (instance.usedCapacity + design.totalCapacityUsed > instance.totalCapacity) {
return false;
}
const applicationTime = calculateApplicationTime(design);
const manaPerHour = calculateApplicationManaPerHour(design);
set(() => ({
currentAction: 'enchant',
applicationProgress: {
equipmentInstanceId,
designId,
progress: 0,
required: applicationTime,
manaPerHour,
paused: false,
manaSpent: 0,
},
}));
return true;
},
pauseApplication: () => {
set((state) => {
if (!state.applicationProgress) return {};
return {
applicationProgress: {
...state.applicationProgress,
paused: true,
},
};
});
},
resumeApplication: () => {
set((state) => {
if (!state.applicationProgress) return {};
return {
applicationProgress: {
...state.applicationProgress,
paused: false,
},
};
});
},
cancelApplication: () => {
set(() => ({
currentAction: 'meditate',
applicationProgress: null,
}));
},
// ─── Disenchanting ─────────────────────────────────────────────────────────
disenchantEquipment: (instanceId: string) => {
const state = get();
const instance = state.equipmentInstances[instanceId];
if (!instance || instance.enchantments.length === 0) return;
const disenchantLevel = state.skills.disenchanting || 0;
const recoveryRate = 0.1 + disenchantLevel * 0.2; // 10% base + 20% per level
let totalRecovered = 0;
for (const ench of instance.enchantments) {
totalRecovered += Math.floor(ench.actualCost * recoveryRate);
}
set((state) => ({
rawMana: state.rawMana + totalRecovered,
equipmentInstances: {
...state.equipmentInstances,
[instanceId]: {
...instance,
enchantments: [],
usedCapacity: 0,
},
},
log: [`✨ Disenchanted ${instance.name}, recovered ${totalRecovered} mana.`, ...state.log.slice(0, 49)],
}));
},
// ─── Computed Getters ─────────────────────────────────────────────────────
getEquipmentSpells: () => {
const state = get();
const spells: string[] = [];
for (const instanceId of Object.values(state.equippedInstances)) {
if (!instanceId) continue;
const instance = state.equipmentInstances[instanceId];
if (!instance) continue;
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
spells.push(effectDef.effect.spellId);
}
}
}
return [...new Set(spells)]; // Remove duplicates
},
getEquipmentEffects: () => {
const state = get();
const effects: Record<string, number> = {};
for (const instanceId of Object.values(state.equippedInstances)) {
if (!instanceId) continue;
const instance = state.equipmentInstances[instanceId];
if (!instance) continue;
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (!effectDef) continue;
if (effectDef.effect.type === 'bonus' && effectDef.effect.stat && effectDef.effect.value) {
effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + effectDef.effect.value * ench.stacks;
}
}
}
return effects;
},
getAvailableCapacity: (instanceId: string) => {
const state = get();
const instance = state.equipmentInstances[instanceId];
if (!instance) return 0;
return instance.totalCapacity - instance.usedCapacity;
},
// ─── Equipment Crafting (from Blueprints) ───────────────────────────────────
startCraftingEquipment: (blueprintId: string) => {
const state = get();
const recipe = CRAFTING_RECIPES[blueprintId];
if (!recipe) return false;
// Check if player has the blueprint
if (!state.lootInventory.blueprints.includes(blueprintId)) return false;
// Check materials
const { canCraft, missingMaterials, missingMana } = canCraftRecipe(
recipe,
state.lootInventory.materials,
state.rawMana
);
if (!canCraft) return false;
// Deduct materials
const newMaterials = { ...state.lootInventory.materials };
for (const [matId, amount] of Object.entries(recipe.materials)) {
newMaterials[matId] = (newMaterials[matId] || 0) - amount;
if (newMaterials[matId] <= 0) {
delete newMaterials[matId];
}
}
// Start crafting progress
set((state) => ({
lootInventory: {
...state.lootInventory,
materials: newMaterials,
},
rawMana: state.rawMana - recipe.manaCost,
currentAction: 'craft',
equipmentCraftingProgress: {
blueprintId,
equipmentTypeId: recipe.equipmentTypeId,
progress: 0,
required: recipe.craftTime,
manaSpent: recipe.manaCost,
},
}));
return true;
},
cancelEquipmentCrafting: () => {
set((state) => {
const progress = state.equipmentCraftingProgress;
if (!progress) return {};
const recipe = CRAFTING_RECIPES[progress.blueprintId];
if (!recipe) return { currentAction: 'meditate', equipmentCraftingProgress: null };
// Refund 50% of mana
const manaRefund = Math.floor(progress.manaSpent * 0.5);
return {
currentAction: 'meditate',
equipmentCraftingProgress: null,
rawMana: state.rawMana + manaRefund,
log: [`🚫 Equipment crafting cancelled. Refunded ${manaRefund} mana.`, ...state.log.slice(0, 49)],
};
});
},
deleteMaterial: (materialId: string, amount: number) => {
set((state) => {
const currentAmount = state.lootInventory.materials[materialId] || 0;
const newAmount = Math.max(0, currentAmount - amount);
const newMaterials = { ...state.lootInventory.materials };
if (newAmount <= 0) {
delete newMaterials[materialId];
} else {
newMaterials[materialId] = newAmount;
}
const dropName = materialId; // Could look up in LOOT_DROPS for proper name
return {
lootInventory: {
...state.lootInventory,
materials: newMaterials,
},
log: [`🗑️ Deleted ${amount}x ${dropName}.`, ...state.log.slice(0, 49)],
};
});
},
};
}
// ─── Tick Processing for Crafting ─────────────────────────────────────────────
export function processCraftingTick(
state: GameState,
effects: { rawMana: number; log: string[] }
): Partial<GameState> {
const { rawMana, log } = effects;
let updates: Partial<GameState> = {};
// Process design progress
if (state.currentAction === 'design' && state.designProgress) {
const progress = state.designProgress.progress + 0.04; // HOURS_PER_TICK
if (progress >= state.designProgress.required) {
// Design complete - but we need the design data to save it
// This will be handled by the UI calling saveDesign
updates = {
...updates,
log: ['✅ Enchantment design complete!', ...log],
};
} else {
updates = {
...updates,
designProgress: {
...state.designProgress,
progress,
},
};
}
}
// Process preparation progress
if (state.currentAction === 'prepare' && state.preparationProgress) {
const prep = state.preparationProgress;
const manaPerHour = calculatePrepManaCost(
state.equipmentInstances[prep.equipmentInstanceId]?.totalCapacity || 50
) / prep.required;
const manaCost = manaPerHour * 0.04; // HOURS_PER_TICK
if (rawMana >= manaCost) {
const progress = prep.progress + 0.04;
const manaCostPaid = prep.manaCostPaid + manaCost;
if (progress >= prep.required) {
updates = {
...updates,
rawMana: rawMana - manaCost,
preparationProgress: null,
currentAction: 'meditate',
log: ['✅ Equipment prepared for enchanting!', ...log],
};
} else {
updates = {
...updates,
rawMana: rawMana - manaCost,
preparationProgress: {
...prep,
progress,
manaCostPaid,
},
};
}
}
}
// Process application progress
if (state.currentAction === 'enchant' && state.applicationProgress && !state.applicationProgress.paused) {
const app = state.applicationProgress;
const manaCost = app.manaPerHour * 0.04; // HOURS_PER_TICK
if (rawMana >= manaCost) {
const progress = app.progress + 0.04;
const manaSpent = app.manaSpent + manaCost;
if (progress >= app.required) {
// Apply the enchantment!
const instance = state.equipmentInstances[app.equipmentInstanceId];
const design = state.enchantmentDesigns.find(d => d.id === app.designId);
if (instance && design) {
const newEnchantments: AppliedEnchantment[] = design.effects.map(eff => ({
effectId: eff.effectId,
stacks: eff.stacks,
actualCost: eff.capacityCost,
}));
// Calculate and grant attunement XP to enchanter
const xpGained = calculateEnchantingXP(design.totalCapacityUsed);
let newAttunements = state.attunements;
if (state.attunements.enchanter?.active && xpGained > 0) {
const enchanterState = state.attunements.enchanter;
let newXP = enchanterState.experience + xpGained;
let newLevel = enchanterState.level;
// Check for level ups
while (newLevel < MAX_ATTUNEMENT_LEVEL) {
const xpNeeded = getAttunementXPForLevel(newLevel + 1);
if (newXP >= xpNeeded) {
newXP -= xpNeeded;
newLevel++;
} else {
break;
}
}
newAttunements = {
...state.attunements,
enchanter: {
...enchanterState,
level: newLevel,
experience: newXP,
},
};
}
updates = {
...updates,
rawMana: rawMana - manaCost,
applicationProgress: null,
currentAction: 'meditate',
attunements: newAttunements,
equipmentInstances: {
...state.equipmentInstances,
[app.equipmentInstanceId]: {
...instance,
enchantments: [...instance.enchantments, ...newEnchantments],
usedCapacity: instance.usedCapacity + design.totalCapacityUsed,
},
},
log: [`✨ Enchantment "${design.name}" applied to ${instance.name}! (+${xpGained} Enchanter XP)`, ...log],
};
}
} else {
updates = {
...updates,
rawMana: rawMana - manaCost,
applicationProgress: {
...app,
progress,
manaSpent,
},
};
}
}
}
// Process equipment crafting progress
if (state.currentAction === 'craft' && state.equipmentCraftingProgress) {
const craft = state.equipmentCraftingProgress;
const progress = craft.progress + 0.04; // HOURS_PER_TICK
if (progress >= craft.required) {
// Crafting complete - create the equipment!
const recipe = CRAFTING_RECIPES[craft.blueprintId];
const equipType = recipe ? EQUIPMENT_TYPES[recipe.equipmentTypeId] : null;
if (recipe && equipType) {
const instanceId = generateInstanceId();
const newInstance: EquipmentInstance = {
instanceId,
typeId: recipe.equipmentTypeId,
name: recipe.name,
enchantments: [],
usedCapacity: 0,
totalCapacity: equipType.baseCapacity,
rarity: recipe.rarity,
quality: 100,
};
updates = {
...updates,
equipmentCraftingProgress: null,
currentAction: 'meditate',
equipmentInstances: {
...state.equipmentInstances,
[instanceId]: newInstance,
},
totalCraftsCompleted: (state.totalCraftsCompleted || 0) + 1,
log: [`🔨 Crafted ${recipe.name}!`, ...log],
};
} else {
updates = {
...updates,
equipmentCraftingProgress: null,
currentAction: 'meditate',
log: ['⚠️ Crafting failed - invalid recipe!', ...log],
};
}
} else {
updates = {
...updates,
equipmentCraftingProgress: {
...craft,
progress,
},
};
}
}
return updates;
}
// ─── Export helper to get equipment instance spells ─────────────────────────────
export function getSpellsFromEquipment(instances: Record<string, EquipmentInstance>, equippedIds: (string | null)[]): string[] {
const spells: string[] = [];
for (const id of equippedIds) {
if (!id) continue;
const instance = instances[id];
if (!instance) continue;
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
spells.push(effectDef.effect.spellId);
}
}
}
return [...new Set(spells)];
}

272
src/lib/game/data/achievements.ts Executable file
View File

@@ -0,0 +1,272 @@
// ─── Achievement Definitions ───────────────────────────────────────────────────
import type { AchievementDef } from '../types';
export const ACHIEVEMENTS: Record<string, AchievementDef> = {
// ─── Combat Achievements ───
firstBlood: {
id: 'firstBlood',
name: 'First Blood',
desc: 'Clear your first floor',
category: 'combat',
requirement: { type: 'floor', value: 2 },
reward: { insight: 10 },
},
floorClimber: {
id: 'floorClimber',
name: 'Floor Climber',
desc: 'Reach floor 10',
category: 'combat',
requirement: { type: 'floor', value: 10 },
reward: { insight: 25, manaBonus: 10 },
},
spireAssault: {
id: 'spireAssault',
name: 'Spire Assault',
desc: 'Reach floor 25',
category: 'combat',
requirement: { type: 'floor', value: 25 },
reward: { insight: 50, damageBonus: 0.05 },
},
towerConqueror: {
id: 'towerConqueror',
name: 'Tower Conqueror',
desc: 'Reach floor 50',
category: 'combat',
requirement: { type: 'floor', value: 50 },
reward: { insight: 100, manaBonus: 50, damageBonus: 0.1 },
},
spireMaster: {
id: 'spireMaster',
name: 'Spire Master',
desc: 'Reach floor 75',
category: 'combat',
requirement: { type: 'floor', value: 75 },
reward: { insight: 200, damageBonus: 0.15, title: 'Spire Master' },
},
apexReached: {
id: 'apexReached',
name: 'Apex Reached',
desc: 'Reach floor 100',
category: 'combat',
requirement: { type: 'floor', value: 100 },
reward: { insight: 500, manaBonus: 200, damageBonus: 0.25, title: 'Apex Climber' },
},
// ─── Combo Achievements ───
comboStarter: {
id: 'comboStarter',
name: 'Combo Starter',
desc: 'Reach a 10-hit combo',
category: 'combat',
requirement: { type: 'combo', value: 10 },
reward: { insight: 15 },
},
comboMaster: {
id: 'comboMaster',
name: 'Combo Master',
desc: 'Reach a 50-hit combo',
category: 'combat',
requirement: { type: 'combo', value: 50 },
reward: { insight: 50, damageBonus: 0.05 },
},
comboLegend: {
id: 'comboLegend',
name: 'Combo Legend',
desc: 'Reach a 100-hit combo',
category: 'combat',
requirement: { type: 'combo', value: 100 },
reward: { insight: 150, damageBonus: 0.1, title: 'Combo Legend' },
},
// ─── Damage Achievements ───
hundredDamage: {
id: 'hundredDamage',
name: 'Heavy Hitter',
desc: 'Deal 100 damage in a single hit',
category: 'combat',
requirement: { type: 'damage', value: 100 },
reward: { insight: 20 },
},
thousandDamage: {
id: 'thousandDamage',
name: 'Devastating Blow',
desc: 'Deal 1,000 damage in a single hit',
category: 'combat',
requirement: { type: 'damage', value: 1000 },
reward: { insight: 75, damageBonus: 0.03 },
},
tenThousandDamage: {
id: 'tenThousandDamage',
name: 'Apocalypse Now',
desc: 'Deal 10,000 damage in a single hit',
category: 'combat',
requirement: { type: 'damage', value: 10000 },
reward: { insight: 200, damageBonus: 0.05, title: 'Apocalypse Bringer' },
},
// ─── Pact Achievements ───
pactSeeker: {
id: 'pactSeeker',
name: 'Pact Seeker',
desc: 'Sign your first guardian pact',
category: 'progression',
requirement: { type: 'pact', value: 1 },
reward: { insight: 30 },
},
pactCollector: {
id: 'pactCollector',
name: 'Pact Collector',
desc: 'Sign 5 guardian pacts',
category: 'progression',
requirement: { type: 'pact', value: 5 },
reward: { insight: 100, manaBonus: 25 },
},
pactMaster: {
id: 'pactMaster',
name: 'Pact Master',
desc: 'Sign all guardian pacts',
category: 'progression',
requirement: { type: 'pact', value: 12 },
reward: { insight: 500, damageBonus: 0.2, title: 'Pact Master' },
},
// ─── Magic Achievements ───
spellCaster: {
id: 'spellCaster',
name: 'Spell Caster',
desc: 'Cast 100 spells',
category: 'magic',
requirement: { type: 'spells', value: 100 },
reward: { insight: 25 },
},
spellWeaver: {
id: 'spellWeaver',
name: 'Spell Weaver',
desc: 'Cast 1,000 spells',
category: 'magic',
requirement: { type: 'spells', value: 1000 },
reward: { insight: 75, regenBonus: 0.5 },
},
spellStorm: {
id: 'spellStorm',
name: 'Spell Storm',
desc: 'Cast 10,000 spells',
category: 'magic',
requirement: { type: 'spells', value: 10000 },
reward: { insight: 200, regenBonus: 1, title: 'Storm Caller' },
},
// ─── Mana Achievements ───
manaPool: {
id: 'manaPool',
name: 'Mana Pool',
desc: 'Accumulate 1,000 total mana',
category: 'magic',
requirement: { type: 'mana', value: 1000 },
reward: { insight: 20 },
},
manaLake: {
id: 'manaLake',
name: 'Mana Lake',
desc: 'Accumulate 100,000 total mana',
category: 'magic',
requirement: { type: 'mana', value: 100000 },
reward: { insight: 100, manaBonus: 50 },
},
manaOcean: {
id: 'manaOcean',
name: 'Mana Ocean',
desc: 'Accumulate 10,000,000 total mana',
category: 'magic',
requirement: { type: 'mana', value: 10000000 },
reward: { insight: 500, manaBonus: 200, title: 'Mana Ocean' },
},
// ─── Crafting Achievements ───
enchanter: {
id: 'enchanter',
name: 'Enchanter',
desc: 'Complete your first enchantment',
category: 'crafting',
requirement: { type: 'craft', value: 1 },
reward: { insight: 30 },
},
masterEnchanter: {
id: 'masterEnchanter',
name: 'Master Enchanter',
desc: 'Complete 10 enchantments',
category: 'crafting',
requirement: { type: 'craft', value: 10 },
reward: { insight: 100, unlockEffect: 'efficiencyBoost' },
},
legendaryEnchanter: {
id: 'legendaryEnchanter',
name: 'Legendary Enchanter',
desc: 'Complete 50 enchantments',
category: 'crafting',
requirement: { type: 'craft', value: 50 },
reward: { insight: 300, title: 'Legendary Enchanter' },
},
// ─── Special Achievements ───
speedRunner: {
id: 'speedRunner',
name: 'Speed Runner',
desc: 'Reach floor 50 in under 5 days',
category: 'special',
requirement: { type: 'time', value: 5, subType: 'floor50' },
reward: { insight: 200, title: 'Speed Demon' },
hidden: true,
},
perfectionist: {
id: 'perfectionist',
name: 'Perfectionist',
desc: 'Reach floor 100 without any guardian pacts',
category: 'special',
requirement: { type: 'floor', value: 100, subType: 'noPacts' },
reward: { insight: 1000, title: 'Perfect Climber' },
hidden: true,
},
survivor: {
id: 'survivor',
name: 'Survivor',
desc: 'Complete a loop during full incursion (day 30+)',
category: 'special',
requirement: { type: 'time', value: 30 },
reward: { insight: 300, manaBonus: 100, title: 'Survivor' },
},
};
// Category colors for UI
export const ACHIEVEMENT_CATEGORY_COLORS: Record<string, string> = {
combat: '#EF4444', // Red
progression: '#F59E0B', // Amber
crafting: '#8B5CF6', // Purple
magic: '#3B82F6', // Blue
special: '#EC4899', // Pink
};
// Get achievements by category
export function getAchievementsByCategory(): Record<string, AchievementDef[]> {
const result: Record<string, AchievementDef[]> = {};
for (const achievement of Object.values(ACHIEVEMENTS)) {
if (!result[achievement.category]) {
result[achievement.category] = [];
}
result[achievement.category].push(achievement);
}
return result;
}
// Check if an achievement should be revealed
export function isAchievementRevealed(
achievement: AchievementDef,
progress: number
): boolean {
if (!achievement.hidden) return true;
// Reveal hidden achievements when at 50% progress
return progress >= achievement.requirement.value * 0.5;
}

185
src/lib/game/data/attunements.ts Executable file
View File

@@ -0,0 +1,185 @@
// ─── Attunement Definitions ─────────────────────────────────────────────────────
// Attunements are class-like abilities tied to body locations
// Each provides unique capabilities, primary mana types, and skill access
import type { AttunementDef, AttunementSlot } from '../types';
// Attunement slot display names
export const ATTUNEMENT_SLOT_NAMES: Record<AttunementSlot, string> = {
rightHand: 'Right Hand',
leftHand: 'Left Hand',
head: 'Head',
back: 'Back',
chest: 'Chest',
leftLeg: 'Left Leg',
rightLeg: 'Right Leg',
};
// All attunement definitions
export const ATTUNEMENTS_DEF: Record<string, AttunementDef> = {
// ─── Enchanter (Right Hand) ─────────────────────────────────────────────────
// Unlocks the enchanting system - applying magical effects to equipment
// Primary mana: Transference (used to move/apply enchantments)
enchanter: {
id: 'enchanter',
name: 'Enchanter',
desc: 'Channel transference mana through your right hand to apply magical enchantments to equipment. The art of enchanting allows you to imbue items with spell effects, stat bonuses, and special properties.',
slot: 'rightHand',
icon: '✨',
color: '#1ABC9C', // Teal (transference color)
primaryManaType: 'transference',
rawManaRegen: 0.5,
conversionRate: 0.2, // Converts 0.2 raw mana to transference per hour
unlocked: true, // Starting attunement
capabilities: ['enchanting', 'disenchanting'],
skillCategories: ['enchant', 'effectResearch'],
},
// ─── Invoker (Chest/Heart) ───────────────────────────────────────────────────
// Enables forming pacts with spire guardians
// No primary mana - instead gains mana types from each pact signed
invoker: {
id: 'invoker',
name: 'Invoker',
desc: 'Open your heart to the guardians of the spire. Form pacts with defeated guardians to gain their elemental affinity and access to their unique powers. Each pact grants access to a new mana type.',
slot: 'chest',
icon: '💜',
color: '#9B59B6', // Purple
primaryManaType: undefined, // Invoker has no primary - gains from pacts
rawManaRegen: 0.3,
conversionRate: 0, // No automatic conversion - mana comes from pacts
unlocked: false, // Unlocked through gameplay
unlockCondition: 'Defeat your first guardian and choose the path of the Invoker',
capabilities: ['pacts', 'guardianPowers', 'elementalMastery'],
skillCategories: ['invocation', 'pact'],
},
// ─── Fabricator (Left Hand) ──────────────────────────────────────────────────
// Crafts earth golems and earthen gear
// Primary mana: Earth
// Later with fire mana -> metal mana, can craft metallic gear and golems
fabricator: {
id: 'fabricator',
name: 'Fabricator',
desc: 'Shape earth and metal through your left hand to craft golems and equipment. Start with earthen constructs, and unlock metalworking when you gain fire mana to create metal mana.',
slot: 'leftHand',
icon: '⚒️',
color: '#F4A261', // Earth color
primaryManaType: 'earth',
rawManaRegen: 0.4,
conversionRate: 0.25, // Converts 0.25 raw mana to earth per hour
unlocked: false, // Unlocked through gameplay
unlockCondition: 'Prove your worth as a crafter',
capabilities: ['golemCrafting', 'gearCrafting', 'earthShaping'],
skillCategories: ['fabrication', 'golemancy'],
},
};
// Helper function to get attunement by slot
export function getAttunementBySlot(slot: AttunementSlot): AttunementDef | undefined {
return Object.values(ATTUNEMENTS_DEF).find(a => a.slot === slot);
}
// Helper function to get all unlocked attunements for a player
export function getUnlockedAttunements(attunements: Record<string, { active: boolean; level: number; experience: number }>): AttunementDef[] {
return Object.entries(attunements)
.filter(([id, state]) => state.active || ATTUNEMENTS_DEF[id]?.unlocked)
.map(([id]) => ATTUNEMENTS_DEF[id])
.filter(Boolean);
}
// Helper function to calculate total raw mana regen from attunements (with level scaling)
export function getTotalAttunementRegen(attunements: Record<string, { active: boolean; level: number; experience: number }>): number {
return Object.entries(attunements)
.filter(([, state]) => state.active)
.reduce((total, [id, state]) => {
const def = ATTUNEMENTS_DEF[id];
if (!def) return total;
// Exponential scaling: base * (1.5 ^ (level - 1))
const levelMult = Math.pow(1.5, (state.level || 1) - 1);
return total + def.rawManaRegen * levelMult;
}, 0);
}
// Get conversion rate with level scaling
export function getAttunementConversionRate(attunementId: string, level: number): number {
const def = ATTUNEMENTS_DEF[attunementId];
if (!def || def.conversionRate <= 0) return 0;
// Exponential scaling: base * (1.5 ^ (level - 1))
return def.conversionRate * Math.pow(1.5, (level || 1) - 1);
}
// XP required for attunement level
export function getAttunementXPForLevel(level: number): number {
// New scaling:
// Level 2: 1000 XP
// Level 3: 2500 XP
// Level 4: 5000 XP
// Level 5: 10000 XP
// etc. (each level requires 2x the previous, starting from 1000)
if (level <= 1) return 0;
if (level === 2) return 1000;
// For level 3+: 1000 * 2.5^(level-2), but rounded nicely
return Math.floor(1000 * Math.pow(2, level - 2) * (level >= 3 ? 1.25 : 1));
}
// Calculate XP gained from enchanting based on capacity used
export function calculateEnchantingXP(capacityUsed: number): number {
// 1 XP per 10 capacity used, floored, minimum 1
return Math.max(1, Math.floor(capacityUsed / 10));
}
// Max attunement level
export const MAX_ATTUNEMENT_LEVEL = 10;
// Helper function to get mana types from active attunements and pacts
export function getAttunementManaTypes(
attunements: Record<string, { active: boolean; level: number; experience: number }>,
signedPacts: number[]
): string[] {
const manaTypes: string[] = [];
// Add primary mana types from active attunements
Object.entries(attunements)
.filter(([, state]) => state.active)
.forEach(([id]) => {
const def = ATTUNEMENTS_DEF[id];
if (def?.primaryManaType) {
manaTypes.push(def.primaryManaType);
}
});
// Invoker gains mana types from signed pacts
if (attunements.invoker?.active && signedPacts.length > 0) {
// Import GUARDIANS would be circular, so this is handled in the store
// For now, just mark that invoker provides pact-based mana
manaTypes.push('pactElements');
}
return [...new Set(manaTypes)]; // Remove duplicates
}
// Get skill categories available to player based on active attunements
export function getAvailableSkillCategories(
attunements: Record<string, { active: boolean; level: number; experience: number }>
): string[] {
const categories = new Set<string>();
// Always available categories
categories.add('mana');
categories.add('study');
categories.add('research');
categories.add('ascension');
// Add categories from active attunements
Object.entries(attunements)
.filter(([, state]) => state.active)
.forEach(([id]) => {
const def = ATTUNEMENTS_DEF[id];
if (def?.skillCategories) {
def.skillCategories.forEach(cat => categories.add(cat));
}
});
return Array.from(categories);
}

View File

@@ -0,0 +1,257 @@
// ─── Crafting Recipes ─────────────────────────────────────────────────────────
// Defines what materials are needed to craft equipment from blueprints
import type { EquipmentSlot } from '../types';
export interface CraftingRecipe {
id: string; // Blueprint ID (matches loot drop)
equipmentTypeId: string; // Resulting equipment type ID
name: string; // Display name
description: string;
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
materials: Record<string, number>; // materialId -> count required
manaCost: number; // Raw mana cost to craft
craftTime: number; // Hours to craft
minFloor: number; // Minimum floor where blueprint drops
unlocked: boolean; // Whether the player has discovered this
}
export const CRAFTING_RECIPES: Record<string, CraftingRecipe> = {
// ─── Staff Blueprints ───
staffBlueprint: {
id: 'staffBlueprint',
equipmentTypeId: 'oakStaff',
name: 'Oak Staff',
description: 'A sturdy oak staff with decent mana capacity.',
rarity: 'uncommon',
materials: {
manaCrystalDust: 5,
arcaneShard: 2,
},
manaCost: 200,
craftTime: 4,
minFloor: 10,
unlocked: false,
},
wandBlueprint: {
id: 'wandBlueprint',
equipmentTypeId: 'crystalWand',
name: 'Crystal Wand',
description: 'A wand tipped with a small crystal. Excellent for elemental enchantments.',
rarity: 'rare',
materials: {
manaCrystalDust: 8,
arcaneShard: 4,
elementalCore: 1,
},
manaCost: 500,
craftTime: 6,
minFloor: 20,
unlocked: false,
},
robeBlueprint: {
id: 'robeBlueprint',
equipmentTypeId: 'scholarRobe',
name: 'Scholar Robe',
description: 'A robe worn by scholars and researchers.',
rarity: 'rare',
materials: {
manaCrystalDust: 6,
arcaneShard: 3,
elementalCore: 1,
},
manaCost: 400,
craftTime: 5,
minFloor: 25,
unlocked: false,
},
artifactBlueprint: {
id: 'artifactBlueprint',
equipmentTypeId: 'arcanistStaff',
name: 'Arcanist Staff',
description: 'A staff designed for advanced spellcasters. High capacity for complex enchantments.',
rarity: 'legendary',
materials: {
manaCrystalDust: 20,
arcaneShard: 10,
elementalCore: 5,
voidEssence: 2,
celestialFragment: 1,
},
manaCost: 2000,
craftTime: 12,
minFloor: 60,
unlocked: false,
},
// ─── Additional Blueprints ───
battlestaffBlueprint: {
id: 'battlestaffBlueprint',
equipmentTypeId: 'battlestaff',
name: 'Battlestaff',
description: 'A reinforced staff suitable for both casting and combat.',
rarity: 'rare',
materials: {
manaCrystalDust: 10,
arcaneShard: 5,
elementalCore: 2,
},
manaCost: 600,
craftTime: 6,
minFloor: 30,
unlocked: false,
},
catalystBlueprint: {
id: 'catalystBlueprint',
equipmentTypeId: 'fireCatalyst',
name: 'Fire Catalyst',
description: 'A catalyst attuned to fire magic. Enhances fire enchantments.',
rarity: 'rare',
materials: {
manaCrystalDust: 8,
arcaneShard: 4,
elementalCore: 3,
},
manaCost: 500,
craftTime: 5,
minFloor: 25,
unlocked: false,
},
shieldBlueprint: {
id: 'shieldBlueprint',
equipmentTypeId: 'runicShield',
name: 'Runic Shield',
description: 'A shield engraved with protective runes.',
rarity: 'rare',
materials: {
manaCrystalDust: 10,
arcaneShard: 6,
elementalCore: 2,
},
manaCost: 450,
craftTime: 5,
minFloor: 28,
unlocked: false,
},
hatBlueprint: {
id: 'hatBlueprint',
equipmentTypeId: 'wizardHat',
name: 'Wizard Hat',
description: 'A classic pointed wizard hat. Decent capacity for headwear.',
rarity: 'uncommon',
materials: {
manaCrystalDust: 4,
arcaneShard: 2,
},
manaCost: 150,
craftTime: 3,
minFloor: 12,
unlocked: false,
},
glovesBlueprint: {
id: 'glovesBlueprint',
equipmentTypeId: 'spellweaveGloves',
name: 'Spellweave Gloves',
description: 'Gloves woven with mana-conductive threads.',
rarity: 'uncommon',
materials: {
manaCrystalDust: 3,
arcaneShard: 2,
},
manaCost: 120,
craftTime: 3,
minFloor: 15,
unlocked: false,
},
bootsBlueprint: {
id: 'bootsBlueprint',
equipmentTypeId: 'travelerBoots',
name: 'Traveler Boots',
description: 'Comfortable boots for long journeys.',
rarity: 'uncommon',
materials: {
manaCrystalDust: 3,
arcaneShard: 1,
},
manaCost: 100,
craftTime: 2,
minFloor: 8,
unlocked: false,
},
ringBlueprint: {
id: 'ringBlueprint',
equipmentTypeId: 'silverRing',
name: 'Silver Ring',
description: 'A silver ring with decent magical conductivity.',
rarity: 'uncommon',
materials: {
manaCrystalDust: 2,
arcaneShard: 1,
},
manaCost: 80,
craftTime: 2,
minFloor: 10,
unlocked: false,
},
amuletBlueprint: {
id: 'amuletBlueprint',
equipmentTypeId: 'silverAmulet',
name: 'Silver Amulet',
description: 'A silver amulet with a small gem.',
rarity: 'uncommon',
materials: {
manaCrystalDust: 3,
arcaneShard: 2,
},
manaCost: 100,
craftTime: 3,
minFloor: 12,
unlocked: false,
},
};
// Helper functions
export function getRecipeByBlueprint(blueprintId: string): CraftingRecipe | undefined {
return CRAFTING_RECIPES[blueprintId];
}
export function canCraftRecipe(
recipe: CraftingRecipe,
materials: Record<string, number>,
rawMana: number
): { canCraft: boolean; missingMaterials: Record<string, number>; missingMana: number } {
const missingMaterials: Record<string, number> = {};
let canCraft = true;
for (const [matId, required] of Object.entries(recipe.materials)) {
const available = materials[matId] || 0;
if (available < required) {
missingMaterials[matId] = required - available;
canCraft = false;
}
}
const missingMana = Math.max(0, recipe.manaCost - rawMana);
if (missingMana > 0) {
canCraft = false;
}
return { canCraft, missingMaterials, missingMana };
}
// Get all recipes available based on unlocked blueprints
export function getAvailableRecipes(unlockedBlueprints: string[]): CraftingRecipe[] {
return unlockedBlueprints
.map(bpId => CRAFTING_RECIPES[bpId])
.filter(Boolean);
}

View File

@@ -0,0 +1,603 @@
// ─── Enchantment Effects Catalogue ────────────────────────────────────────────────
// All available enchantment effects that can be applied to equipment
import type { EquipmentCategory } from './equipment'
// Helper to define allowed equipment categories for each effect type
const ALL_CASTER: EquipmentCategory[] = ['caster']
const CASTER_AND_HANDS: EquipmentCategory[] = ['caster', 'hands']
const BODY_AND_SHIELD: EquipmentCategory[] = ['body', 'shield']
const CASTER_CATALYST_ACCESSORY: EquipmentCategory[] = ['caster', 'catalyst', 'accessory']
const MANA_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'accessory']
const UTILITY_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'hands', 'feet', 'accessory']
const ALL_EQUIPMENT: EquipmentCategory[] = ['caster', 'shield', 'catalyst', 'head', 'body', 'hands', 'feet', 'accessory']
export type EnchantmentEffectCategory = 'spell' | 'mana' | 'combat' | 'elemental' | 'defense' | 'utility' | 'special'
export interface EnchantmentEffectDef {
id: string;
name: string;
description: string;
category: EnchantmentEffectCategory;
baseCapacityCost: number;
maxStacks: number;
allowedEquipmentCategories: EquipmentCategory[];
effect: {
type: 'spell' | 'bonus' | 'multiplier' | 'special';
spellId?: string;
stat?: string;
value?: number;
specialId?: string;
};
}
export const ENCHANTMENT_EFFECTS: Record<string, EnchantmentEffectDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// SPELL EFFECTS - Only for CASTER equipment (staves, wands, rods, orbs)
// ═══════════════════════════════════════════════════════════════════════════
// Tier 0 - Basic Spells
spell_manaBolt: {
id: 'spell_manaBolt',
name: 'Mana Bolt',
description: 'Grants the ability to cast Mana Bolt (5 base damage, raw mana cost)',
category: 'spell',
baseCapacityCost: 50,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'manaBolt' }
},
spell_manaStrike: {
id: 'spell_manaStrike',
name: 'Mana Strike',
description: 'Grants the ability to cast Mana Strike (8 base damage, raw mana cost)',
category: 'spell',
baseCapacityCost: 40,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'manaStrike' }
},
// Tier 1 - Basic Elemental Spells
spell_fireball: {
id: 'spell_fireball',
name: 'Fireball',
description: 'Grants the ability to cast Fireball (15 fire damage)',
category: 'spell',
baseCapacityCost: 80,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'fireball' }
},
spell_emberShot: {
id: 'spell_emberShot',
name: 'Ember Shot',
description: 'Grants the ability to cast Ember Shot (10 fire damage, fast cast)',
category: 'spell',
baseCapacityCost: 60,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'emberShot' }
},
spell_waterJet: {
id: 'spell_waterJet',
name: 'Water Jet',
description: 'Grants the ability to cast Water Jet (12 water damage)',
category: 'spell',
baseCapacityCost: 70,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'waterJet' }
},
spell_iceShard: {
id: 'spell_iceShard',
name: 'Ice Shard',
description: 'Grants the ability to cast Ice Shard (14 water damage)',
category: 'spell',
baseCapacityCost: 75,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'iceShard' }
},
spell_gust: {
id: 'spell_gust',
name: 'Gust',
description: 'Grants the ability to cast Gust (10 air damage, fast cast)',
category: 'spell',
baseCapacityCost: 60,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'gust' }
},
spell_stoneBullet: {
id: 'spell_stoneBullet',
name: 'Stone Bullet',
description: 'Grants the ability to cast Stone Bullet (16 earth damage)',
category: 'spell',
baseCapacityCost: 80,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'stoneBullet' }
},
spell_lightLance: {
id: 'spell_lightLance',
name: 'Light Lance',
description: 'Grants the ability to cast Light Lance (18 light damage)',
category: 'spell',
baseCapacityCost: 95,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'lightLance' }
},
spell_shadowBolt: {
id: 'spell_shadowBolt',
name: 'Shadow Bolt',
description: 'Grants the ability to cast Shadow Bolt (16 dark damage)',
category: 'spell',
baseCapacityCost: 95,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'shadowBolt' }
},
spell_drain: {
id: 'spell_drain',
name: 'Drain',
description: 'Grants the ability to cast Drain (10 death damage, 20% lifesteal)',
category: 'spell',
baseCapacityCost: 85,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'drain' }
},
spell_lifeTap: {
id: 'spell_lifeTap',
name: 'Life Tap',
description: 'Grants the ability to cast Life Tap (8 life damage, 30% lifesteal)',
category: 'spell',
baseCapacityCost: 70,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'lifeTap' }
},
// Tier 2 - Advanced Spells
spell_inferno: {
id: 'spell_inferno',
name: 'Inferno',
description: 'Grants the ability to cast Inferno (60 fire damage)',
category: 'spell',
baseCapacityCost: 180,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'inferno' }
},
spell_tidalWave: {
id: 'spell_tidalWave',
name: 'Tidal Wave',
description: 'Grants the ability to cast Tidal Wave (55 water damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'tidalWave' }
},
spell_hurricane: {
id: 'spell_hurricane',
name: 'Hurricane',
description: 'Grants the ability to cast Hurricane (50 air damage)',
category: 'spell',
baseCapacityCost: 170,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'hurricane' }
},
spell_earthquake: {
id: 'spell_earthquake',
name: 'Earthquake',
description: 'Grants the ability to cast Earthquake (70 earth damage)',
category: 'spell',
baseCapacityCost: 200,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'earthquake' }
},
spell_solarFlare: {
id: 'spell_solarFlare',
name: 'Solar Flare',
description: 'Grants the ability to cast Solar Flare (65 light damage)',
category: 'spell',
baseCapacityCost: 190,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'solarFlare' }
},
spell_voidRift: {
id: 'spell_voidRift',
name: 'Void Rift',
description: 'Grants the ability to cast Void Rift (55 dark damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'voidRift' }
},
// Additional Tier 1 Spells
spell_windSlash: {
id: 'spell_windSlash',
name: 'Wind Slash',
description: 'Grants the ability to cast Wind Slash (12 air damage)',
category: 'spell',
baseCapacityCost: 72,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'windSlash' }
},
spell_rockSpike: {
id: 'spell_rockSpike',
name: 'Rock Spike',
description: 'Grants the ability to cast Rock Spike (18 earth damage)',
category: 'spell',
baseCapacityCost: 88,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'rockSpike' }
},
spell_radiance: {
id: 'spell_radiance',
name: 'Radiance',
description: 'Grants the ability to cast Radiance (14 light damage)',
category: 'spell',
baseCapacityCost: 80,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'radiance' }
},
spell_darkPulse: {
id: 'spell_darkPulse',
name: 'Dark Pulse',
description: 'Grants the ability to cast Dark Pulse (12 dark damage)',
category: 'spell',
baseCapacityCost: 68,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'darkPulse' }
},
// Additional Tier 2 Spells
spell_flameWave: {
id: 'spell_flameWave',
name: 'Flame Wave',
description: 'Grants the ability to cast Flame Wave (45 fire damage)',
category: 'spell',
baseCapacityCost: 165,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'flameWave' }
},
spell_iceStorm: {
id: 'spell_iceStorm',
name: 'Ice Storm',
description: 'Grants the ability to cast Ice Storm (50 water damage)',
category: 'spell',
baseCapacityCost: 170,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'iceStorm' }
},
spell_windBlade: {
id: 'spell_windBlade',
name: 'Wind Blade',
description: 'Grants the ability to cast Wind Blade (40 air damage)',
category: 'spell',
baseCapacityCost: 155,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'windBlade' }
},
spell_stoneBarrage: {
id: 'spell_stoneBarrage',
name: 'Stone Barrage',
description: 'Grants the ability to cast Stone Barrage (55 earth damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'stoneBarrage' }
},
spell_divineSmite: {
id: 'spell_divineSmite',
name: 'Divine Smite',
description: 'Grants the ability to cast Divine Smite (55 light damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'divineSmite' }
},
spell_shadowStorm: {
id: 'spell_shadowStorm',
name: 'Shadow Storm',
description: 'Grants the ability to cast Shadow Storm (48 dark damage)',
category: 'spell',
baseCapacityCost: 168,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'shadowStorm' }
},
// Tier 3 - Master Spells
spell_pyroclasm: {
id: 'spell_pyroclasm',
name: 'Pyroclasm',
description: 'Grants the ability to cast Pyroclasm (250 fire damage)',
category: 'spell',
baseCapacityCost: 400,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'pyroclasm' }
},
spell_tsunami: {
id: 'spell_tsunami',
name: 'Tsunami',
description: 'Grants the ability to cast Tsunami (220 water damage)',
category: 'spell',
baseCapacityCost: 380,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'tsunami' }
},
spell_meteorStrike: {
id: 'spell_meteorStrike',
name: 'Meteor Strike',
description: 'Grants the ability to cast Meteor Strike (280 earth damage)',
category: 'spell',
baseCapacityCost: 420,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'meteorStrike' }
},
// ═══════════════════════════════════════════════════════════════════════════
// MANA EFFECTS - Boost mana capacity and regeneration
// ═══════════════════════════════════════════════════════════════════════════
mana_cap_50: {
id: 'mana_cap_50',
name: 'Mana Reserve',
description: '+50 maximum mana',
category: 'mana',
baseCapacityCost: 20,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'maxMana', value: 50 }
},
mana_cap_100: {
id: 'mana_cap_100',
name: 'Mana Reservoir',
description: '+100 maximum mana',
category: 'mana',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'maxMana', value: 100 }
},
mana_regen_1: {
id: 'mana_regen_1',
name: 'Trickle',
description: '+1 mana per hour regeneration',
category: 'mana',
baseCapacityCost: 15,
maxStacks: 5,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'regen', value: 1 }
},
mana_regen_2: {
id: 'mana_regen_2',
name: 'Stream',
description: '+2 mana per hour regeneration',
category: 'mana',
baseCapacityCost: 28,
maxStacks: 4,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'regen', value: 2 }
},
mana_regen_5: {
id: 'mana_regen_5',
name: 'River',
description: '+5 mana per hour regeneration',
category: 'mana',
baseCapacityCost: 50,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'regen', value: 5 }
},
click_mana_1: {
id: 'click_mana_1',
name: 'Mana Tap',
description: '+1 mana per click',
category: 'mana',
baseCapacityCost: 20,
maxStacks: 5,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'clickMana', value: 1 }
},
click_mana_3: {
id: 'click_mana_3',
name: 'Mana Surge',
description: '+3 mana per click',
category: 'mana',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'clickMana', value: 3 }
},
// ═══════════════════════════════════════════════════════════════════════════
// COMBAT EFFECTS - Damage and attack enhancements
// ═══════════════════════════════════════════════════════════════════════════
damage_5: {
id: 'damage_5',
name: 'Minor Power',
description: '+5 base damage',
category: 'combat',
baseCapacityCost: 15,
maxStacks: 5,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'bonus', stat: 'baseDamage', value: 5 }
},
damage_10: {
id: 'damage_10',
name: 'Moderate Power',
description: '+10 base damage',
category: 'combat',
baseCapacityCost: 28,
maxStacks: 4,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'bonus', stat: 'baseDamage', value: 10 }
},
damage_pct_10: {
id: 'damage_pct_10',
name: 'Amplification',
description: '+10% damage',
category: 'combat',
baseCapacityCost: 30,
maxStacks: 3,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'multiplier', stat: 'baseDamage', value: 1.10 }
},
crit_5: {
id: 'crit_5',
name: 'Sharp Edge',
description: '+5% critical hit chance',
category: 'combat',
baseCapacityCost: 20,
maxStacks: 4,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'bonus', stat: 'critChance', value: 0.05 }
},
attack_speed_10: {
id: 'attack_speed_10',
name: 'Swift Casting',
description: '+10% attack speed',
category: 'combat',
baseCapacityCost: 22,
maxStacks: 4,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'multiplier', stat: 'attackSpeed', value: 1.10 }
},
// ═══════════════════════════════════════════════════════════════════════════
// UTILITY EFFECTS - Study speed, insight, meditation
// ═══════════════════════════════════════════════════════════════════════════
meditate_10: {
id: 'meditate_10',
name: 'Meditative Focus',
description: '+10% meditation efficiency',
category: 'utility',
baseCapacityCost: 18,
maxStacks: 5,
allowedEquipmentCategories: ['head', 'body', 'accessory'],
effect: { type: 'multiplier', stat: 'meditationEfficiency', value: 1.10 }
},
study_10: {
id: 'study_10',
name: 'Quick Study',
description: '+10% study speed',
category: 'utility',
baseCapacityCost: 22,
maxStacks: 4,
allowedEquipmentCategories: UTILITY_EQUIPMENT,
effect: { type: 'multiplier', stat: 'studySpeed', value: 1.10 }
},
insight_5: {
id: 'insight_5',
name: 'Insightful',
description: '+5% insight gain',
category: 'utility',
baseCapacityCost: 25,
maxStacks: 4,
allowedEquipmentCategories: ['head', 'accessory'],
effect: { type: 'multiplier', stat: 'insightGain', value: 1.05 }
},
// ═══════════════════════════════════════════════════════════════════════════
// SPECIAL EFFECTS - Unique and powerful effects
// ═══════════════════════════════════════════════════════════════════════════
spell_echo_10: {
id: 'spell_echo_10',
name: 'Echo Chamber',
description: '10% chance to cast a spell twice',
category: 'special',
baseCapacityCost: 60,
maxStacks: 2,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'special', specialId: 'spellEcho10' }
},
lifesteal_5: {
id: 'lifesteal_5',
name: 'Siphoning',
description: '5% of damage dealt is returned as mana',
category: 'special',
baseCapacityCost: 45,
maxStacks: 2,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'special', specialId: 'lifesteal5' }
},
guardian_dmg_10: {
id: 'guardian_dmg_10',
name: 'Bane',
description: '+10% damage to guardians',
category: 'special',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: CASTER_CATALYST_ACCESSORY,
effect: { type: 'multiplier', stat: 'guardianDamage', value: 1.10 }
},
overpower_80: {
id: 'overpower_80',
name: 'Overpower',
description: '+50% damage when mana above 80%',
category: 'special',
baseCapacityCost: 55,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'special', specialId: 'overpower' }
},
};
// ─── Helper Functions ────────────────────────────────────────────────────────────
export function getEnchantmentEffect(id: string): EnchantmentEffectDef | undefined {
return ENCHANTMENT_EFFECTS[id];
}
export function getEffectsForEquipment(equipmentCategory: EquipmentCategory): EnchantmentEffectDef[] {
return Object.values(ENCHANTMENT_EFFECTS).filter(effect =>
effect.allowedEquipmentCategories.includes(equipmentCategory)
);
}
export function canApplyEffect(effectId: string, equipmentCategory: EquipmentCategory): boolean {
const effect = ENCHANTMENT_EFFECTS[effectId];
if (!effect) return false;
return effect.allowedEquipmentCategories.includes(equipmentCategory);
}
export function calculateEffectCapacityCost(effectId: string, stacks: number, efficiencyBonus: number = 0): number {
const effect = ENCHANTMENT_EFFECTS[effectId];
if (!effect) return 0;
let totalCost = 0;
for (let i = 0; i < stacks; i++) {
// Each additional stack costs 20% more
const stackMultiplier = 1 + (i * 0.2);
totalCost += effect.baseCapacityCost * stackMultiplier;
}
// Apply efficiency bonus (reduces cost)
return Math.floor(totalCost * (1 - efficiencyBonus));
}

411
src/lib/game/data/equipment.ts Executable file
View File

@@ -0,0 +1,411 @@
// ─── Equipment Types ─────────────────────────────────────────────────────────
export type EquipmentSlot = 'mainHand' | 'offHand' | 'head' | 'body' | 'hands' | 'feet' | 'accessory1' | 'accessory2';
export type EquipmentCategory = 'caster' | 'shield' | 'catalyst' | 'head' | 'body' | 'hands' | 'feet' | 'accessory';
// All equipment slots in order
export const EQUIPMENT_SLOTS: EquipmentSlot[] = ['mainHand', 'offHand', 'head', 'body', 'hands', 'feet', 'accessory1', 'accessory2'];
export interface EquipmentType {
id: string;
name: string;
category: EquipmentCategory;
slot: EquipmentSlot;
baseCapacity: number;
description: string;
}
// ─── Equipment Types Definition ─────────────────────────────────────────────
export const EQUIPMENT_TYPES: Record<string, EquipmentType> = {
// ─── Main Hand - Casters ─────────────────────────────────────────────────
basicStaff: {
id: 'basicStaff',
name: 'Basic Staff',
category: 'caster',
slot: 'mainHand',
baseCapacity: 50,
description: 'A simple wooden staff, basic but reliable for channeling mana.',
},
apprenticeWand: {
id: 'apprenticeWand',
name: 'Apprentice Wand',
category: 'caster',
slot: 'mainHand',
baseCapacity: 35,
description: 'A lightweight wand favored by apprentices. Lower capacity but faster to prepare.',
},
oakStaff: {
id: 'oakStaff',
name: 'Oak Staff',
category: 'caster',
slot: 'mainHand',
baseCapacity: 65,
description: 'A sturdy oak staff with decent mana capacity.',
},
crystalWand: {
id: 'crystalWand',
name: 'Crystal Wand',
category: 'caster',
slot: 'mainHand',
baseCapacity: 45,
description: 'A wand tipped with a small crystal. Excellent for elemental enchantments.',
},
arcanistStaff: {
id: 'arcanistStaff',
name: 'Arcanist Staff',
category: 'caster',
slot: 'mainHand',
baseCapacity: 80,
description: 'A staff designed for advanced spellcasters. High capacity for complex enchantments.',
},
battlestaff: {
id: 'battlestaff',
name: 'Battlestaff',
category: 'caster',
slot: 'mainHand',
baseCapacity: 70,
description: 'A reinforced staff suitable for both casting and combat.',
},
// ─── Main Hand - Catalysts ────────────────────────────────────────────────
basicCatalyst: {
id: 'basicCatalyst',
name: 'Basic Catalyst',
category: 'catalyst',
slot: 'mainHand',
baseCapacity: 40,
description: 'A simple catalyst for amplifying magical effects.',
},
fireCatalyst: {
id: 'fireCatalyst',
name: 'Fire Catalyst',
category: 'catalyst',
slot: 'mainHand',
baseCapacity: 55,
description: 'A catalyst attuned to fire magic. Enhances fire enchantments.',
},
voidCatalyst: {
id: 'voidCatalyst',
name: 'Void Catalyst',
category: 'catalyst',
slot: 'mainHand',
baseCapacity: 75,
description: 'A rare catalyst touched by void energy. High capacity but volatile.',
},
// ─── Off Hand - Shields ───────────────────────────────────────────────────
basicShield: {
id: 'basicShield',
name: 'Basic Shield',
category: 'shield',
slot: 'offHand',
baseCapacity: 40,
description: 'A simple wooden shield. Provides basic protection.',
},
reinforcedShield: {
id: 'reinforcedShield',
name: 'Reinforced Shield',
category: 'shield',
slot: 'offHand',
baseCapacity: 55,
description: 'A metal-reinforced shield with enhanced durability and capacity.',
},
runicShield: {
id: 'runicShield',
name: 'Runic Shield',
category: 'shield',
slot: 'offHand',
baseCapacity: 70,
description: 'A shield engraved with protective runes. Excellent for defensive enchantments.',
},
manaShield: {
id: 'manaShield',
name: 'Mana Shield',
category: 'shield',
slot: 'offHand',
baseCapacity: 60,
description: 'A crystalline shield that can store and reflect mana.',
},
// ─── Head ─────────────────────────────────────────────────────────────────
clothHood: {
id: 'clothHood',
name: 'Cloth Hood',
category: 'head',
slot: 'head',
baseCapacity: 25,
description: 'A simple cloth hood. Minimal protection but comfortable.',
},
apprenticeCap: {
id: 'apprenticeCap',
name: 'Apprentice Cap',
category: 'head',
slot: 'head',
baseCapacity: 30,
description: 'The traditional cap of magic apprentices.',
},
wizardHat: {
id: 'wizardHat',
name: 'Wizard Hat',
category: 'head',
slot: 'head',
baseCapacity: 45,
description: 'A classic pointed wizard hat. Decent capacity for headwear.',
},
arcanistCirclet: {
id: 'arcanistCirclet',
name: 'Arcanist Circlet',
category: 'head',
slot: 'head',
baseCapacity: 40,
description: 'A silver circlet worn by accomplished arcanists.',
},
battleHelm: {
id: 'battleHelm',
name: 'Battle Helm',
category: 'head',
slot: 'head',
baseCapacity: 50,
description: 'A sturdy helm for battle mages.',
},
// ─── Body ────────────────────────────────────────────────────────────────
civilianShirt: {
id: 'civilianShirt',
name: 'Civilian Shirt',
category: 'body',
slot: 'body',
baseCapacity: 30,
description: 'A plain shirt with minimal magical properties.',
},
apprenticeRobe: {
id: 'apprenticeRobe',
name: 'Apprentice Robe',
category: 'body',
slot: 'body',
baseCapacity: 45,
description: 'The standard robe for magic apprentices.',
},
scholarRobe: {
id: 'scholarRobe',
name: 'Scholar Robe',
category: 'body',
slot: 'body',
baseCapacity: 55,
description: 'A robe worn by scholars and researchers.',
},
battleRobe: {
id: 'battleRobe',
name: 'Battle Robe',
category: 'body',
slot: 'body',
baseCapacity: 65,
description: 'A reinforced robe designed for combat mages.',
},
arcanistRobe: {
id: 'arcanistRobe',
name: 'Arcanist Robe',
category: 'body',
slot: 'body',
baseCapacity: 80,
description: 'An ornate robe for master arcanists. High capacity for body armor.',
},
// ─── Hands ───────────────────────────────────────────────────────────────
civilianGloves: {
id: 'civilianGloves',
name: 'Civilian Gloves',
category: 'hands',
slot: 'hands',
baseCapacity: 20,
description: 'Simple cloth gloves. Minimal magical capacity.',
},
apprenticeGloves: {
id: 'apprenticeGloves',
name: 'Apprentice Gloves',
category: 'hands',
slot: 'hands',
baseCapacity: 30,
description: 'Basic gloves for handling magical components.',
},
spellweaveGloves: {
id: 'spellweaveGloves',
name: 'Spellweave Gloves',
category: 'hands',
slot: 'hands',
baseCapacity: 40,
description: 'Gloves woven with mana-conductive threads.',
},
combatGauntlets: {
id: 'combatGauntlets',
name: 'Combat Gauntlets',
category: 'hands',
slot: 'hands',
baseCapacity: 35,
description: 'Armored gauntlets for battle mages.',
},
// ─── Feet ────────────────────────────────────────────────────────────────
civilianShoes: {
id: 'civilianShoes',
name: 'Civilian Shoes',
category: 'feet',
slot: 'feet',
baseCapacity: 15,
description: 'Simple leather shoes. No special properties.',
},
apprenticeBoots: {
id: 'apprenticeBoots',
name: 'Apprentice Boots',
category: 'feet',
slot: 'feet',
baseCapacity: 25,
description: 'Basic boots for magic students.',
},
travelerBoots: {
id: 'travelerBoots',
name: 'Traveler Boots',
category: 'feet',
slot: 'feet',
baseCapacity: 30,
description: 'Comfortable boots for long journeys.',
},
battleBoots: {
id: 'battleBoots',
name: 'Battle Boots',
category: 'feet',
slot: 'feet',
baseCapacity: 35,
description: 'Sturdy boots for combat situations.',
},
// ─── Accessories ────────────────────────────────────────────────────────
copperRing: {
id: 'copperRing',
name: 'Copper Ring',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 15,
description: 'A simple copper ring. Basic capacity for accessories.',
},
silverRing: {
id: 'silverRing',
name: 'Silver Ring',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 25,
description: 'A silver ring with decent magical conductivity.',
},
goldRing: {
id: 'goldRing',
name: 'Gold Ring',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 35,
description: 'A gold ring with excellent magical properties.',
},
signetRing: {
id: 'signetRing',
name: 'Signet Ring',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 30,
description: 'A ring bearing a magical sigil.',
},
copperAmulet: {
id: 'copperAmulet',
name: 'Copper Amulet',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 20,
description: 'A simple copper amulet on a leather cord.',
},
silverAmulet: {
id: 'silverAmulet',
name: 'Silver Amulet',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 30,
description: 'A silver amulet with a small gem.',
},
crystalPendant: {
id: 'crystalPendant',
name: 'Crystal Pendant',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 45,
description: 'A pendant with a mana-infused crystal.',
},
manaBrooch: {
id: 'manaBrooch',
name: 'Mana Brooch',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 40,
description: 'A decorative brooch that can hold enchantments.',
},
arcanistPendant: {
id: 'arcanistPendant',
name: 'Arcanist Pendant',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 55,
description: 'A powerful pendant worn by master arcanists.',
},
voidTouchedRing: {
id: 'voidTouchedRing',
name: 'Void-Touched Ring',
category: 'accessory',
slot: 'accessory1',
baseCapacity: 50,
description: 'A ring corrupted by void energy. High capacity but risky.',
},
};
// ─── Helper Functions ─────────────────────────────────────────────────────────
export function getEquipmentType(id: string): EquipmentType | undefined {
return EQUIPMENT_TYPES[id];
}
export function getEquipmentByCategory(category: EquipmentCategory): EquipmentType[] {
return Object.values(EQUIPMENT_TYPES).filter(e => e.category === category);
}
export function getEquipmentBySlot(slot: EquipmentSlot): EquipmentType[] {
return Object.values(EQUIPMENT_TYPES).filter(e => e.slot === slot);
}
export function getAllEquipmentTypes(): EquipmentType[] {
return Object.values(EQUIPMENT_TYPES);
}
// Get valid slots for a category
export function getValidSlotsForCategory(category: EquipmentCategory): EquipmentSlot[] {
switch (category) {
case 'caster':
case 'catalyst':
return ['mainHand'];
case 'shield':
return ['offHand'];
case 'head':
return ['head'];
case 'body':
return ['body'];
case 'hands':
return ['hands'];
case 'feet':
return ['feet'];
case 'accessory':
return ['accessory1', 'accessory2'];
default:
return [];
}
}
// Check if an equipment type can be equipped in a specific slot
export function canEquipInSlot(equipmentType: EquipmentType, slot: EquipmentSlot): boolean {
const validSlots = getValidSlotsForCategory(equipmentType.category);
return validSlots.includes(slot);
}

242
src/lib/game/data/loot-drops.ts Executable file
View File

@@ -0,0 +1,242 @@
// ─── Loot Drop Definitions ─────────────────────────────────────────────────────
import type { LootDrop } from '../types';
export const LOOT_DROPS: Record<string, LootDrop> = {
// ─── Materials (used for crafting) ───
manaCrystalDust: {
id: 'manaCrystalDust',
name: 'Mana Crystal Dust',
rarity: 'common',
type: 'material',
minFloor: 1,
dropChance: 0.15,
},
arcaneShard: {
id: 'arcaneShard',
name: 'Arcane Shard',
rarity: 'uncommon',
type: 'material',
minFloor: 10,
dropChance: 0.10,
},
elementalCore: {
id: 'elementalCore',
name: 'Elemental Core',
rarity: 'rare',
type: 'material',
minFloor: 25,
dropChance: 0.08,
},
voidEssence: {
id: 'voidEssence',
name: 'Void Essence',
rarity: 'epic',
type: 'material',
minFloor: 50,
dropChance: 0.05,
guardianOnly: true,
},
celestialFragment: {
id: 'celestialFragment',
name: 'Celestial Fragment',
rarity: 'legendary',
type: 'material',
minFloor: 75,
dropChance: 0.02,
guardianOnly: true,
},
// ─── Elemental Essence (grants elemental mana) ───
fireEssenceDrop: {
id: 'fireEssenceDrop',
name: 'Fire Essence',
rarity: 'uncommon',
type: 'essence',
minFloor: 5,
dropChance: 0.12,
amount: { min: 5, max: 15 },
},
waterEssenceDrop: {
id: 'waterEssenceDrop',
name: 'Water Essence',
rarity: 'uncommon',
type: 'essence',
minFloor: 5,
dropChance: 0.12,
amount: { min: 5, max: 15 },
},
airEssenceDrop: {
id: 'airEssenceDrop',
name: 'Air Essence',
rarity: 'uncommon',
type: 'essence',
minFloor: 5,
dropChance: 0.12,
amount: { min: 5, max: 15 },
},
earthEssenceDrop: {
id: 'earthEssenceDrop',
name: 'Earth Essence',
rarity: 'uncommon',
type: 'essence',
minFloor: 5,
dropChance: 0.12,
amount: { min: 5, max: 15 },
},
lightEssenceDrop: {
id: 'lightEssenceDrop',
name: 'Light Essence',
rarity: 'rare',
type: 'essence',
minFloor: 20,
dropChance: 0.08,
amount: { min: 3, max: 10 },
},
darkEssenceDrop: {
id: 'darkEssenceDrop',
name: 'Dark Essence',
rarity: 'rare',
type: 'essence',
minFloor: 20,
dropChance: 0.08,
amount: { min: 3, max: 10 },
},
lifeEssenceDrop: {
id: 'lifeEssenceDrop',
name: 'Life Essence',
rarity: 'epic',
type: 'essence',
minFloor: 40,
dropChance: 0.05,
amount: { min: 2, max: 8 },
},
deathEssenceDrop: {
id: 'deathEssenceDrop',
name: 'Death Essence',
rarity: 'epic',
type: 'essence',
minFloor: 40,
dropChance: 0.05,
amount: { min: 2, max: 8 },
},
// ─── Raw Mana Drops ───
manaOrb: {
id: 'manaOrb',
name: 'Mana Orb',
rarity: 'common',
type: 'gold', // Uses gold type but gives raw mana
minFloor: 1,
dropChance: 0.20,
amount: { min: 10, max: 50 },
},
greaterManaOrb: {
id: 'greaterManaOrb',
name: 'Greater Mana Orb',
rarity: 'uncommon',
type: 'gold',
minFloor: 15,
dropChance: 0.10,
amount: { min: 50, max: 150 },
},
supremeManaOrb: {
id: 'supremeManaOrb',
name: 'Supreme Mana Orb',
rarity: 'rare',
type: 'gold',
minFloor: 35,
dropChance: 0.05,
amount: { min: 100, max: 500 },
},
// ─── Equipment Blueprints ───
staffBlueprint: {
id: 'staffBlueprint',
name: 'Staff Blueprint',
rarity: 'uncommon',
type: 'blueprint',
minFloor: 10,
dropChance: 0.03,
},
wandBlueprint: {
id: 'wandBlueprint',
name: 'Wand Blueprint',
rarity: 'rare',
type: 'blueprint',
minFloor: 20,
dropChance: 0.02,
},
robeBlueprint: {
id: 'robeBlueprint',
name: 'Mage Robe Blueprint',
rarity: 'rare',
type: 'blueprint',
minFloor: 25,
dropChance: 0.02,
},
artifactBlueprint: {
id: 'artifactBlueprint',
name: 'Artifact Blueprint',
rarity: 'legendary',
type: 'blueprint',
minFloor: 60,
dropChance: 0.01,
guardianOnly: true,
},
};
// Rarity colors for UI
export const RARITY_COLORS: Record<string, { color: string; glow: string }> = {
common: { color: '#9CA3AF', glow: '#9CA3AF40' },
uncommon: { color: '#22C55E', glow: '#22C55E40' },
rare: { color: '#3B82F6', glow: '#3B82F640' },
epic: { color: '#A855F7', glow: '#A855F740' },
legendary: { color: '#F59E0B', glow: '#F59E0B60' },
};
// Get loot drops available at a given floor
export function getAvailableDrops(floor: number, isGuardian: boolean): LootDrop[] {
return Object.values(LOOT_DROPS).filter(drop => {
if (drop.minFloor > floor) return false;
if (drop.guardianOnly && !isGuardian) return false;
return true;
});
}
// Roll for loot drops
export function rollLootDrops(
floor: number,
isGuardian: boolean,
luckBonus: number = 0
): Array<{ drop: LootDrop; amount: number }> {
const available = getAvailableDrops(floor, isGuardian);
const drops: Array<{ drop: LootDrop; amount: number }> = [];
for (const drop of available) {
// Calculate adjusted drop chance
let chance = drop.dropChance;
chance *= (1 + luckBonus); // Apply luck bonus
// Guardian floors have 2x drop rate
if (isGuardian) chance *= 2;
// Cap at 50% for any single drop
chance = Math.min(0.5, chance);
if (Math.random() < chance) {
let amount = 1;
// For gold/essence types, roll amount
if (drop.amount) {
amount = Math.floor(
Math.random() * (drop.amount.max - drop.amount.min + 1) + drop.amount.min
);
}
drops.push({ drop, amount });
}
}
return drops;
}

215
src/lib/game/effects.ts Executable file
View File

@@ -0,0 +1,215 @@
// ─── 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)
// - Direct skill bonuses (from skill levels)
import type { GameState, EquipmentInstance } from './types';
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects';
// Re-export for convenience
export { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects };
// ─── Equipment Effect Computation ────────────────────────────────────────────────
/**
* Compute all effects from equipped enchantments
*/
export function computeEquipmentEffects(
equipmentInstances: Record<string, EquipmentInstance>,
equippedInstances: Record<string, string | null>
): {
bonuses: Record<string, number>;
multipliers: Record<string, number>;
specials: Set<string>;
} {
const bonuses: Record<string, number> = {};
const multipliers: Record<string, number> = {};
const specials = new Set<string>();
// Iterate through all equipped items
for (const instanceId of Object.values(equippedInstances)) {
if (!instanceId) continue;
const instance = equipmentInstances[instanceId];
if (!instance) continue;
// Process each enchantment on the item
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (!effectDef) continue;
const { effect } = effectDef;
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;
} 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
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;
}
} else if (effect.type === 'special' && effect.specialId) {
specials.add(effect.specialId);
}
}
}
return { bonuses, multipliers, specials };
}
// ─── Unified Computed Effects ────────────────────────────────────────────────────
export interface UnifiedEffects extends ComputedEffects {
// Equipment bonuses
equipmentBonuses: Record<string, number>;
equipmentMultipliers: Record<string, number>;
equipmentSpecials: Set<string>;
}
/**
* Compute all effects from all sources: skill upgrades + equipment enchantments
*/
export function computeAllEffects(
skillUpgrades: Record<string, string[]>,
skillTiers: Record<string, number>,
equipmentInstances: Record<string, EquipmentInstance>,
equippedInstances: Record<string, string | null>
): UnifiedEffects {
// Get skill upgrade effects
const upgradeEffects = computeEffects(skillUpgrades, skillTiers);
// Get equipment effects
const equipmentEffects = computeEquipmentEffects(equipmentInstances, equippedInstances);
// Merge the effects
const merged: UnifiedEffects = {
...upgradeEffects,
// Merge equipment bonuses with upgrade bonuses
maxManaBonus: upgradeEffects.maxManaBonus + (equipmentEffects.bonuses.maxMana || 0),
regenBonus: upgradeEffects.regenBonus + (equipmentEffects.bonuses.regen || 0),
clickManaBonus: upgradeEffects.clickManaBonus + (equipmentEffects.bonuses.clickMana || 0),
baseDamageBonus: upgradeEffects.baseDamageBonus + (equipmentEffects.bonuses.baseDamage || 0),
elementCapBonus: upgradeEffects.elementCapBonus + (equipmentEffects.bonuses.elementCap || 0),
// Merge equipment multipliers with upgrade multipliers
maxManaMultiplier: upgradeEffects.maxManaMultiplier * (equipmentEffects.multipliers.maxMana || 1),
regenMultiplier: upgradeEffects.regenMultiplier * (equipmentEffects.multipliers.regen || 1),
clickManaMultiplier: upgradeEffects.clickManaMultiplier * (equipmentEffects.multipliers.clickMana || 1),
baseDamageMultiplier: upgradeEffects.baseDamageMultiplier * (equipmentEffects.multipliers.baseDamage || 1),
attackSpeedMultiplier: upgradeEffects.attackSpeedMultiplier * (equipmentEffects.multipliers.attackSpeed || 1),
elementCapMultiplier: upgradeEffects.elementCapMultiplier * (equipmentEffects.multipliers.elementCap || 1),
// Merge specials
specials: new Set([...upgradeEffects.specials, ...equipmentEffects.specials]),
// Store equipment effects for reference
equipmentBonuses: equipmentEffects.bonuses,
equipmentMultipliers: equipmentEffects.multipliers,
equipmentSpecials: equipmentEffects.specials,
};
// Handle special stats that are equipment-only
if (equipmentEffects.bonuses.critChance) {
merged.critChanceBonus += equipmentEffects.bonuses.critChance;
}
if (equipmentEffects.bonuses.meditationEfficiency) {
// This is a multiplier in equipment, convert to additive for simplicity
// Equipment gives +10% per stack, so add it to the base
merged.meditationEfficiency *= (equipmentEffects.multipliers.meditationEfficiency || 1);
}
if (equipmentEffects.bonuses.studySpeed) {
merged.studySpeedMultiplier *= (equipmentEffects.multipliers.studySpeed || 1);
}
if (equipmentEffects.bonuses.insightGain) {
// Store separately - insight multiplier
(merged as any).insightGainMultiplier = (equipmentEffects.multipliers.insightGain || 1);
}
if (equipmentEffects.bonuses.guardianDamage) {
(merged as any).guardianDamageMultiplier = (equipmentEffects.multipliers.guardianDamage || 1);
}
return merged;
}
/**
* Helper to get unified effects from game state
*/
export function getUnifiedEffects(state: Pick<GameState, 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>): UnifiedEffects {
return computeAllEffects(
state.skillUpgrades || {},
state.skillTiers || {},
state.equipmentInstances,
state.equippedInstances
);
}
// ─── Stat Computation with All Effects ───────────────────────────────────────────
/**
* Compute max mana with all effect sources
*/
export function computeTotalMaxMana(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
effects?: UnifiedEffects
): number {
const pu = state.prestigeUpgrades;
const base =
100 +
(state.skills.manaWell || 0) * 100 +
(pu.manaWell || 0) * 500;
if (!effects) {
effects = getUnifiedEffects(state);
}
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
}
/**
* Compute regen with all effect sources
*/
export function computeTotalRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
effects?: UnifiedEffects
): number {
const pu = state.prestigeUpgrades;
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
const base =
2 +
(state.skills.manaFlow || 0) * 1 +
(state.skills.manaSpring || 0) * 2 +
(pu.manaFlow || 0) * 0.5;
let regen = base * temporalBonus;
if (!effects) {
effects = getUnifiedEffects(state);
}
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
return regen;
}
/**
* Compute click mana with all effect sources
*/
export function computeTotalClickMana(
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
effects?: UnifiedEffects
): number {
const base =
1 +
(state.skills.manaTap || 0) * 1 +
(state.skills.manaSurge || 0) * 3;
if (!effects) {
effects = getUnifiedEffects(state as any);
}
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
}

46
src/lib/game/formatting.ts Executable file
View File

@@ -0,0 +1,46 @@
// ─── Shared Formatting Utilities ─────────────────────────────────────────────────
// Utility functions for consistent formatting across components
import { ELEMENTS } from '@/lib/game/constants';
import type { SpellCost } from '@/lib/game/types';
// Re-export number formatting functions from computed-stats.ts
export { fmt, fmtDec } from './computed-stats';
/**
* Format a spell cost for display
*/
export function formatSpellCost(cost: SpellCost): string {
if (cost.type === 'raw') {
return `${cost.amount} raw`;
}
const elemDef = ELEMENTS[cost.element || ''];
return `${cost.amount} ${elemDef?.sym || '?'}`;
}
/**
* Get the display color for a spell cost
*/
export function getSpellCostColor(cost: SpellCost): string {
if (cost.type === 'raw') {
return '#60A5FA'; // Blue for raw mana
}
return ELEMENTS[cost.element || '']?.color || '#9CA3AF';
}
/**
* Format study time in hours to human-readable string
*/
export function formatStudyTime(hours: number): string {
if (hours < 1) return `${Math.round(hours * 60)}m`;
return `${hours.toFixed(1)}h`;
}
/**
* Format time (hour of day) to HH:MM format
*/
export function formatHour(hour: number): string {
const h = Math.floor(hour);
const m = Math.floor((hour % 1) * 60);
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
}

View File

@@ -0,0 +1,63 @@
// ─── Navigation Slice ─────────────────────────────────────────────────────────
// Actions for floor navigation: climbing direction and manual floor changes
import type { GameState } from './types';
import { getFloorMaxHP } from './computed-stats';
// ─── Navigation Actions Interface ─────────────────────────────────────────────
export interface NavigationActions {
// Floor Navigation
setClimbDirection: (direction: 'up' | 'down') => void;
changeFloor: (direction: 'up' | 'down') => void;
}
// ─── Navigation Slice Factory ─────────────────────────────────────────────────
export function createNavigationSlice(
set: (partial: Partial<GameState> | ((state: GameState) => Partial<GameState>)) => void,
get: () => GameState
): NavigationActions {
return {
// Set the climbing direction (up or down)
setClimbDirection: (direction: 'up' | 'down') => {
set({ climbDirection: direction });
},
// Manually change floors by one
changeFloor: (direction: 'up' | 'down') => {
const state = get();
const currentFloor = state.currentFloor;
// Calculate next floor
const nextFloor = direction === 'up'
? Math.min(currentFloor + 1, 100)
: Math.max(currentFloor - 1, 1);
// Can't stay on same floor
if (nextFloor === currentFloor) return;
// Mark current floor as cleared (it will respawn when we come back)
const clearedFloors = { ...state.clearedFloors };
clearedFloors[currentFloor] = true;
// Check if next floor was cleared (needs respawn)
const nextFloorCleared = clearedFloors[nextFloor];
if (nextFloorCleared) {
// Respawn the floor
delete clearedFloors[nextFloor];
}
set({
currentFloor: nextFloor,
floorMaxHP: getFloorMaxHP(nextFloor),
floorHP: getFloorMaxHP(nextFloor),
maxFloorReached: Math.max(state.maxFloorReached, nextFloor),
clearedFloors,
climbDirection: direction,
equipmentSpellStates: state.equipmentSpellStates.map(s => ({ ...s, castProgress: 0 })),
log: [`🚶 Moved to floor ${nextFloor}${nextFloorCleared ? ' (respawned)' : ''}.`, ...state.log.slice(0, 49)],
});
},
};
}

797
src/lib/game/skill-evolution.ts Executable file
View File

@@ -0,0 +1,797 @@
// ─── Skill Evolution System ───────────────────────────────────────────────────────
// Each base skill has 5 tiers of evolution
// At level 5 and 10, you choose 2 out of 4 upgrades
// At max level (10), you tier up to the next evolution
// Tier multiplier: each tier is 10x more powerful (so tier N level 1 = tier N-1 level 10)
import type { SkillDef, SkillUpgradeChoice, SkillEvolutionPath, SkillTierDef } from './types';
// ─── Upgrade Choice Definitions ───────────────────────────────────────────────────
// Mana Well Upgrades (Tiers 1-5)
// Mana Well: +100 max mana per level
const MANA_WELL_TIER1_UPGRADES_L5: SkillUpgradeChoice[] = [
{ id: 'mw_t1_l5_capacity', name: 'Expanded Capacity', desc: '+25% max mana bonus', milestone: 5, effect: { type: 'multiplier', stat: 'maxMana', value: 1.25 } },
{ id: 'mw_t1_l5_regen', name: 'Natural Spring', desc: '+0.5 regen per hour', milestone: 5, effect: { type: 'bonus', stat: 'regen', value: 0.5 } },
{ id: 'mw_t1_l5_threshold', name: 'Mana Threshold', desc: '+20% max mana but reduces natural regen by 10%', milestone: 5, effect: { type: 'special', specialId: 'manaThreshold', specialDesc: 'Trade regen for capacity' } },
{ id: 'mw_t1_l5_desperate', name: 'Desperate Wells', desc: '+50% regen when below 25% mana', milestone: 5, effect: { type: 'special', specialId: 'desperateWells', specialDesc: 'Emergency regen boost' } },
];
const MANA_WELL_TIER1_UPGRADES_L10: SkillUpgradeChoice[] = [
{ id: 'mw_t1_l10_echo', name: 'Mana Echo', desc: '10% chance to gain double mana from clicks', milestone: 10, effect: { type: 'special', specialId: 'manaEcho', specialDesc: 'Double mana chance' } },
{ id: 'mw_t1_l10_reserve', name: 'Emergency Reserve', desc: 'Keep 10% max mana when starting a new loop', milestone: 10, effect: { type: 'special', specialId: 'emergencyReserve', specialDesc: 'Keep mana on loop' } },
{ id: 'mw_t1_l10_efficiency', name: 'Mana Efficiency', desc: '-5% spell costs', milestone: 10, effect: { type: 'multiplier', stat: 'spellCost', value: 0.95 } },
{ id: 'mw_t1_l10_meditation', name: 'Deep Wellspring', desc: '+50% meditation efficiency', milestone: 10, effect: { type: 'multiplier', stat: 'meditationEfficiency', value: 1.5 } },
];
// Mana Flow Upgrades (+1 regen per level)
const MANA_FLOW_TIER1_UPGRADES_L5: SkillUpgradeChoice[] = [
{ id: 'mf_t1_l5_rapid', name: 'Rapid Flow', desc: '+25% regen speed', milestone: 5, effect: { type: 'multiplier', stat: 'regen', value: 1.25 } },
{ id: 'mf_t1_l5_steady', name: 'Steady Stream', desc: 'Regen never drops below base even with incursion', milestone: 5, effect: { type: 'special', specialId: 'steadyStream', specialDesc: 'Immune to regen reduction' } },
{ id: 'mf_t1_l5_cascade', name: 'Mana Cascade', desc: '+0.1 regen per 100 max mana', milestone: 5, effect: { type: 'special', specialId: 'manaCascade', specialDesc: 'Scaling regen' } },
{ id: 'mf_t1_l5_overflow', name: 'Mana Overflow', desc: 'Raw mana can exceed max by 20%', milestone: 5, effect: { type: 'special', specialId: 'manaOverflow', specialDesc: 'Mana overfill' } },
];
const MANA_FLOW_TIER1_UPGRADES_L10: SkillUpgradeChoice[] = [
{ id: 'mf_t1_l10_torrent', name: 'Mana Torrent', desc: '+50% regen when above 75% mana', milestone: 10, effect: { type: 'special', specialId: 'manaTorrent', specialDesc: 'High mana regen bonus' } },
{ id: 'mf_t1_l10_ambient', name: 'Ambient Absorption', desc: '+1 regen permanently (persists through loops)', milestone: 10, effect: { type: 'bonus', stat: 'permanentRegen', value: 1 } },
{ id: 'mf_t1_l10_surge', name: 'Flow Surge', desc: 'Clicks restore 2x regen for 1 hour', milestone: 10, effect: { type: 'special', specialId: 'flowSurge', specialDesc: 'Click boosts regen' } },
{ id: 'mf_t1_l10_mastery', name: 'Flow Mastery', desc: '+10% mana from all sources', milestone: 10, effect: { type: 'multiplier', stat: 'allManaSources', value: 1.1 } },
];
// Combat Training Upgrades (+5 base damage per level)
const COMBAT_TRAIN_TIER1_UPGRADES_L5: SkillUpgradeChoice[] = [
{ id: 'ct_t1_l5_power', name: 'Raw Power', desc: '+25% base damage', milestone: 5, effect: { type: 'multiplier', stat: 'baseDamage', value: 1.25 } },
{ id: 'ct_t1_l5_crit', name: 'Critical Eye', desc: '+10% critical hit chance', milestone: 5, effect: { type: 'bonus', stat: 'critChance', value: 10 } },
{ id: 'ct_t1_l5_firstStrike', name: 'Power Strike', desc: '+15% damage on first attack each floor', milestone: 5, effect: { type: 'special', specialId: 'firstStrike', specialDesc: 'Opening attack bonus' } },
{ id: 'ct_t1_l5_speed', name: 'Quick Strikes', desc: '+20% attack speed', milestone: 5, effect: { type: 'multiplier', stat: 'attackSpeed', value: 1.2 } },
];
const COMBAT_TRAIN_TIER1_UPGRADES_L10: SkillUpgradeChoice[] = [
{ id: 'ct_t1_l10_overpower', name: 'Overpower', desc: '+50% damage when mana above 80%', milestone: 10, effect: { type: 'special', specialId: 'overpower', specialDesc: 'High mana damage bonus' } },
{ id: 'ct_t1_l10_berserker', name: 'Berserker', desc: '+50% damage when below 50% mana', milestone: 10, effect: { type: 'special', specialId: 'berserker', specialDesc: 'Low mana damage bonus' } },
{ id: 'ct_t1_l10_combo', name: 'Combo Master', desc: 'Every 5th attack deals 3x damage', milestone: 10, effect: { type: 'special', specialId: 'comboMaster', specialDesc: 'Combo finisher' } },
{ id: 'ct_t1_l10_adrenaline', name: 'Adrenaline Rush', desc: 'Defeating an enemy restores 5% mana', milestone: 10, effect: { type: 'special', specialId: 'adrenalineRush', specialDesc: 'Kill restore' } },
];
// Quick Learner Upgrades (+10% study speed per level)
const QUICK_LEARNER_TIER1_UPGRADES_L5: SkillUpgradeChoice[] = [
{ id: 'ql_t1_l5_focus', name: 'Deep Focus', desc: '+25% study speed', milestone: 5, effect: { type: 'multiplier', stat: 'studySpeed', value: 1.25 } },
{ id: 'ql_t1_l5_recall', name: 'Quick Grasp', desc: '5% chance for double study progress per hour', milestone: 5, effect: { type: 'special', specialId: 'quickGrasp', specialDesc: 'Double study progress chance' } },
{ id: 'ql_t1_l5_mastery', name: 'Quick Mastery', desc: '-20% study time for final 3 levels', milestone: 5, effect: { type: 'special', specialId: 'quickMastery', specialDesc: 'Faster final levels' } },
{ id: 'ql_t1_l5_parallel', name: 'Parallel Study', desc: 'Can study 2 things at once at 50% speed each', milestone: 5, effect: { type: 'special', specialId: 'parallelStudy', specialDesc: 'Dual study' } },
];
const QUICK_LEARNER_TIER1_UPGRADES_L10: SkillUpgradeChoice[] = [
{ id: 'ql_t1_l10_concentration', name: 'Deep Concentration', desc: '+20% study speed when mana > 90%', milestone: 10, effect: { type: 'special', specialId: 'deepConcentration', specialDesc: 'High mana study bonus' } },
{ id: 'ql_t1_l10_momentum', name: 'Study Momentum', desc: '+5% study speed for each consecutive hour (max 50%)', milestone: 10, effect: { type: 'special', specialId: 'studyMomentum', specialDesc: 'Consecutive study bonus' } },
{ id: 'ql_t1_l10_echo', name: 'Knowledge Echo', desc: '10% chance to instantly complete study', milestone: 10, effect: { type: 'special', specialId: 'knowledgeEcho', specialDesc: 'Instant study chance' } },
{ id: 'ql_t1_l10_transfer', name: 'Knowledge Transfer', desc: 'New spells/skills start at 10% progress', milestone: 10, effect: { type: 'special', specialId: 'knowledgeTransfer', specialDesc: 'Starting progress' } },
];
// Focused Mind Upgrades (-5% study cost per level)
const FOCUSED_MIND_TIER1_UPGRADES_L5: SkillUpgradeChoice[] = [
{ id: 'fm_t1_l5_efficiency', name: 'Mind Efficiency', desc: '+25% cost reduction', milestone: 5, effect: { type: 'multiplier', stat: 'costReduction', value: 1.25 } },
{ id: 'fm_t1_l5_clarity', name: 'Mental Clarity', desc: 'Study speed +10% when mana is above 75%', milestone: 5, effect: { type: 'special', specialId: 'mentalClarity', specialDesc: 'High mana study bonus' } },
{ id: 'fm_t1_l5_refund', name: 'Study Refund', desc: 'Get 25% mana back when study completes', milestone: 5, effect: { type: 'special', specialId: 'studyRefund', specialDesc: 'Study completion refund' } },
{ id: 'fm_t1_l5_discount', name: 'Bulk Discount', desc: '-10% cost for tier 2+ skills/spells', milestone: 5, effect: { type: 'special', specialId: 'bulkDiscount', specialDesc: 'High tier discount' } },
];
const FOCUSED_MIND_TIER1_UPGRADES_L10: SkillUpgradeChoice[] = [
{ id: 'fm_t1_l10_efficient', name: 'Efficient Learning', desc: '-10% study mana cost', milestone: 10, effect: { type: 'multiplier', stat: 'studyCost', value: 0.9 } },
{ id: 'fm_t1_l10_understanding', name: 'Deep Understanding', desc: '+10% bonus from all skill levels', milestone: 10, effect: { type: 'special', specialId: 'deepUnderstanding', specialDesc: 'Enhanced skills' } },
{ id: 'fm_t1_l10_rush', name: 'Study Rush', desc: 'First hour of study is 2x speed', milestone: 10, effect: { type: 'special', specialId: 'studyRush', specialDesc: 'Fast first hour' } },
{ id: 'fm_t1_l10_chain', name: 'Chain Study', desc: '-5% cost for each skill already maxed', milestone: 10, effect: { type: 'special', specialId: 'chainStudy', specialDesc: 'Learning synergy' } },
];
// Elemental Attunement Upgrades (+50 element cap per level)
const ELEM_ATTUNE_TIER1_UPGRADES_L5: SkillUpgradeChoice[] = [
{ id: 'ea_t1_l5_expand', name: 'Expanded Attunement', desc: '+25% element cap', milestone: 5, effect: { type: 'multiplier', stat: 'elementCap', value: 1.25 } },
{ id: 'ea_t1_l5_surge', name: 'Elemental Surge', desc: '+15% elemental damage', milestone: 5, effect: { type: 'multiplier', stat: 'elementalDamage', value: 1.15 } },
{ id: 'ea_t1_l5_expand2', name: 'Element Mastery', desc: '+10% element capacity', milestone: 5, effect: { type: 'bonus', stat: 'elementCap', value: 10 } },
{ id: 'ea_t1_l5_affinity', name: 'Elemental Affinity', desc: 'Newly unlocked elements start with 10 capacity', milestone: 5, effect: { type: 'special', specialId: 'elementalAffinity', specialDesc: 'Starting element capacity' } },
];
const ELEM_ATTUNE_TIER1_UPGRADES_L10: SkillUpgradeChoice[] = [
{ id: 'ea_t1_l10_master', name: 'Element Master', desc: '+20% elemental damage to all spells', milestone: 10, effect: { type: 'multiplier', stat: 'elementalDamage', value: 1.2 } },
{ id: 'ea_t1_l10_power', name: 'Elemental Power', desc: '+15% elemental damage', milestone: 10, effect: { type: 'multiplier', stat: 'elementalDamage', value: 1.15 } },
{ id: 'ea_t1_l10_resonance', name: 'Elemental Resonance', desc: 'Using element spells restores 1 of that element', milestone: 10, effect: { type: 'special', specialId: 'elementalResonance', specialDesc: 'Spell use restores element' } },
{ id: 'ea_t1_l10_exotic', name: 'Exotic Mastery', desc: '+20% exotic element damage', milestone: 10, effect: { type: 'special', specialId: 'exoticMastery', specialDesc: 'Exotic damage bonus' } },
];
// ─── Skill Evolution Paths ─────────────────────────────────────────────────────
// Tier multiplier: Each tier is 10x more powerful
// So Tier 2 level 1 = Tier 1 level 10, Tier 3 level 1 = Tier 2 level 10, etc.
export const SKILL_EVOLUTION_PATHS: Record<string, SkillEvolutionPath> = {
manaWell: {
baseSkillId: 'manaWell',
tiers: [
{
tier: 1,
skillId: 'manaWell',
name: 'Mana Well',
multiplier: 1,
upgrades: [...MANA_WELL_TIER1_UPGRADES_L5, ...MANA_WELL_TIER1_UPGRADES_L10],
},
{
tier: 2,
skillId: 'manaWell_t2',
name: 'Deep Reservoir',
multiplier: 10,
upgrades: [
{ id: 'mw_t2_l5_depth', name: 'Abyssal Depth', desc: '+50% max mana', milestone: 5, effect: { type: 'multiplier', stat: 'maxMana', value: 1.5 } },
{ id: 'mw_t2_l5_ancient', name: 'Ancient Well', desc: '+500 starting mana per loop', milestone: 5, effect: { type: 'bonus', stat: 'startingMana', value: 500 } },
{ id: 'mw_t2_l5_condense', name: 'Mana Condense', desc: 'Max mana +1% per 1000 total mana gathered', milestone: 5, effect: { type: 'special', specialId: 'manaCondense', specialDesc: 'Scaling max mana' } },
{ id: 'mw_t2_l5_reserve', name: 'Deep Reserve', desc: 'Regen +0.5 per 100 max mana', milestone: 5, effect: { type: 'special', specialId: 'deepReserve', specialDesc: 'Max mana scaling regen' } },
{ id: 'mw_t2_l10_ocean', name: 'Ocean of Mana', desc: '+1000 max mana', milestone: 10, effect: { type: 'bonus', stat: 'maxMana', value: 1000 } },
{ id: 'mw_t2_l10_tide', name: 'Mana Tide', desc: 'Mana regeneration pulses with time (+/- 50%)', milestone: 10, effect: { type: 'special', specialId: 'manaTide', specialDesc: 'Cyclic regen' } },
{ id: 'mw_t2_l10_void', name: 'Void Storage', desc: 'Store up to 150% max mana temporarily', milestone: 10, effect: { type: 'special', specialId: 'voidStorage', specialDesc: 'Overfill mana' } },
{ id: 'mw_t2_l10_core', name: 'Mana Core', desc: 'Gain mana regen equal to 0.5% of max mana', milestone: 10, effect: { type: 'special', specialId: 'manaCore', specialDesc: 'Max mana based regen' } },
],
},
{
tier: 3,
skillId: 'manaWell_t3',
name: 'Abyssal Pool',
multiplier: 100,
upgrades: [
{ id: 'mw_t3_l5_abyss', name: 'Abyssal Power', desc: '+100% max mana', milestone: 5, effect: { type: 'multiplier', stat: 'maxMana', value: 2 } },
{ id: 'mw_t3_l5_siphon', name: 'Mana Siphon', desc: 'Convert 1% of floor HP to mana on defeat', milestone: 5, effect: { type: 'special', specialId: 'manaSiphon', specialDesc: 'HP to mana conversion' } },
{ id: 'mw_t3_l5_heart', name: 'Mana Heart', desc: '+10% max mana per loop completed', milestone: 5, effect: { type: 'special', specialId: 'manaHeart', specialDesc: 'Loop scaling mana' } },
{ id: 'mw_t3_l5_ancient', name: 'Ancient Reserve', desc: 'Start each loop with 25% max mana', milestone: 5, effect: { type: 'special', specialId: 'ancientReserve', specialDesc: 'Loop starting mana' } },
{ id: 'mw_t3_l10_realm', name: 'Mana Realm', desc: '+5000 max mana', milestone: 10, effect: { type: 'bonus', stat: 'maxMana', value: 5000 } },
{ id: 'mw_t3_l10_avatar', name: 'Mana Avatar', desc: 'When full, all spells cost 50% less', milestone: 10, effect: { type: 'special', specialId: 'manaAvatar', specialDesc: 'Full mana discount' } },
{ id: 'mw_t3_l10_genesis', name: 'Mana Genesis', desc: 'Generate 1% max mana per hour passively', milestone: 10, effect: { type: 'special', specialId: 'manaGenesis', specialDesc: 'Passive mana generation' } },
{ id: 'mw_t3_l10_reflect', name: 'Mana Reflect', desc: '10% chance to reflect spell cost as damage', milestone: 10, effect: { type: 'special', specialId: 'manaReflect', specialDesc: 'Cost to damage' } },
],
},
{
tier: 4,
skillId: 'manaWell_t4',
name: 'Ocean of Power',
multiplier: 1000,
upgrades: [
{ id: 'mw_t4_l5_tsunami', name: 'Mana Tsunami', desc: '+200% max mana', milestone: 5, effect: { type: 'multiplier', stat: 'maxMana', value: 3 } },
{ id: 'mw_t4_l5_breath', name: 'Deep Breath', desc: 'Meditate for 1 hour = fill 50% mana', milestone: 5, effect: { type: 'special', specialId: 'deepBreath', specialDesc: 'Quick fill' } },
{ id: 'mw_t4_l5_sovereign', name: 'Mana Sovereign', desc: 'All mana costs reduced by 20%', milestone: 5, effect: { type: 'multiplier', stat: 'allCosts', value: 0.8 } },
{ id: 'mw_t4_l5_wellspring', name: 'Primordial Wellspring', desc: 'Clicks give 5% of max mana', milestone: 5, effect: { type: 'special', specialId: 'primordialWellspring', specialDesc: 'Max mana clicks' } },
{ id: 'mw_t4_l10_infinite', name: 'Infinite Reservoir', desc: '+50000 max mana', milestone: 10, effect: { type: 'bonus', stat: 'maxMana', value: 50000 } },
{ id: 'mw_t4_l10_ascend', name: 'Mana Conduit', desc: 'Meditation also regenerates 5% max elemental mana per hour', milestone: 10, effect: { type: 'special', specialId: 'manaConduit', specialDesc: 'Meditation element regen' } },
{ id: 'mw_t4_l10_nova', name: 'Mana Nova', desc: 'When taking damage, release 5% mana as damage', milestone: 10, effect: { type: 'special', specialId: 'manaNova', specialDesc: 'Defensive burst' } },
{ id: 'mw_t4_l10_overflow', name: 'Mana Overflow', desc: 'Excess mana from clicks is doubled', milestone: 10, effect: { type: 'special', specialId: 'manaOverflowT4', specialDesc: 'Click overflow' } },
],
},
{
tier: 5,
skillId: 'manaWell_t5',
name: 'Infinite Reservoir',
multiplier: 10000,
upgrades: [
{ id: 'mw_t5_l5_cosmic', name: 'Cosmic Mana', desc: '+500% max mana', milestone: 5, effect: { type: 'multiplier', stat: 'maxMana', value: 6 } },
{ id: 'mw_t5_l5_omega', name: 'Omega Well', desc: 'All mana effects +50%', milestone: 5, effect: { type: 'multiplier', stat: 'manaEffects', value: 1.5 } },
{ id: 'mw_t5_l5_origin', name: 'Origin Point', desc: 'Start loops with 100% mana', milestone: 5, effect: { type: 'special', specialId: 'originPoint', specialDesc: 'Full start' } },
{ id: 'mw_t5_l5_zenith', name: 'Mana Zenith', desc: 'At max mana, deal +50% damage', milestone: 5, effect: { type: 'special', specialId: 'manaZenith', specialDesc: 'Max mana damage' } },
{ id: 'mw_t5_l10_godhood', name: 'Mana Godhood', desc: '+100000 max mana', milestone: 10, effect: { type: 'bonus', stat: 'maxMana', value: 100000 } },
{ id: 'mw_t5_l10_ultimate', name: 'Ultimate Reservoir', desc: 'All spells enhanced by 1% per 1000 max mana', milestone: 10, effect: { type: 'special', specialId: 'ultimateReservoir', specialDesc: 'Mana scaling spells' } },
{ id: 'mw_t5_l10_immortal', name: 'Immortal Mana', desc: 'Mana regeneration never stops', milestone: 10, effect: { type: 'special', specialId: 'immortalMana', specialDesc: 'Always regen' } },
{ id: 'mw_t5_l10_victory', name: 'Mana Victory', desc: 'Alternative victory: reach 1M mana', milestone: 10, effect: { type: 'special', specialId: 'manaVictory', specialDesc: 'Mana victory' } },
],
},
],
},
manaFlow: {
baseSkillId: 'manaFlow',
tiers: [
{
tier: 1,
skillId: 'manaFlow',
name: 'Mana Flow',
multiplier: 1,
upgrades: [...MANA_FLOW_TIER1_UPGRADES_L5, ...MANA_FLOW_TIER1_UPGRADES_L10],
},
{
tier: 2,
skillId: 'manaFlow_t2',
name: 'Rushing Stream',
multiplier: 10,
upgrades: [
{ id: 'mf_t2_l5_river', name: 'River of Mana', desc: '+50% regen', milestone: 5, effect: { type: 'multiplier', stat: 'regen', value: 1.5 } },
{ id: 'mf_t2_l5_flood', name: 'Mana Flood', desc: 'Regen +2 per guardian defeated', milestone: 5, effect: { type: 'special', specialId: 'manaFlood', specialDesc: 'Guardian regen' } },
{ id: 'mf_t2_l5_whirlpool', name: 'Mana Whirlpool', desc: 'Convert overflow mana to random elements', milestone: 5, effect: { type: 'special', specialId: 'manaWhirlpool', specialDesc: 'Overflow conversion' } },
{ id: 'mf_t2_l5_current', name: 'Swift Current', desc: '+25% regen during combat', milestone: 5, effect: { type: 'special', specialId: 'swiftCurrent', specialDesc: 'Combat regen' } },
{ id: 'mf_t2_l10_cascade', name: 'Mana Cascade', desc: '+20 regen', milestone: 10, effect: { type: 'bonus', stat: 'regen', value: 20 } },
{ id: 'mf_t2_l10_storm', name: 'Mana Storm', desc: 'Every 6 hours, gain 500 mana instantly', milestone: 10, effect: { type: 'special', specialId: 'manaStorm', specialDesc: 'Periodic burst' } },
{ id: 'mf_t2_l10_tributary', name: 'Tributary Flow', desc: '+0.5 regen per learned spell', milestone: 10, effect: { type: 'special', specialId: 'tributaryFlow', specialDesc: 'Spell regen' } },
{ id: 'mf_t2_l10_eternal', name: 'Eternal Flow', desc: 'Regen is immune to incursion penalty', milestone: 10, effect: { type: 'special', specialId: 'eternalFlow', specialDesc: 'Incursion immunity' } },
],
},
{
tier: 3,
skillId: 'manaFlow_t3',
name: 'Eternal River',
multiplier: 100,
upgrades: [
{ id: 'mf_t3_l5_ocean', name: 'Ocean Current', desc: '+100% regen', milestone: 5, effect: { type: 'multiplier', stat: 'regen', value: 2 } },
{ id: 'mf_t3_l5_tide', name: 'Tidal Force', desc: 'Regen varies with time of day (0.5x to 2x)', milestone: 5, effect: { type: 'special', specialId: 'tidalForce', specialDesc: 'Time scaling' } },
{ id: 'mf_t3_l5_abyss', name: 'Abyssal Current', desc: '+1 regen per floor reached', milestone: 5, effect: { type: 'special', specialId: 'abyssalCurrent', specialDesc: 'Floor regen' } },
{ id: 'mf_t3_l5_monsoon', name: 'Mana Monsoon', desc: '+5 regen per loop completed', milestone: 5, effect: { type: 'special', specialId: 'manaMonsoon', specialDesc: 'Loop regen' } },
{ id: 'mf_t3_l10_deluge', name: 'Mana Deluge', desc: '+100 regen', milestone: 10, effect: { type: 'bonus', stat: 'regen', value: 100 } },
{ id: 'mf_t3_l10_fountain', name: 'Infinite Fountain', desc: 'Mana regen has no upper limit to overflow', milestone: 10, effect: { type: 'special', specialId: 'infiniteFountain', specialDesc: 'Always regen' } },
{ id: 'mf_t3_l10_source', name: 'Primordial Source', desc: 'Regen +1% of max mana per hour', milestone: 10, effect: { type: 'special', specialId: 'primordialSource', specialDesc: 'Max mana regen' } },
{ id: 'mf_t3_l10_blessing', name: 'River Blessing', desc: 'Spells cost 1 less mana minimum (min 1)', milestone: 10, effect: { type: 'special', specialId: 'riverBlessing', specialDesc: 'Min cost reduction' } },
],
},
{
tier: 4,
skillId: 'manaFlow_t4',
name: 'Cosmic Torrent',
multiplier: 1000,
upgrades: [
{ id: 'mf_t4_l5_nova', name: 'Mana Nova', desc: '+200% regen', milestone: 5, effect: { type: 'multiplier', stat: 'regen', value: 3 } },
{ id: 'mf_t4_l5_nebula', name: 'Nebula Flow', desc: 'Gain 10% regen from all actions', milestone: 5, effect: { type: 'special', specialId: 'nebulaFlow', specialDesc: 'Action mana' } },
{ id: 'mf_t4_l5_constellation', name: 'Constellation Link', desc: '+5 regen per skill maxed', milestone: 5, effect: { type: 'special', specialId: 'constellationLink', specialDesc: 'Skill regen' } },
{ id: 'mf_t4_l5_supernova', name: 'Supernova Burst', desc: 'Once per loop, instantly fill all mana', milestone: 5, effect: { type: 'special', specialId: 'supernovaBurst', specialDesc: 'Loop burst' } },
{ id: 'mf_t4_l10_galaxy', name: 'Galaxy Flow', desc: '+500 regen', milestone: 10, effect: { type: 'bonus', stat: 'regen', value: 500 } },
{ id: 'mf_t4_l10_universe', name: 'Universal Mana', desc: 'All mana sources +100%', milestone: 10, effect: { type: 'multiplier', stat: 'allManaSources', value: 2 } },
{ id: 'mf_t4_l10_omega', name: 'Omega Flow', desc: 'Regen = max mana / 50', milestone: 10, effect: { type: 'special', specialId: 'omegaFlow', specialDesc: 'Max mana scaling' } },
{ id: 'mf_t4_l10_zenith', name: 'Flow Zenith', desc: 'At peak hours (12:00), gain 10x regen', milestone: 10, effect: { type: 'special', specialId: 'flowZenith', specialDesc: 'Peak hours bonus' } },
],
},
{
tier: 5,
skillId: 'manaFlow_t5',
name: 'Infinite Cascade',
multiplier: 10000,
upgrades: [
{ id: 'mf_t5_l5_multiverse', name: 'Multiverse Flow', desc: '+500% regen', milestone: 5, effect: { type: 'multiplier', stat: 'regen', value: 6 } },
{ id: 'mf_t5_l5_dimension', name: 'Dimensional Tap', desc: 'Draw mana from alternate dimensions (+50% all sources)', milestone: 5, effect: { type: 'multiplier', stat: 'allManaSources', value: 1.5 } },
{ id: 'mf_t5_l5_omniscience', name: 'Omniscient Flow', desc: 'Know when mana peaks (predict high regen times)', milestone: 5, effect: { type: 'special', specialId: 'omniscientFlow', specialDesc: 'Peak prediction' } },
{ id: 'mf_t5_l5_ultimate', name: 'Ultimate Stream', desc: 'All mana effects doubled', milestone: 5, effect: { type: 'multiplier', stat: 'manaEffects', value: 2 } },
{ id: 'mf_t5_l10_godhood', name: 'Flow Godhood', desc: '+2000 regen', milestone: 10, effect: { type: 'bonus', stat: 'regen', value: 2000 } },
{ id: 'mf_t5_l10_infinity', name: 'Infinite Flow', desc: 'Mana regeneration has no limits', milestone: 10, effect: { type: 'special', specialId: 'infiniteFlowRegen', specialDesc: 'Uncapped regen' } },
{ id: 'mf_t5_l10_transcend', name: 'Flow Transcendence', desc: 'Become one with mana flow (all actions give mana)', milestone: 10, effect: { type: 'special', specialId: 'flowTranscendence', specialDesc: 'Mana unity' } },
{ id: 'mf_t5_l10_victory', name: 'Flow Victory', desc: 'Victory: regenerate 10000 mana/hour', milestone: 10, effect: { type: 'special', specialId: 'flowVictory', specialDesc: 'Flow victory' } },
],
},
],
},
combatTrain: {
baseSkillId: 'combatTrain',
tiers: [
{
tier: 1,
skillId: 'combatTrain',
name: 'Combat Training',
multiplier: 1,
upgrades: [...COMBAT_TRAIN_TIER1_UPGRADES_L5, ...COMBAT_TRAIN_TIER1_UPGRADES_L10],
},
{
tier: 2,
skillId: 'combatTrain_t2',
name: 'Warrior Instinct',
multiplier: 10,
upgrades: [
{ id: 'ct_t2_l5_mastery', name: 'Combat Mastery', desc: '+50% base damage', milestone: 5, effect: { type: 'multiplier', stat: 'baseDamage', value: 1.5 } },
{ id: 'ct_t2_l5_cleave', name: 'Cleave', desc: 'Deal 25% damage to next floor enemy', milestone: 5, effect: { type: 'special', specialId: 'cleave', specialDesc: 'Multi-floor damage' } },
{ id: 'ct_t2_l5_berserk', name: 'Berserk Training', desc: '+5% damage per consecutive hit (max +100%)', milestone: 5, effect: { type: 'special', specialId: 'berserkTraining', specialDesc: 'Consecutive bonus' } },
{ id: 'ct_t2_l5_weapon', name: 'Weapon Mastery', desc: '+25% equipment damage bonuses', milestone: 5, effect: { type: 'multiplier', stat: 'equipmentDamage', value: 1.25 } },
{ id: 'ct_t2_l10_devastate', name: 'Devastation', desc: '+100 base damage', milestone: 10, effect: { type: 'bonus', stat: 'baseDamage', value: 100 } },
{ id: 'ct_t2_l10_streak', name: 'Kill Streak', desc: '+5% damage per kill this loop (max +100%)', milestone: 10, effect: { type: 'special', specialId: 'killStreak', specialDesc: 'Kill scaling' } },
{ id: 'ct_t2_l10_finisher', name: 'Finisher', desc: '+100% damage to enemies below 50% HP', milestone: 10, effect: { type: 'special', specialId: 'finisherBonus', specialDesc: 'Execute mastery' } },
{ id: 'ct_t2_l10_frenzy', name: 'Battle Frenzy', desc: 'Attack speed +50% for 1 hour after kill', milestone: 10, effect: { type: 'special', specialId: 'battleFrenzy', specialDesc: 'Kill speed boost' } },
],
},
{
tier: 3,
skillId: 'combatTrain_t3',
name: 'Battlemaster',
multiplier: 100,
upgrades: [
{ id: 'ct_t3_l5_legendary', name: 'Legendary Combat', desc: '+100% base damage', milestone: 5, effect: { type: 'multiplier', stat: 'baseDamage', value: 2 } },
{ id: 'ct_t3_l5_annihilate', name: 'Annihilation', desc: '10% chance to deal 5x damage', milestone: 5, effect: { type: 'special', specialId: 'annihilation', specialDesc: 'Massive crit chance' } },
{ id: 'ct_t3_l5_bane', name: 'Guardian Bane+', desc: '+50% damage vs guardians', milestone: 5, effect: { type: 'special', specialId: 'guardianBanePlus', specialDesc: 'Guardian bonus' } },
{ id: 'ct_t3_l5_onslaught', name: 'Onslaught', desc: 'Each hit increases next hit by 5% (resets on floor clear)', milestone: 5, effect: { type: 'special', specialId: 'onslaught', specialDesc: 'Cumulative damage' } },
{ id: 'ct_t3_l10_dominator', name: 'Floor Dominator', desc: '+500 base damage', milestone: 10, effect: { type: 'bonus', stat: 'baseDamage', value: 500 } },
{ id: 'ct_t3_l10_aura', name: 'Battle Aura', desc: 'Passively deal 5% damage per hour while climbing', milestone: 10, effect: { type: 'special', specialId: 'battleAura', specialDesc: 'Passive damage' } },
{ id: 'ct_t3_l10_chain', name: 'Chain Strike', desc: '25% chance to hit again at 50% damage', milestone: 10, effect: { type: 'special', specialId: 'chainStrike', specialDesc: 'Chain attack' } },
{ id: 'ct_t3_l10_rage', name: 'Eternal Rage', desc: 'Damage increases by 10% per loop completed', milestone: 10, effect: { type: 'special', specialId: 'eternalRage', specialDesc: 'Loop scaling damage' } },
],
},
{
tier: 4,
skillId: 'combatTrain_t4',
name: 'Avatar of War',
multiplier: 1000,
upgrades: [
{ id: 'ct_t4_l5_godlike', name: 'Godlike Combat', desc: '+200% base damage', milestone: 5, effect: { type: 'multiplier', stat: 'baseDamage', value: 3 } },
{ id: 'ct_t4_l5_void', name: 'Void Strike', desc: 'Attacks deal 10% true damage (ignores defense)', milestone: 5, effect: { type: 'special', specialId: 'voidStrike', specialDesc: 'True damage' } },
{ id: 'ct_t4_l5_master', name: 'Combat Grandmaster', desc: 'All combat skills +2 levels', milestone: 5, effect: { type: 'special', specialId: 'combatGrandmaster', specialDesc: 'Skill boost' } },
{ id: 'ct_t4_l5_tempest', name: 'Tempest Strike', desc: 'Every 10th attack is a guaranteed crit', milestone: 5, effect: { type: 'special', specialId: 'tempestStrike', specialDesc: 'Guaranteed crit' } },
{ id: 'ct_t4_l10_destruction', name: 'Avatar of Destruction', desc: '+2000 base damage', milestone: 10, effect: { type: 'bonus', stat: 'baseDamage', value: 2000 } },
{ id: 'ct_t4_l10_apocalypse', name: 'Apocalypse Strike', desc: '1% chance to instantly clear floor', milestone: 10, effect: { type: 'special', specialId: 'apocalypseStrike', specialDesc: 'Instant clear' } },
{ id: 'ct_t4_l10_omega', name: 'Omega Strike', desc: 'Final hit on floor deals +300% damage to next floor', milestone: 10, effect: { type: 'special', specialId: 'omegaStrike', specialDesc: 'Finisher carryover' } },
{ id: 'ct_t4_l10_immortal', name: 'Immortal Warrior', desc: 'Combat unaffected by incursion', milestone: 10, effect: { type: 'special', specialId: 'immortalWarrior', specialDesc: 'Incursion immunity' } },
],
},
{
tier: 5,
skillId: 'combatTrain_t5',
name: 'Eternal Conqueror',
multiplier: 10000,
upgrades: [
{ id: 'ct_t5_l5_transcend', name: 'Transcendent Combat', desc: '+500% base damage', milestone: 5, effect: { type: 'multiplier', stat: 'baseDamage', value: 6 } },
{ id: 'ct_t5_l5_ultimate', name: 'Ultimate Warrior', desc: 'All attacks have +50% crit chance', milestone: 5, effect: { type: 'special', specialId: 'ultimateWarrior', specialDesc: 'Enhanced attacks' } },
{ id: 'ct_t5_l5_legend', name: 'Living Legend', desc: '+50% damage per loop completed', milestone: 5, effect: { type: 'special', specialId: 'livingLegend', specialDesc: 'Loop scaling' } },
{ id: 'ct_t5_l5_dominator', name: 'Absolute Dominator', desc: 'Guardians take 3x damage', milestone: 5, effect: { type: 'special', specialId: 'absoluteDominator', specialDesc: 'Triple guardian damage' } },
{ id: 'ct_t5_l10_godhood', name: 'War Godhood', desc: '+10000 base damage', milestone: 10, effect: { type: 'bonus', stat: 'baseDamage', value: 10000 } },
{ id: 'ct_t5_l10_oneshot', name: 'One Shot', desc: '5% chance to deal 50x damage', milestone: 10, effect: { type: 'special', specialId: 'oneShot', specialDesc: 'Massive damage' } },
{ id: 'ct_t5_l10_victory', name: 'Combat Victory', desc: 'Victory: defeat all guardians automatically', milestone: 10, effect: { type: 'special', specialId: 'combatVictory', specialDesc: 'Combat victory' } },
{ id: 'ct_t5_l10_omnipotence', name: 'Omnipotent Strike', desc: 'Every attack is a critical hit', milestone: 10, effect: { type: 'special', specialId: 'omnipotentStrike', specialDesc: 'Always crit' } },
],
},
],
},
quickLearner: {
baseSkillId: 'quickLearner',
tiers: [
{
tier: 1,
skillId: 'quickLearner',
name: 'Quick Learner',
multiplier: 1,
upgrades: [...QUICK_LEARNER_TIER1_UPGRADES_L5, ...QUICK_LEARNER_TIER1_UPGRADES_L10],
},
{
tier: 2,
skillId: 'quickLearner_t2',
name: 'Swift Scholar',
multiplier: 10,
upgrades: [
{ id: 'ql_t2_l5_genius', name: 'Study Genius', desc: '+50% study speed', milestone: 5, effect: { type: 'multiplier', stat: 'studySpeed', value: 1.5 } },
{ id: 'ql_t2_l5_snap', name: 'Snap Learning', desc: 'Instantly complete 10% of study when starting', milestone: 5, effect: { type: 'special', specialId: 'snapLearning', specialDesc: 'Study head start' } },
{ id: 'ql_t2_l5_archive', name: 'Mental Archive', desc: 'Keep 50% study progress between loops', milestone: 5, effect: { type: 'special', specialId: 'mentalArchive', specialDesc: 'Progress retention' } },
{ id: 'ql_t2_l5_rush', name: 'Study Rush+', desc: 'First 2 hours of study are 2x speed', milestone: 5, effect: { type: 'special', specialId: 'studyRushT2', specialDesc: 'Quick start' } },
{ id: 'ql_t2_l10_master', name: 'Study Master', desc: '+100% study speed', milestone: 10, effect: { type: 'multiplier', stat: 'studySpeed', value: 2 } },
{ id: 'ql_t2_l10_instant', name: 'Instant Grasp', desc: '5% chance to instantly learn', milestone: 10, effect: { type: 'special', specialId: 'instantGrasp', specialDesc: 'Instant learn chance' } },
{ id: 'ql_t2_l10_resonance', name: 'Knowledge Resonance', desc: 'Learning one thing speeds up others by 5%', milestone: 10, effect: { type: 'special', specialId: 'knowledgeResonance', specialDesc: 'Study synergy' } },
{ id: 'ql_t2_l10_adept', name: 'Quick Adept', desc: 'Tier 2+ studies are 25% faster', milestone: 10, effect: { type: 'special', specialId: 'quickAdept', specialDesc: 'High tier speed' } },
],
},
{
tier: 3,
skillId: 'quickLearner_t3',
name: 'Sage Mind',
multiplier: 100,
upgrades: [
{ id: 'ql_t3_l5_enlightenment', name: 'Enlightenment', desc: '+100% study speed', milestone: 5, effect: { type: 'multiplier', stat: 'studySpeed', value: 2 } },
{ id: 'ql_t3_l5_palace', name: 'Mind Palace+', desc: 'Store 3 skills for instant study next loop', milestone: 5, effect: { type: 'special', specialId: 'mindPalacePlus', specialDesc: 'Stored skills' } },
{ id: 'ql_t3_l5_burst', name: 'Study Burst', desc: 'First 50% of study takes half time', milestone: 5, effect: { type: 'special', specialId: 'studyBurst', specialDesc: 'Quick first half' } },
{ id: 'ql_t3_l5_legacy', name: 'Scholar Legacy', desc: 'Start loops with 1 random skill at level 1', milestone: 5, effect: { type: 'special', specialId: 'scholarLegacy', specialDesc: 'Starting skill' } },
{ id: 'ql_t3_l10_transcend', name: 'Study Transcendence', desc: 'Studies complete at 90% progress', milestone: 10, effect: { type: 'special', specialId: 'studyTranscendence', specialDesc: 'Early completion' } },
{ id: 'ql_t3_l10_overflow', name: 'Knowledge Overflow', desc: 'Excess study progress carries to next study', milestone: 10, effect: { type: 'special', specialId: 'knowledgeOverflow', specialDesc: 'Progress carryover' } },
{ id: 'ql_t3_l10_triple', name: 'Triple Mind', desc: 'Study 3 things at once at 33% speed each', milestone: 10, effect: { type: 'special', specialId: 'tripleMind', specialDesc: 'Triple study' } },
{ id: 'ql_t3_l10_ancient', name: 'Ancient Scholar', desc: 'Tier 4+ studies are 50% faster', milestone: 10, effect: { type: 'special', specialId: 'ancientScholar', specialDesc: 'High tier bonus' } },
],
},
{
tier: 4,
skillId: 'quickLearner_t4',
name: 'Cosmic Scholar',
multiplier: 1000,
upgrades: [
{ id: 'ql_t4_l5_cosmic', name: 'Cosmic Learning', desc: '+200% study speed', milestone: 5, effect: { type: 'multiplier', stat: 'studySpeed', value: 3 } },
{ id: 'ql_t4_l5_archive', name: 'Cosmic Archive', desc: 'Access all known spells/skills instantly for re-study', milestone: 5, effect: { type: 'special', specialId: 'cosmicArchive', specialDesc: 'All knowledge' } },
{ id: 'ql_t4_l5_dimension', name: 'Dimension Study', desc: 'Study continues in background while doing other actions', milestone: 5, effect: { type: 'special', specialId: 'dimensionStudy', specialDesc: 'Background study' } },
{ id: 'ql_t4_l5_grant', name: 'Knowledge Grant', desc: 'Each loop, gain 1 free random spell', milestone: 5, effect: { type: 'special', specialId: 'knowledgeGrant', specialDesc: 'Free spell per loop' } },
{ id: 'ql_t4_l10_omniscient', name: 'Omniscient Mind', desc: 'All studies are 50% faster', milestone: 10, effect: { type: 'multiplier', stat: 'allStudy', value: 1.5 } },
{ id: 'ql_t4_l10_infinite', name: 'Infinite Learning', desc: 'No maximum on study queue', milestone: 10, effect: { type: 'special', specialId: 'infiniteLearning', specialDesc: 'No learning cap' } },
{ id: 'ql_t4_l10_echo', name: 'Knowledge Echo+', desc: '20% instant learn chance', milestone: 10, effect: { type: 'special', specialId: 'knowledgeEchoPlus', specialDesc: 'Better instant' } },
{ id: 'ql_t4_l10_mastery', name: 'Study Mastery', desc: 'Completing study gives 25% mana back', milestone: 10, effect: { type: 'special', specialId: 'studyMastery', specialDesc: 'Completion refund' } },
],
},
{
tier: 5,
skillId: 'quickLearner_t5',
name: 'Omniscient Being',
multiplier: 10000,
upgrades: [
{ id: 'ql_t5_l5_godhood', name: 'Learning Godhood', desc: '+500% study speed', milestone: 5, effect: { type: 'multiplier', stat: 'studySpeed', value: 6 } },
{ id: 'ql_t5_l5_allknowing', name: 'All-Knowing', desc: 'See all unlock requirements', milestone: 5, effect: { type: 'special', specialId: 'allKnowing', specialDesc: 'All secrets' } },
{ id: 'ql_t5_l5_ultimate', name: 'Ultimate Scholar', desc: 'All learning is instant', milestone: 5, effect: { type: 'special', specialId: 'ultimateScholar', specialDesc: 'Instant all' } },
{ id: 'ql_t5_l5_transcend', name: 'Mind Transcendence', desc: 'Keep 5 skill levels across loops', milestone: 5, effect: { type: 'special', specialId: 'mindTranscendence', specialDesc: 'Unlimited retention' } },
{ id: 'ql_t5_l10_perfection', name: 'Perfect Learning', desc: 'All studies complete instantly', milestone: 10, effect: { type: 'special', specialId: 'perfectLearning', specialDesc: 'Instant mastery' } },
{ id: 'ql_t5_l10_victory', name: 'Knowledge Victory', desc: 'Victory: learn all things', milestone: 10, effect: { type: 'special', specialId: 'knowledgeVictory', specialDesc: 'Learn victory' } },
{ id: 'ql_t5_l10_eternal', name: 'Eternal Knowledge', desc: 'Keep all knowledge forever', milestone: 10, effect: { type: 'special', specialId: 'eternalKnowledge', specialDesc: 'Permanent knowledge' } },
{ id: 'ql_t5_l10_origin', name: 'Origin Mind', desc: 'Become the source of all knowledge', milestone: 10, effect: { type: 'special', specialId: 'originMind', specialDesc: 'Knowledge source' } },
],
},
],
},
focusedMind: {
baseSkillId: 'focusedMind',
tiers: [
{
tier: 1,
skillId: 'focusedMind',
name: 'Focused Mind',
multiplier: 1,
upgrades: [...FOCUSED_MIND_TIER1_UPGRADES_L5, ...FOCUSED_MIND_TIER1_UPGRADES_L10],
},
{
tier: 2,
skillId: 'focusedMind_t2',
name: 'Crystal Mind',
multiplier: 10,
upgrades: [
{ id: 'fm_t2_l5_clarity', name: 'Crystal Clarity', desc: '+50% cost reduction', milestone: 5, effect: { type: 'multiplier', stat: 'costReduction', value: 1.5 } },
{ id: 'fm_t2_l5_store', name: 'Mana Store', desc: 'Store up to 50% of study cost for next study', milestone: 5, effect: { type: 'special', specialId: 'manaStore', specialDesc: 'Cost storage' } },
{ id: 'fm_t2_l5_efficient', name: 'Efficient Mind', desc: 'Tier 2+ skills cost 20% less', milestone: 5, effect: { type: 'special', specialId: 'efficientMind', specialDesc: 'High tier discount' } },
{ id: 'fm_t2_l5_resonance', name: 'Cost Resonance', desc: 'Each study reduces next study cost by 5%', milestone: 5, effect: { type: 'special', specialId: 'costResonance', specialDesc: 'Cumulative discount' } },
{ id: 'fm_t2_l10_mastery', name: 'Cost Mastery', desc: 'All costs reduced by 25%', milestone: 10, effect: { type: 'multiplier', stat: 'allCosts', value: 0.75 } },
{ id: 'fm_t2_l10_refund', name: 'Full Refund', desc: 'Get 50% mana back when study completes', milestone: 10, effect: { type: 'special', specialId: 'fullRefund', specialDesc: 'Big refund' } },
{ id: 'fm_t2_l10_discount', name: 'Master Discount', desc: 'Skills cost 10% of base instead of level scaling', milestone: 10, effect: { type: 'special', specialId: 'masterDiscount', specialDesc: 'Flat cost' } },
{ id: 'fm_t2_l10_memory', name: 'Cost Memory', desc: 'First study of each type is free', milestone: 10, effect: { type: 'special', specialId: 'costMemory', specialDesc: 'Free first study' } },
],
},
{
tier: 3,
skillId: 'focusedMind_t3',
name: 'Void Mind',
multiplier: 100,
upgrades: [
{ id: 'fm_t3_l5_void', name: 'Void Focus', desc: '+100% cost reduction', milestone: 5, effect: { type: 'multiplier', stat: 'costReduction', value: 2 } },
{ id: 'fm_t3_l5_negate', name: 'Cost Negation', desc: '25% chance for study to be free', milestone: 5, effect: { type: 'special', specialId: 'costNegation', specialDesc: 'Free chance' } },
{ id: 'fm_t3_l5_reverse', name: 'Cost Reverse', desc: '10% chance to gain mana from study', milestone: 5, effect: { type: 'special', specialId: 'costReverse', specialDesc: 'Reverse cost' } },
{ id: 'fm_t3_l5_unlimited', name: 'Unlimited Focus', desc: 'Study cost can go below 1', milestone: 5, effect: { type: 'special', specialId: 'unlimitedFocus', specialDesc: 'No minimum' } },
{ id: 'fm_t3_l10_free', name: 'Mostly Free', desc: '50% of studies are free', milestone: 10, effect: { type: 'special', specialId: 'mostlyFree', specialDesc: 'Mostly free' } },
{ id: 'fm_t3_l10_zenith', name: 'Mind Zenith', desc: 'When mana is full, all studies free', milestone: 10, effect: { type: 'special', specialId: 'mindZenith', specialDesc: 'Peak free' } },
{ id: 'fm_t3_l10_ultimate', name: 'Ultimate Efficiency', desc: 'All costs are 10% of base', milestone: 10, effect: { type: 'multiplier', stat: 'allCosts', value: 0.1 } },
{ id: 'fm_t3_l10_infinite', name: 'Infinite Focus', desc: 'Never run out of mana for study', milestone: 10, effect: { type: 'special', specialId: 'infiniteFocus', specialDesc: 'Infinite study mana' } },
],
},
{
tier: 4,
skillId: 'focusedMind_t4',
name: 'Cosmic Mind',
multiplier: 1000,
upgrades: [
{ id: 'fm_t4_l5_cosmic', name: 'Cosmic Focus', desc: '+200% cost reduction', milestone: 5, effect: { type: 'multiplier', stat: 'costReduction', value: 3 } },
{ id: 'fm_t4_l5_void', name: 'Void Cost', desc: 'Studies draw from void instead of mana (75% free)', milestone: 5, effect: { type: 'special', specialId: 'voidCost', specialDesc: 'Void power' } },
{ id: 'fm_t4_l5_transcend', name: 'Cost Transcendence', desc: 'Costs are capped at 10% of current mana', milestone: 5, effect: { type: 'special', specialId: 'costTranscendence', specialDesc: 'Capped costs' } },
{ id: 'fm_t4_l5_omega', name: 'Omega Focus', desc: 'All sources of mana cost reduction are doubled', milestone: 5, effect: { type: 'special', specialId: 'omegaFocus', specialDesc: 'Doubled reduction' } },
{ id: 'fm_t4_l10_free', name: 'Mostly Free+', desc: '90% of studies are free', milestone: 10, effect: { type: 'special', specialId: 'mostlyFreePlus', specialDesc: 'Almost all free' } },
{ id: 'fm_t4_l10_zero', name: 'Zero Cost', desc: 'All studies cost 0', milestone: 10, effect: { type: 'special', specialId: 'zeroCost', specialDesc: 'All free' } },
{ id: 'fm_t4_l10_profit', name: 'Study Profit', desc: 'Gain mana when studying', milestone: 10, effect: { type: 'special', specialId: 'studyProfit', specialDesc: 'Study gives mana' } },
{ id: 'fm_t4_l10_eternal', name: 'Eternal Focus', desc: 'Mind never tires - no cost ever', milestone: 10, effect: { type: 'special', specialId: 'eternalFocus', specialDesc: 'Never tire' } },
],
},
{
tier: 5,
skillId: 'focusedMind_t5',
name: 'Omniscient Mind',
multiplier: 10000,
upgrades: [
{ id: 'fm_t5_l5_godhood', name: 'Focus Godhood', desc: '+500% cost reduction', milestone: 5, effect: { type: 'multiplier', stat: 'costReduction', value: 6 } },
{ id: 'fm_t5_l5_all', name: 'All Free', desc: 'All studies cost nothing', milestone: 5, effect: { type: 'special', specialId: 'allFree', specialDesc: 'All free' } },
{ id: 'fm_t5_l5_source', name: 'Mana Source', desc: 'Become a source of study mana (regen while studying)', milestone: 5, effect: { type: 'special', specialId: 'manaSource', specialDesc: 'Mana source' } },
{ id: 'fm_t5_l5_transcend', name: 'Cost Transcendence', desc: 'Transcend all costs permanently', milestone: 5, effect: { type: 'special', specialId: 'costTranscendenceFinal', specialDesc: 'Transcend costs' } },
{ id: 'fm_t5_l10_perfection', name: 'Perfect Mind', desc: 'All costs are 0, always', milestone: 10, effect: { type: 'special', specialId: 'perfectMind', specialDesc: 'Zero costs' } },
{ id: 'fm_t5_l10_victory', name: 'Focus Victory', desc: 'Victory: study everything instantly', milestone: 10, effect: { type: 'special', specialId: 'focusVictory', specialDesc: 'Free victory' } },
{ id: 'fm_t5_l10_eternal', name: 'Eternal Focus', desc: 'Mind never tires, infinite capacity', milestone: 10, effect: { type: 'special', specialId: 'eternalFocusFinal', specialDesc: 'Never tire' } },
{ id: 'fm_t5_l10_origin', name: 'Origin Focus', desc: 'You are the source of focus', milestone: 10, effect: { type: 'special', specialId: 'originFocus', specialDesc: 'Focus origin' } },
],
},
],
},
elemAttune: {
baseSkillId: 'elemAttune',
tiers: [
{
tier: 1,
skillId: 'elemAttune',
name: 'Elemental Attunement',
multiplier: 1,
upgrades: [...ELEM_ATTUNE_TIER1_UPGRADES_L5, ...ELEM_ATTUNE_TIER1_UPGRADES_L10],
},
{
tier: 2,
skillId: 'elemAttune_t2',
name: 'Elemental Affinity',
multiplier: 10,
upgrades: [
{ id: 'ea_t2_l5_expand', name: 'Expanded Affinity', desc: '+50% element cap', milestone: 5, effect: { type: 'multiplier', stat: 'elementCap', value: 1.5 } },
{ id: 'ea_t2_l5_dual', name: 'Dual Elements', desc: 'Convert to 2 elements at once', milestone: 5, effect: { type: 'special', specialId: 'dualElements', specialDesc: 'Dual convert' } },
{ id: 'ea_t2_l5_stable', name: 'Stable Elements', desc: 'Elemental mana never decays', milestone: 5, effect: { type: 'special', specialId: 'stableElements', specialDesc: 'No decay' } },
{ id: 'ea_t2_l5_amplify', name: 'Element Amplify', desc: '+25% elemental damage', milestone: 5, effect: { type: 'multiplier', stat: 'elementalDamage', value: 1.25 } },
{ id: 'ea_t2_l10_mastery', name: 'Element Mastery', desc: '+200 element cap', milestone: 10, effect: { type: 'bonus', stat: 'elementCap', value: 200 } },
{ id: 'ea_t2_l10_convert', name: 'Quick Convert', desc: 'Convert 10 at a time with 10% bonus', milestone: 10, effect: { type: 'special', specialId: 'quickConvert', specialDesc: 'Bulk convert' } },
{ id: 'ea_t2_l10_harmony', name: 'Element Harmony+', desc: 'All elements work together (+10% all element damage)', milestone: 10, effect: { type: 'special', specialId: 'elementHarmonyPlus', specialDesc: 'Element synergy' } },
{ id: 'ea_t2_l10_overflow', name: 'Element Overflow', desc: 'Excess elements convert to raw mana', milestone: 10, effect: { type: 'special', specialId: 'elementOverflow', specialDesc: 'Overflow conversion' } },
],
},
{
tier: 3,
skillId: 'elemAttune_t3',
name: 'Elemental Mastery',
multiplier: 100,
upgrades: [
{ id: 'ea_t3_l5_dominator', name: 'Element Dominator', desc: '+100% element cap', milestone: 5, effect: { type: 'multiplier', stat: 'elementCap', value: 2 } },
{ id: 'ea_t3_l5_forge', name: 'Element Forge', desc: 'Craft exotic elements without recipe', milestone: 5, effect: { type: 'special', specialId: 'elementForge', specialDesc: 'Exotic crafting' } },
{ id: 'ea_t3_l5_surge', name: 'Element Surge', desc: '+50% elemental spell damage', milestone: 5, effect: { type: 'multiplier', stat: 'elementalDamage', value: 1.5 } },
{ id: 'ea_t3_l5_well', name: 'Element Well', desc: 'Elements regenerate 1 per hour', milestone: 5, effect: { type: 'special', specialId: 'elementWell', specialDesc: 'Element regen' } },
{ id: 'ea_t3_l10_transcend', name: 'Element Transcendence', desc: '+1000 element cap', milestone: 10, effect: { type: 'bonus', stat: 'elementCap', value: 1000 } },
{ id: 'ea_t3_l10_avatar', name: 'Element Avatar', desc: 'Become one with elements (immune to element weaknesses)', milestone: 10, effect: { type: 'special', specialId: 'elementAvatar', specialDesc: 'Element unity' } },
{ id: 'ea_t3_l10_chain', name: 'Element Chain', desc: 'Using one element boosts next element by 20%', milestone: 10, effect: { type: 'special', specialId: 'elementChain', specialDesc: 'Chain bonus' } },
{ id: 'ea_t3_l10_prime', name: 'Prime Elements', desc: 'Base elements give 2x capacity bonus', milestone: 10, effect: { type: 'special', specialId: 'primeElements', specialDesc: 'Prime bonus' } },
],
},
{
tier: 4,
skillId: 'elemAttune_t4',
name: 'Elemental Sovereign',
multiplier: 1000,
upgrades: [
{ id: 'ea_t4_l5_sovereign', name: 'Sovereign Elements', desc: '+200% element cap', milestone: 5, effect: { type: 'multiplier', stat: 'elementCap', value: 3 } },
{ id: 'ea_t4_l5_exotic', name: 'Exotic Mastery', desc: 'Exotic elements +100% damage', milestone: 5, effect: { type: 'special', specialId: 'exoticMastery', specialDesc: 'Exotic bonus' } },
{ id: 'ea_t4_l5_infinite', name: 'Infinite Elements', desc: 'Element cap scales with loop count', milestone: 5, effect: { type: 'special', specialId: 'infiniteElements', specialDesc: 'Loop scaling cap' } },
{ id: 'ea_t4_l5_conduit', name: 'Element Conduit', desc: 'Channel all elements at once for combined damage', milestone: 5, effect: { type: 'special', specialId: 'elementConduit', specialDesc: 'All elements' } },
{ id: 'ea_t4_l10_ascension', name: 'Element Ascension', desc: '+5000 element cap', milestone: 10, effect: { type: 'bonus', stat: 'elementCap', value: 5000 } },
{ id: 'ea_t4_l10_omega', name: 'Omega Element', desc: 'Unlock the Omega element (combines all)', milestone: 10, effect: { type: 'special', specialId: 'omegaElement', specialDesc: 'Omega unlock' } },
{ id: 'ea_t4_l10_perfect', name: 'Perfect Attunement', desc: 'All elements at max power always', milestone: 10, effect: { type: 'special', specialId: 'perfectAttunement', specialDesc: 'Perfect power' } },
{ id: 'ea_t4_l10_storm', name: 'Element Storm', desc: 'All elements attack together for 3x damage', milestone: 10, effect: { type: 'special', specialId: 'elementStorm', specialDesc: 'Combined attack' } },
],
},
{
tier: 5,
skillId: 'elemAttune_t5',
name: 'Primordial Element',
multiplier: 10000,
upgrades: [
{ id: 'ea_t5_l5_primordial', name: 'Primordial Power', desc: '+500% element cap', milestone: 5, effect: { type: 'multiplier', stat: 'elementCap', value: 6 } },
{ id: 'ea_t5_l5_omega', name: 'Omega Mastery', desc: 'Control the Omega element (all elements in one)', milestone: 5, effect: { type: 'special', specialId: 'omegaMastery', specialDesc: 'Omega control' } },
{ id: 'ea_t5_l5_origin', name: 'Element Origin', desc: 'You are the source of elements', milestone: 5, effect: { type: 'special', specialId: 'elementOrigin', specialDesc: 'Element source' } },
{ id: 'ea_t5_l5_transcend', name: 'Element Transcendence', desc: 'Transcend elemental limits', milestone: 5, effect: { type: 'special', specialId: 'elementTranscendence', specialDesc: 'Limitless' } },
{ id: 'ea_t5_l10_godhood', name: 'Element Godhood', desc: '+50000 element cap', milestone: 10, effect: { type: 'bonus', stat: 'elementCap', value: 50000 } },
{ id: 'ea_t5_l10_victory', name: 'Element Victory', desc: 'Victory: master all elements', milestone: 10, effect: { type: 'special', specialId: 'elementVictory', specialDesc: 'Element victory' } },
{ id: 'ea_t5_l10_eternal', name: 'Eternal Elements', desc: 'Elements never deplete', milestone: 10, effect: { type: 'special', specialId: 'eternalElements', specialDesc: 'Infinite elements' } },
{ id: 'ea_t5_l10_ultimate', name: 'Ultimate Element', desc: 'Create your own element', milestone: 10, effect: { type: 'special', specialId: 'ultimateElement', specialDesc: 'Custom element' } },
],
},
],
},
// ─── Enchanting Skills Evolution Paths ─────────────────────────────────────
enchanting: {
baseSkillId: 'enchanting',
tiers: [
{
tier: 1,
skillId: 'enchanting',
name: 'Enchanting',
multiplier: 1,
upgrades: [
{ id: 'ench_t1_l5_capacity', name: 'Efficient Runes', desc: '-10% enchantment capacity cost', milestone: 5, effect: { type: 'multiplier', stat: 'enchantCapacityCost', value: 0.9 } },
{ id: 'ench_t1_l5_speed', name: 'Quick Scribing', desc: '-15% design time', milestone: 5, effect: { type: 'multiplier', stat: 'designTime', value: 0.85 } },
{ id: 'ench_t1_l5_power', name: 'Potent Enchantments', desc: '+10% effect power', milestone: 5, effect: { type: 'multiplier', stat: 'enchantPower', value: 1.1 } },
{ id: 'ench_t1_l5_stable', name: 'Stable Runes', desc: 'Enchantments never degrade', milestone: 5, effect: { type: 'special', specialId: 'stableRunes', specialDesc: 'No degradation' } },
{ id: 'ench_t1_l10_master', name: 'Enchant Master', desc: '-20% capacity cost', milestone: 10, effect: { type: 'multiplier', stat: 'enchantCapacityCost', value: 0.8 } },
{ id: 'ench_t1_l10_speed', name: 'Swift Enchanter', desc: '-25% all enchant times', milestone: 10, effect: { type: 'multiplier', stat: 'enchantTime', value: 0.75 } },
{ id: 'ench_t1_l10_double', name: 'Double Enchant', desc: '10% chance to apply enchant twice', milestone: 10, effect: { type: 'special', specialId: 'doubleEnchant', specialDesc: 'Double application' } },
{ id: 'ench_t1_l10_quality', name: 'Quality Craft', desc: '+25% effect power', milestone: 10, effect: { type: 'multiplier', stat: 'enchantPower', value: 1.25 } },
],
},
{
tier: 2,
skillId: 'enchanting_t2',
name: 'Rune Master',
multiplier: 10,
upgrades: [
{ id: 'ench_t2_l5_advanced', name: 'Advanced Runes', desc: '-25% capacity cost', milestone: 5, effect: { type: 'multiplier', stat: 'enchantCapacityCost', value: 0.75 } },
{ id: 'ench_t2_l5_quick', name: 'Quick Application', desc: '-30% application time', milestone: 5, effect: { type: 'multiplier', stat: 'applicationTime', value: 0.7 } },
{ id: 'ench_t2_l5_overcharge', name: 'Overcharge', desc: 'Effects are 50% stronger', milestone: 5, effect: { type: 'multiplier', stat: 'enchantPower', value: 1.5 } },
{ id: 'ench_t2_l5_save', name: 'Design Memory', desc: 'Save 3 designs per equipment type', milestone: 5, effect: { type: 'special', specialId: 'designMemory', specialDesc: 'More designs' } },
{ id: 'ench_t2_l10_expert', name: 'Expert Enchanter', desc: '-40% all costs', milestone: 10, effect: { type: 'multiplier', stat: 'enchantCapacityCost', value: 0.6 } },
{ id: 'ench_t2_l10_rapid', name: 'Rapid Enchant', desc: '-50% all times', milestone: 10, effect: { type: 'multiplier', stat: 'enchantTime', value: 0.5 } },
{ id: 'ench_t2_l10_triple', name: 'Triple Chance', desc: '15% chance for triple effect', milestone: 10, effect: { type: 'special', specialId: 'tripleEnchant', specialDesc: 'Triple chance' } },
{ id: 'ench_t2_l10_essence', name: 'Essence Infusion', desc: 'Enchantments grant bonus mana', milestone: 10, effect: { type: 'special', specialId: 'essenceInfusion', specialDesc: 'Mana from enchants' } },
],
},
{
tier: 3,
skillId: 'enchanting_t3',
name: 'Arcane Forgemaster',
multiplier: 100,
upgrades: [
{ id: 'ench_t3_l5_efficient', name: 'Efficient Runes', desc: '-50% capacity cost', milestone: 5, effect: { type: 'multiplier', stat: 'enchantCapacityCost', value: 0.5 } },
{ id: 'ench_t3_l5_instant', name: 'Instant Prep', desc: 'Preparation takes 1 hour', milestone: 5, effect: { type: 'special', specialId: 'instantPrep', specialDesc: 'Fast prep' } },
{ id: 'ench_t3_l5_mighty', name: 'Mighty Enchantments', desc: '+100% effect power', milestone: 5, effect: { type: 'multiplier', stat: 'enchantPower', value: 2 } },
{ id: 'ench_t3_l5_transfer', name: 'Enchant Transfer', desc: 'Move enchantments between items', milestone: 5, effect: { type: 'special', specialId: 'enchantTransfer', specialDesc: 'Transfer enchants' } },
{ id: 'ench_t3_l10_master', name: 'Forge Master', desc: '-60% capacity cost', milestone: 10, effect: { type: 'multiplier', stat: 'enchantCapacityCost', value: 0.4 } },
{ id: 'ench_t3_l10_spellweave', name: 'Spellweaving', desc: 'Combine spell effects', milestone: 10, effect: { type: 'special', specialId: 'spellweaving', specialDesc: 'Combine spells' } },
{ id: 'ench_t3_l10_legendary', name: 'Legendary Enchanter', desc: 'Create legendary tier items', milestone: 10, effect: { type: 'special', specialId: 'legendaryEnchanter', specialDesc: 'Legendary tier' } },
{ id: 'ench_t3_l10_eternal', name: 'Eternal Enchantments', desc: 'Enchantments last forever', milestone: 10, effect: { type: 'special', specialId: 'eternalEnchantments', specialDesc: 'Permanent enchants' } },
],
},
{
tier: 4,
skillId: 'enchanting_t4',
name: 'Void Enchanter',
multiplier: 1000,
upgrades: [
{ id: 'ench_t4_l5_void', name: 'Void Runes', desc: '-70% capacity cost', milestone: 5, effect: { type: 'multiplier', stat: 'enchantCapacityCost', value: 0.3 } },
{ id: 'ench_t4_l5_instant', name: 'Instant Design', desc: 'Designs complete instantly', milestone: 5, effect: { type: 'special', specialId: 'instantDesign', specialDesc: 'Instant design' } },
{ id: 'ench_t4_l5_power', name: 'Void Power', desc: '+200% effect power', milestone: 5, effect: { type: 'multiplier', stat: 'enchantPower', value: 3 } },
{ id: 'ench_t4_l5_copy', name: 'Enchant Copy', desc: 'Copy enchantments from other items', milestone: 5, effect: { type: 'special', specialId: 'enchantCopy', specialDesc: 'Copy enchants' } },
{ id: 'ench_t4_l10_transcend', name: 'Transcendent Enchanting', desc: '-80% capacity cost', milestone: 10, effect: { type: 'multiplier', stat: 'enchantCapacityCost', value: 0.2 } },
{ id: 'ench_t4_l10_mythic', name: 'Mythic Crafter', desc: 'Create mythic tier items', milestone: 10, effect: { type: 'special', specialId: 'mythicCrafter', specialDesc: 'Mythic tier' } },
{ id: 'ench_t4_l10_soulbind', name: 'Soulbinding', desc: 'Enchantments persist through loops', milestone: 10, effect: { type: 'special', specialId: 'soulbinding', specialDesc: 'Loop persistence' } },
{ id: 'ench_t4_l10_overflow', name: 'Capacity Overflow', desc: 'Equipment can exceed capacity limits', milestone: 10, effect: { type: 'special', specialId: 'capacityOverflow', specialDesc: 'Overfill capacity' } },
],
},
{
tier: 5,
skillId: 'enchanting_t5',
name: 'Enchantment God',
multiplier: 10000,
upgrades: [
{ id: 'ench_t5_l5_godhood', name: 'Enchant Godhood', desc: '-90% capacity cost', milestone: 5, effect: { type: 'multiplier', stat: 'enchantCapacityCost', value: 0.1 } },
{ id: 'ench_t5_l5_instant', name: 'Instant All', desc: 'All enchanting is instant', milestone: 5, effect: { type: 'special', specialId: 'instantAllEnchant', specialDesc: 'All instant' } },
{ id: 'ench_t5_l5_ultimate', name: 'Ultimate Power', desc: '+500% effect power', milestone: 5, effect: { type: 'multiplier', stat: 'enchantPower', value: 6 } },
{ id: 'ench_t5_l5_create', name: 'Create Effects', desc: 'Design custom enchantment effects', milestone: 5, effect: { type: 'special', specialId: 'createEffects', specialDesc: 'Custom effects' } },
{ id: 'ench_t5_l10_perfection', name: 'Perfect Enchanting', desc: 'All costs are 0', milestone: 10, effect: { type: 'special', specialId: 'perfectEnchanting', specialDesc: 'Zero costs' } },
{ id: 'ench_t5_l10_victory', name: 'Enchant Victory', desc: 'Victory: enchant anything', milestone: 10, effect: { type: 'special', specialId: 'enchantVictory', specialDesc: 'Enchant victory' } },
{ id: 'ench_t5_l10_infinite', name: 'Infinite Capacity', desc: 'No capacity limits on equipment', milestone: 10, effect: { type: 'special', specialId: 'infiniteCapacity', specialDesc: 'Infinite capacity' } },
{ id: 'ench_t5_l10_origin', name: 'Enchantment Origin', desc: 'You are the source of all enchantments', milestone: 10, effect: { type: 'special', specialId: 'enchantmentOrigin', specialDesc: 'Enchant source' } },
],
},
],
},
efficientEnchant: {
baseSkillId: 'efficientEnchant',
tiers: [
{
tier: 1,
skillId: 'efficientEnchant',
name: 'Efficient Enchant',
multiplier: 1,
upgrades: [
{ id: 'ee_t1_l5_extra', name: 'Extra Efficiency', desc: '-10% additional capacity cost', milestone: 5, effect: { type: 'bonus', stat: 'enchantEfficiency', value: 0.1 } },
{ id: 'ee_t1_l5_stacking', name: 'Stacking Efficiency', desc: 'Each enchant is 5% cheaper', milestone: 5, effect: { type: 'special', specialId: 'stackingEfficiency', specialDesc: 'Stacking discount' } },
{ id: 'ee_t1_l10_master', name: 'Efficiency Master', desc: '-20% capacity cost', milestone: 10, effect: { type: 'bonus', stat: 'enchantEfficiency', value: 0.2 } },
{ id: 'ee_t1_l10_overflow', name: 'Efficient Overflow', desc: 'Spare capacity becomes bonus power', milestone: 10, effect: { type: 'special', specialId: 'efficientOverflow', specialDesc: 'Capacity to power' } },
],
},
],
},
disenchanting: {
baseSkillId: 'disenchanting',
tiers: [
{
tier: 1,
skillId: 'disenchanting',
name: 'Disenchanting',
multiplier: 1,
upgrades: [
{ id: 'dis_t1_l5_recover', name: 'Better Recovery', desc: '+15% mana recovery', milestone: 5, effect: { type: 'bonus', stat: 'disenchantRecovery', value: 0.15 } },
{ id: 'dis_t1_l5_partial', name: 'Partial Disenchant', desc: 'Remove individual enchantments', milestone: 5, effect: { type: 'special', specialId: 'partialDisenchant', specialDesc: 'Selective removal' } },
{ id: 'dis_t1_l10_full', name: 'Full Recovery', desc: '+30% mana recovery', milestone: 10, effect: { type: 'bonus', stat: 'disenchantRecovery', value: 0.3 } },
{ id: 'dis_t1_l10_salvage', name: 'Effect Salvage', desc: 'Save removed effects as scrolls', milestone: 10, effect: { type: 'special', specialId: 'effectSalvage', specialDesc: 'Save as scroll' } },
],
},
],
},
enchantSpeed: {
baseSkillId: 'enchantSpeed',
tiers: [
{
tier: 1,
skillId: 'enchantSpeed',
name: 'Enchant Speed',
multiplier: 1,
upgrades: [
{ id: 'es_t1_l5_haste', name: 'Enchant Haste', desc: '-15% all enchant times', milestone: 5, effect: { type: 'multiplier', stat: 'enchantTime', value: 0.85 } },
{ id: 'es_t1_l5_parallel', name: 'Parallel Enchant', desc: 'Work on 2 items at once', milestone: 5, effect: { type: 'special', specialId: 'parallelEnchant', specialDesc: 'Dual work' } },
{ id: 'es_t1_l10_swift', name: 'Swift Enchanter', desc: '-30% all enchant times', milestone: 10, effect: { type: 'multiplier', stat: 'enchantTime', value: 0.7 } },
{ id: 'es_t1_l10_instant', name: 'Quick Design', desc: 'Designs complete in half time', milestone: 10, effect: { type: 'special', specialId: 'quickDesign', specialDesc: 'Fast design' } },
],
},
],
},
};
// ─── Get Upgrades for Skill at Milestone ──────────────────────────────────────────
export function getUpgradesForSkillAtMilestone(
skillId: string,
milestone: 5 | 10,
skillTiers: Record<string, number> = {}
): SkillUpgradeChoice[] {
// Find the base skill and current tier
let baseSkillId = skillId;
let currentTier = skillTiers[skillId] || 1;
// Check if this is a tier skill (e.g., 'manaWell_t2')
if (skillId.includes('_t')) {
const parts = skillId.split('_t');
baseSkillId = parts[0];
currentTier = parseInt(parts[1]) || 1;
}
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
if (!path) return [];
const tierDef = path.tiers.find(t => t.tier === currentTier);
if (!tierDef) return [];
return tierDef.upgrades.filter(u => u.milestone === milestone);
}
// ─── Get Next Tier Skill ─────────────────────────────────────────────────────────
export function getNextTierSkill(skillId: string): string | null {
let baseSkillId = skillId;
let currentTier = 1;
if (skillId.includes('_t')) {
const parts = skillId.split('_t');
baseSkillId = parts[0];
currentTier = parseInt(parts[1]) || 1;
}
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
if (!path) return null;
const nextTier = path.tiers.find(t => t.tier === currentTier + 1);
return nextTier?.skillId || null;
}
// ─── Get Tier Multiplier ─────────────────────────────────────────────────────────
export function getTierMultiplier(skillId: string): number {
let baseSkillId = skillId;
let currentTier = 1;
if (skillId.includes('_t')) {
const parts = skillId.split('_t');
baseSkillId = parts[0];
currentTier = parseInt(parts[1]) || 1;
}
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
if (!path) return 1;
const tierDef = path.tiers.find(t => t.tier === currentTier);
return tierDef?.multiplier || 1;
}
// ─── Generate Tier Skills Dynamically ─────────────────────────────────────────────
export function generateTierSkillDef(baseSkillId: string, tier: number): SkillDef | null {
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
if (!path) return null;
const tierDef = path.tiers.find(t => t.tier === tier);
if (!tierDef) return null;
const baseDef = path.tiers[0];
return {
name: tierDef.name,
desc: `Tier ${tier} evolution - ${tierDef.multiplier}x base effect`,
cat: getCategoryForBaseSkill(baseSkillId),
max: 10,
base: 100 * tier, // Cost scales with tier
studyTime: 4 * tier, // Study time scales with tier
tier: tier,
baseSkill: baseSkillId,
tierMultiplier: tierDef.multiplier,
};
}
function getCategoryForBaseSkill(baseSkillId: string): string {
const categoryMap: Record<string, string> = {
manaWell: 'mana',
manaFlow: 'mana',
combatTrain: 'combat',
quickLearner: 'study',
focusedMind: 'study',
elemAttune: 'mana',
};
return categoryMap[baseSkillId] || 'study';
}

1900
src/lib/game/store.ts Executable file

File diff suppressed because it is too large Load Diff

180
src/lib/game/study-slice.ts Executable file
View File

@@ -0,0 +1,180 @@
// ─── Study Slice ─────────────────────────────────────────────────────────────
// Actions for studying skills and spells
import type { GameState } from './types';
import { SKILLS_DEF, SPELLS_DEF, getStudyCostMultiplier } from './constants';
// ─── Study Actions Interface ──────────────────────────────────────────────────
export interface StudyActions {
startStudyingSkill: (skillId: string) => void;
startStudyingSpell: (spellId: string) => void;
cancelStudy: () => void;
startParallelStudySkill: (skillId: string) => void;
cancelParallelStudy: () => void;
}
// ─── Study Slice Factory ──────────────────────────────────────────────────────
export function createStudySlice(
set: (partial: Partial<GameState> | ((state: GameState) => Partial<GameState>)) => void,
get: () => GameState
): StudyActions {
return {
// Start studying a skill - mana is deducted per hour, not upfront
startStudyingSkill: (skillId: string) => {
const state = get();
const sk = SKILLS_DEF[skillId];
if (!sk) return;
const currentLevel = state.skills[skillId] || 0;
if (currentLevel >= sk.max) return;
// Check prerequisites
if (sk.req) {
for (const [r, rl] of Object.entries(sk.req)) {
if ((state.skills[r] || 0) < rl) return;
}
}
// Calculate total mana cost and cost per hour
const costMult = getStudyCostMultiplier(state.skills);
const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult);
const manaCostPerHour = Math.ceil(totalCost / sk.studyTime);
// Must have at least 1 hour worth of mana to start
if (state.rawMana < manaCostPerHour) return;
// Start studying (no upfront cost - mana is deducted per hour during study)
set({
currentAction: 'study',
currentStudyTarget: {
type: 'skill',
id: skillId,
progress: state.skillProgress[skillId] || 0,
required: sk.studyTime,
manaCostPerHour: manaCostPerHour,
totalCost: totalCost,
},
log: [`📚 Started studying ${sk.name} (${manaCostPerHour} mana/hr)...`, ...state.log.slice(0, 49)],
});
},
// Start studying a spell
startStudyingSpell: (spellId: string) => {
const state = get();
const sp = SPELLS_DEF[spellId];
if (!sp || state.spells[spellId]?.learned) return;
// Calculate total mana cost and cost per hour
const costMult = getStudyCostMultiplier(state.skills);
const totalCost = Math.floor(sp.unlock * costMult);
const studyTime = sp.studyTime || (sp.tier * 4);
const manaCostPerHour = Math.ceil(totalCost / studyTime);
// Must have at least 1 hour worth of mana to start
if (state.rawMana < manaCostPerHour) return;
// Start studying (no upfront cost - mana is deducted per hour during study)
set({
currentAction: 'study',
currentStudyTarget: {
type: 'spell',
id: spellId,
progress: state.spells[spellId]?.studyProgress || 0,
required: studyTime,
manaCostPerHour: manaCostPerHour,
totalCost: totalCost,
},
spells: {
...state.spells,
[spellId]: { ...(state.spells[spellId] || { learned: false, level: 0 }), studyProgress: state.spells[spellId]?.studyProgress || 0 },
},
log: [`📚 Started studying ${sp.name} (${manaCostPerHour} mana/hr)...`, ...state.log.slice(0, 49)],
});
},
// Cancel current study (saves progress)
cancelStudy: () => {
const state = get();
if (!state.currentStudyTarget) return;
// Knowledge retention bonus
const retentionBonus = 1 + (state.skills.knowledgeRetention || 0) * 0.2;
const savedProgress = Math.min(
state.currentStudyTarget.progress,
state.currentStudyTarget.required * retentionBonus
);
// Save progress
if (state.currentStudyTarget.type === 'skill') {
set({
currentStudyTarget: null,
currentAction: 'meditate',
skillProgress: {
...state.skillProgress,
[state.currentStudyTarget.id]: savedProgress,
},
log: [`📖 Study interrupted. Progress saved.`, ...state.log.slice(0, 49)],
});
} else if (state.currentStudyTarget.type === 'spell') {
set({
currentStudyTarget: null,
currentAction: 'meditate',
spells: {
...state.spells,
[state.currentStudyTarget.id]: {
...(state.spells[state.currentStudyTarget.id] || { learned: false, level: 0 }),
studyProgress: savedProgress,
},
},
log: [`📖 Study interrupted. Progress saved.`, ...state.log.slice(0, 49)],
});
}
},
// Start parallel study of a skill (requires Parallel Mind upgrade)
startParallelStudySkill: (skillId: string) => {
const state = get();
if (state.parallelStudyTarget) return; // Already have parallel study
if (!state.currentStudyTarget) return; // Need primary study
const sk = SKILLS_DEF[skillId];
if (!sk) return;
const currentLevel = state.skills[skillId] || 0;
if (currentLevel >= sk.max) return;
// Can't study same thing in parallel
if (state.currentStudyTarget.id === skillId) return;
// Calculate mana cost for parallel study
const costMult = getStudyCostMultiplier(state.skills);
const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult);
const manaCostPerHour = Math.ceil(totalCost / sk.studyTime);
set({
parallelStudyTarget: {
type: 'skill',
id: skillId,
progress: state.skillProgress[skillId] || 0,
required: sk.studyTime,
manaCostPerHour: Math.ceil(manaCostPerHour / 2), // Half speed = half mana cost per tick
totalCost: totalCost,
},
log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)],
});
},
// Cancel parallel study
cancelParallelStudy: () => {
set((state) => {
if (!state.parallelStudyTarget) return state;
return {
parallelStudyTarget: null,
log: ['📖 Parallel study cancelled.', ...state.log.slice(0, 49)],
};
});
},
};
}

448
src/lib/game/types.ts Executable file
View File

@@ -0,0 +1,448 @@
// ─── Game Types ───────────────────────────────────────────────────────────────
export type ElementCategory = 'base' | 'utility' | 'composite' | 'exotic';
// Attunement body slots
export type AttunementSlot = 'rightHand' | 'leftHand' | 'head' | 'back' | 'chest' | 'leftLeg' | 'rightLeg';
// Attunement definition
export interface AttunementDef {
id: string;
name: string;
desc: string;
slot: AttunementSlot;
icon: string;
color: string;
primaryManaType?: string; // Primary mana type this attunement generates (null for Invoker)
rawManaRegen: number; // Raw mana regeneration per hour granted by this attunement
conversionRate: number; // Raw mana converted to primary type per hour
unlocked: boolean; // Whether this is unlocked by default
unlockCondition?: string; // Description of how to unlock (for future challenges)
capabilities: string[]; // What this attunement enables (e.g., 'enchanting', 'pacts', 'golemCrafting')
skillCategories: string[]; // Skill categories this attunement provides access to
}
// Attunement instance state (tracks player's attunements)
export interface AttunementState {
id: string;
active: boolean; // Whether this attunement is currently active
level: number; // Attunement level (for future progression)
experience: number; // Progress toward next level
}
export interface ElementDef {
name: string;
sym: string;
color: string;
glow: string;
cat: ElementCategory;
recipe?: string[];
}
export interface ElementState {
current: number;
max: number;
unlocked: boolean;
}
// Boon types that guardians can grant
export interface GuardianBoon {
type: 'maxMana' | 'manaRegen' | 'castingSpeed' | 'elementalDamage' | 'rawDamage' |
'critChance' | 'critDamage' | 'spellEfficiency' | 'manaGain' | 'insightGain' |
'studySpeed' | 'prestigeInsight';
value: number;
desc: string;
}
export interface GuardianDef {
name: string;
element: string;
hp: number;
pact: number; // Pact multiplier when signed
color: string;
boons: GuardianBoon[]; // Bonuses granted when pact is signed
pactCost: number; // Mana cost to perform pact ritual
pactTime: number; // Hours required for pact ritual
uniquePerk: string; // Description of unique perk
}
// Spell cost can be raw mana or elemental mana
export interface SpellCost {
type: 'raw' | 'element'; // 'raw' for raw mana, 'element' for specific elemental mana
element?: string; // Required if type is 'element'
amount: number; // Amount of mana required
}
export interface SpellDef {
name: string;
elem: string; // Element type for damage calculations
dmg: number;
cost: SpellCost; // Changed from number to SpellCost object
tier: number;
unlock: number; // Mana cost to start studying
studyTime?: number; // Hours needed to study (optional, defaults based on tier)
castSpeed?: number; // Casts per hour (default 1, higher = faster)
desc?: string; // Optional spell description
effects?: SpellEffect[]; // Optional special effects
}
export interface SpellEffect {
type: 'lifesteal' | 'burn' | 'freeze' | 'stun' | 'pierce' | 'multicast' | 'shield' | 'buff';
value: number; // Effect potency
duration?: number; // Duration in hours for timed effects
}
export interface SpellState {
learned: boolean;
level: number;
studyProgress?: number; // Hours studied so far (for in-progress spells)
}
export interface SkillDef {
name: string;
desc: string;
cat: string;
attunement?: string; // Which attunement this skill belongs to (null = core)
max: number;
base: number; // Mana cost to start studying
req?: Record<string, number>;
studyTime: number; // Hours needed to study
level?: number; // Current level (optional, for UI display)
tier?: number; // Skill tier (1-5)
tierUp?: string; // Skill ID this evolves into at max level
baseSkill?: string; // Original skill ID this evolved from
tierMultiplier?: number; // Multiplier for each tier (default 2)
}
// Skill upgrade choices at milestones (level 5 and level 10)
export interface SkillUpgradeDef {
id: string;
name: string;
desc: string;
skillId: string; // Which skill this upgrade belongs to
milestone: 5 | 10; // Level at which this upgrade is available
effect: SkillUpgradeEffect;
}
export interface SkillUpgradeEffect {
type: 'multiplier' | 'bonus' | 'special';
stat?: string; // Stat to modify
value?: number; // Multiplier or bonus value
specialId?: string; // Special effect identifier
specialDesc?: string; // Description of special effect
}
// Skill evolution system
export interface SkillEvolutionPath {
baseSkillId: string; // Starting skill ID
tiers: SkillTierDef[]; // 5 tiers of evolution
}
export interface SkillTierDef {
tier: number;
skillId: string; // Skill ID for this tier
name: string;
multiplier: number; // Base effect multiplier
upgrades: SkillUpgradeChoice[]; // 4 upgrades available at each milestone
}
export interface SkillUpgradeChoice {
id: string;
name: string;
desc: string;
milestone: 5 | 10; // Level at which this upgrade is available
effect: SkillUpgradeEffect;
}
export interface PrestigeDef {
name: string;
desc: string;
max: number;
cost: number;
}
// Legacy EquipmentDef for backward compatibility
export interface EquipmentDef {
id: string;
name: string;
slot: 'mainHand' | 'offHand' | 'head' | 'body' | 'hands' | 'accessory';
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' | 'mythic';
stats: Record<string, number>;
durability: number;
maxDurability: number;
element?: string;
}
// Equipment Instance (actual equipped item with enchantments)
export interface EquipmentInstance {
instanceId: string; // Unique ID for this specific item
typeId: string; // Reference to EquipmentType (e.g., 'basicStaff')
name: string; // Display name (defaults to type name)
enchantments: AppliedEnchantment[];
usedCapacity: number; // Currently used capacity
totalCapacity: number; // Base capacity + bonuses
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' | 'mythic';
quality: number; // 0-100, affects capacity efficiency
}
export interface AppliedEnchantment {
effectId: string; // Reference to EnchantmentEffectDef
stacks: number; // Number of times this effect is applied
actualCost: number; // Actual capacity cost (after efficiency)
}
// Enchantment Design (saved design for later application)
export interface EnchantmentDesign {
id: string;
name: string;
equipmentType: string; // Which equipment type this is designed for
effects: DesignEffect[];
totalCapacityUsed: number;
designTime: number; // Hours required to design
created: number; // Timestamp
}
export interface DesignEffect {
effectId: string;
stacks: number;
capacityCost: number;
}
// Crafting Progress States
export interface DesignProgress {
designId: string;
progress: number; // Hours spent designing
required: number; // Total hours needed
}
export interface PreparationProgress {
equipmentInstanceId: string;
progress: number; // Hours spent preparing
required: number; // Total hours needed
manaCostPaid: number; // Mana cost already paid
}
export interface ApplicationProgress {
equipmentInstanceId: string;
designId: string;
progress: number; // Hours spent applying
required: number; // Total hours needed
manaPerHour: number; // Mana cost per hour
paused: boolean;
manaSpent: number; // Total mana spent so far
}
// Equipment crafting progress (from blueprints)
export interface EquipmentCraftingProgress {
blueprintId: string;
equipmentTypeId: string;
progress: number; // Hours spent crafting
required: number; // Total hours needed
manaSpent: number; // Total mana spent so far
}
// Equipment spell state (for multi-spell casting)
export interface EquipmentSpellState {
spellId: string;
sourceEquipment: string; // Equipment instance ID
castProgress: number; // 0-1 progress toward next cast
}
export interface BlueprintDef {
id: string;
name: string;
tier: number;
slot: string;
stats: Record<string, number>;
studyTime: number;
craftTime: number;
craftCost: number;
discovered: boolean;
learned: boolean;
}
// Loot inventory for materials and blueprints
export interface LootInventory {
materials: Record<string, number>; // materialId -> count
blueprints: string[]; // blueprint IDs discovered
}
// Achievement definitions
export interface AchievementDef {
id: string;
name: string;
desc: string;
category: string;
requirement: {
type: string;
value: number;
subType?: string;
};
reward: {
insight?: number;
manaBonus?: number;
damageBonus?: number;
regenBonus?: number;
title?: string;
unlockEffect?: string;
};
hidden?: boolean;
}
// Achievement state tracks unlocked achievements and progress
export interface AchievementState {
unlocked: string[]; // IDs of unlocked achievements
progress: Record<string, number>; // Progress toward achievement requirements
}
export type GameAction = 'meditate' | 'climb' | 'study' | 'craft' | 'repair' | 'convert' | 'design' | 'prepare' | 'enchant';
export interface ScheduleBlock {
id: string;
action: GameAction;
startHour: number;
endHour: number;
enabled: boolean;
target?: string; // spell id, blueprint id, skill id, element id
}
export interface StudyTarget {
type: 'skill' | 'spell' | 'blueprint';
id: string;
progress: number; // Hours studied
required: number; // Total hours needed
}
// Combo state for combat
export interface ComboState {
count: number; // Current combo hits
maxCombo: number; // Highest combo this session
multiplier: number; // Current damage multiplier
elementChain: string[]; // Last 3 elements used
decayTimer: number; // Hours until decay starts
}
export interface GameState {
// Time
day: number;
hour: number;
loopCount: number;
gameOver: boolean;
victory: boolean;
paused: boolean;
// Raw Mana
rawMana: number;
meditateTicks: number;
totalManaGathered: number;
// Attunements (class-like system)
attunements: Record<string, AttunementState>; // attunement id -> state
// Elements
elements: Record<string, ElementState>;
// Spire
currentFloor: number;
floorHP: number;
floorMaxHP: number;
maxFloorReached: number;
signedPacts: number[];
activeSpell: string;
currentAction: GameAction;
castProgress: number; // Progress towards next spell cast (0-1)
combo: ComboState; // Combat combo tracking
// Spells
spells: Record<string, SpellState>;
// Skills
skills: Record<string, number>;
skillProgress: Record<string, number>; // Saved study progress for skills
skillUpgrades: Record<string, string[]>; // Selected upgrade IDs per skill
skillTiers: Record<string, number>; // Current tier for each base skill
// Equipment System (new instance-based system)
equippedInstances: Record<string, string | null>; // slot -> instanceId
equipmentInstances: Record<string, EquipmentInstance>; // instanceId -> instance
enchantmentDesigns: EnchantmentDesign[]; // Saved enchantment designs
// Crafting Progress
designProgress: DesignProgress | null;
preparationProgress: PreparationProgress | null;
applicationProgress: ApplicationProgress | null;
equipmentCraftingProgress: EquipmentCraftingProgress | null;
// Unlocked enchantment effects for designing
unlockedEffects: string[]; // Effect IDs that have been researched
// Equipment spell states for multi-casting
equipmentSpellStates: EquipmentSpellState[];
// Legacy Equipment (for backward compatibility)
equipment: Record<string, EquipmentDef | null>;
inventory: EquipmentDef[];
// Blueprints
blueprints: Record<string, BlueprintDef>;
// Loot Inventory
lootInventory: LootInventory;
// Schedule
schedule: ScheduleBlock[];
autoSchedule: boolean;
studyQueue: string[];
craftQueue: string[];
// Current Study Target
currentStudyTarget: StudyTarget | null;
// Parallel Study Target (for Parallel Mind milestone upgrade)
parallelStudyTarget: StudyTarget | null;
// Achievements
achievements: AchievementState;
// Stats tracking
totalSpellsCast: number;
totalDamageDealt: number;
totalCraftsCompleted: number;
// Prestige
insight: number;
totalInsight: number;
prestigeUpgrades: Record<string, number>;
memorySlots: number;
memories: string[];
// Incursion
incursionStrength: number;
containmentWards: number;
// Log
log: string[];
// Loop insight (earned at end of current loop)
loopInsight: number;
}
// Action types for the store
export type GameActionType =
| { type: 'TICK' }
| { type: 'GATHER_MANA' }
| { type: 'SET_ACTION'; action: GameAction }
| { type: 'SET_SPELL'; spellId: string }
| { type: 'LEARN_SPELL'; spellId: string }
| { type: 'START_STUDYING_SKILL'; skillId: string }
| { type: 'START_STUDYING_SPELL'; spellId: string }
| { type: 'CANCEL_STUDY' }
| { type: 'CONVERT_MANA'; element: string; amount: number }
| { type: 'UNLOCK_ELEMENT'; element: string }
| { type: 'CRAFT_COMPOSITE'; target: string }
| { type: 'DO_PRESTIGE'; id: string }
| { type: 'START_NEW_LOOP' }
| { type: 'TOGGLE_PAUSE' }
| { type: 'RESET_GAME' }
| { type: 'SELECT_SKILL_UPGRADE'; skillId: string; upgradeId: string }
| { type: 'TIER_UP_SKILL'; skillId: string };

362
src/lib/game/upgrade-effects.ts Executable file
View 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;
}

372
src/lib/game/utils.ts Executable file
View File

@@ -0,0 +1,372 @@
// ─── Game Utilities ───────────────────────────────────────────────────────────
import type { GameState, SpellCost } from './types';
import type { ComputedEffects } from './upgrade-effects';
import {
GUARDIANS,
SPELLS_DEF,
FLOOR_ELEM_CYCLE,
HOURS_PER_TICK,
MAX_DAY,
INCURSION_START_DAY,
ELEMENT_OPPOSITES,
} from './constants';
// ─── Formatting Functions ─────────────────────────────────────────────────────
export function fmt(n: number): string {
if (!isFinite(n) || isNaN(n)) return '0';
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
return Math.floor(n).toString();
}
export function fmtDec(n: number, d: number = 1): string {
return isFinite(n) ? n.toFixed(d) : '0';
}
// ─── Floor Helpers ────────────────────────────────────────────────────────────
export function getFloorMaxHP(floor: number): number {
if (GUARDIANS[floor]) return GUARDIANS[floor].hp;
// Improved scaling: slower early game, faster late game
const baseHP = 100;
const floorScaling = floor * 50;
const exponentialScaling = Math.pow(floor, 1.7);
return Math.floor(baseHP + floorScaling + exponentialScaling);
}
export function getFloorElement(floor: number): string {
return FLOOR_ELEM_CYCLE[(floor - 1) % 8];
}
// ─── Computed Stats Functions ─────────────────────────────────────────────────
export function computeMaxMana(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
): number {
const pu = state.prestigeUpgrades;
const base =
100 +
(state.skills.manaWell || 0) * 100 +
(pu.manaWell || 0) * 500;
// Apply upgrade effects if provided
if (effects) {
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
}
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;
}
export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
): number {
const pu = state.prestigeUpgrades;
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
const base =
2 +
(state.skills.manaFlow || 0) * 1 +
(state.skills.manaSpring || 0) * 2 +
(pu.manaFlow || 0) * 0.5;
let regen = base * temporalBonus;
// Apply upgrade effects if provided
if (effects) {
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
}
return regen;
}
/**
* Compute regen with dynamic special effects (needs current mana, max mana, incursion)
*/
export function computeEffectiveRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects
): number {
// Base regen from existing function
let regen = computeRegen(state, effects);
const incursionStrength = state.incursionStrength || 0;
// Apply incursion penalty
regen *= (1 - incursionStrength);
return regen;
}
export function computeClickMana(state: Pick<GameState, 'skills'>): number {
return (
1 +
(state.skills.manaTap || 0) * 1 +
(state.skills.manaSurge || 0) * 3
);
}
// ─── Elemental Damage Bonus ──────────────────────────────────────────────────
// Elemental damage bonus: +50% if spell element opposes floor element (super effective)
// -25% if spell element matches its own opposite (weak)
export function getElementalBonus(spellElem: string, floorElem: string): number {
if (spellElem === 'raw') return 1.0; // Raw mana has no elemental bonus
if (spellElem === floorElem) return 1.25; // Same element: +25% damage
// Check for super effective first: spell is the opposite of floor
// e.g., casting water (opposite of fire) at fire floor = super effective
if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective: +50% damage
// Check for weak: spell's opposite matches floor
// e.g., casting fire (whose opposite is water) at water floor = weak
if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak: -25% damage
return 1.0; // Neutral
}
// ─── Boon Bonuses ─────────────────────────────────────────────────────────────
// Helper to calculate total boon bonuses from signed pacts
export function getBoonBonuses(signedPacts: number[]): {
maxMana: number;
manaRegen: number;
castingSpeed: number;
elementalDamage: number;
rawDamage: number;
critChance: number;
critDamage: number;
spellEfficiency: number;
manaGain: number;
insightGain: number;
studySpeed: number;
prestigeInsight: number;
} {
const bonuses = {
maxMana: 0,
manaRegen: 0,
castingSpeed: 0,
elementalDamage: 0,
rawDamage: 0,
critChance: 0,
critDamage: 0,
spellEfficiency: 0,
manaGain: 0,
insightGain: 0,
studySpeed: 0,
prestigeInsight: 0,
};
for (const floor of signedPacts) {
const guardian = GUARDIANS[floor];
if (!guardian) continue;
for (const boon of guardian.boons) {
switch (boon.type) {
case 'maxMana':
bonuses.maxMana += boon.value;
break;
case 'manaRegen':
bonuses.manaRegen += boon.value;
break;
case 'castingSpeed':
bonuses.castingSpeed += boon.value;
break;
case 'elementalDamage':
bonuses.elementalDamage += boon.value;
break;
case 'rawDamage':
bonuses.rawDamage += boon.value;
break;
case 'critChance':
bonuses.critChance += boon.value;
break;
case 'critDamage':
bonuses.critDamage += boon.value;
break;
case 'spellEfficiency':
bonuses.spellEfficiency += boon.value;
break;
case 'manaGain':
bonuses.manaGain += boon.value;
break;
case 'insightGain':
bonuses.insightGain += boon.value;
break;
case 'studySpeed':
bonuses.studySpeed += boon.value;
break;
case 'prestigeInsight':
bonuses.prestigeInsight += boon.value;
break;
}
}
}
return bonuses;
}
// ─── Damage Calculation ───────────────────────────────────────────────────────
export function calcDamage(
state: Pick<GameState, 'skills' | 'signedPacts'>,
spellId: string,
floorElem?: string
): number {
const sp = SPELLS_DEF[spellId];
if (!sp) return 5;
const skills = state.skills;
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5;
const pct = 1 + (skills.arcaneFury || 0) * 0.1;
// Elemental mastery bonus
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
// Guardian bane bonus - check if current floor has a guardian with matching element
const isGuardianFloor = floorElem && Object.values(GUARDIANS).some(g => g.element === floorElem);
const guardianBonus = isGuardianFloor
? 1 + (skills.guardianBane || 0) * 0.2
: 1;
// Get boon bonuses from pacts
const boons = getBoonBonuses(state.signedPacts);
// Apply raw damage and elemental damage bonuses
const rawDamageMult = 1 + boons.rawDamage / 100;
const elemDamageMult = 1 + boons.elementalDamage / 100;
// Apply crit chance and damage from boons
const critChance = (skills.precision || 0) * 0.05 + boons.critChance / 100;
const critDamageMult = 1.5 + boons.critDamage / 100;
let damage = baseDmg * pct * elemMasteryBonus * guardianBonus * rawDamageMult * elemDamageMult;
// Apply elemental bonus if floor element provided
if (floorElem) {
damage *= getElementalBonus(sp.elem, floorElem);
}
// Apply crit
if (Math.random() < critChance) {
damage *= critDamageMult;
}
return damage;
}
// ─── Insight Calculation ──────────────────────────────────────────────────────
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
const pu = state.prestigeUpgrades;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1;
// Get boon bonuses for insight gain
const boons = getBoonBonuses(state.signedPacts);
const boonInsightMult = 1 + boons.insightGain / 100;
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus * boonInsightMult;
// Add prestigeInsight bonus per loop
const prestigeInsightBonus = boons.prestigeInsight;
return Math.floor(
(state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150 + prestigeInsightBonus) * mult
);
}
// ─── Meditation Bonus ─────────────────────────────────────────────────────────
// Meditation bonus now affects regen rate directly
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number {
const hasMeditation = skills.meditation === 1;
const hasDeepTrance = skills.deepTrance === 1;
const hasVoidMeditation = skills.voidMeditation === 1;
const hours = meditateTicks * HOURS_PER_TICK;
// Base meditation: ramps up over 4 hours to 1.5x
let bonus = 1 + Math.min(hours / 4, 0.5);
// With Meditation Focus: up to 2.5x after 4 hours
if (hasMeditation && hours >= 4) {
bonus = 2.5;
}
// With Deep Trance: up to 3.0x after 6 hours
if (hasDeepTrance && hours >= 6) {
bonus = 3.0;
}
// With Void Meditation: up to 5.0x after 8 hours
if (hasVoidMeditation && hours >= 8) {
bonus = 5.0;
}
// Apply meditation efficiency from upgrades (Deep Wellspring, etc.)
bonus *= meditationEfficiency;
return bonus;
}
// ─── Incursion Strength ───────────────────────────────────────────────────────
export function getIncursionStrength(day: number, hour: number): number {
if (day < INCURSION_START_DAY) return 0;
const totalHours = (day - INCURSION_START_DAY) * 24 + hour;
const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24;
return Math.min(0.95, (totalHours / maxHours) * 0.95);
}
// ─── Spell Cost Helpers ───────────────────────────────────────────────────────
// Check if player can afford spell cost
export function canAffordSpellCost(
cost: SpellCost,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>
): boolean {
if (cost.type === 'raw') {
return rawMana >= cost.amount;
} else {
const elem = elements[cost.element || ''];
return elem && elem.unlocked && elem.current >= cost.amount;
}
}
// Deduct spell cost from appropriate mana pool
export function deductSpellCost(
cost: SpellCost,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>
): { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> } {
const newElements = { ...elements };
if (cost.type === 'raw') {
return { rawMana: rawMana - cost.amount, elements: newElements };
} else if (cost.element && newElements[cost.element]) {
newElements[cost.element] = {
...newElements[cost.element],
current: newElements[cost.element].current - cost.amount
};
return { rawMana, elements: newElements };
}
return { rawMana, elements: newElements };
}