Initial commit
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s
This commit is contained in:
272
src/lib/game/__tests__/computed-stats.test.ts
Executable file
272
src/lib/game/__tests__/computed-stats.test.ts
Executable 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
567
src/lib/game/attunements.ts
Executable 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
492
src/lib/game/computed-stats.ts
Executable 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
876
src/lib/game/constants.ts
Executable 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
827
src/lib/game/crafting-slice.ts
Executable 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
272
src/lib/game/data/achievements.ts
Executable 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
185
src/lib/game/data/attunements.ts
Executable 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);
|
||||
}
|
||||
257
src/lib/game/data/crafting-recipes.ts
Executable file
257
src/lib/game/data/crafting-recipes.ts
Executable 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);
|
||||
}
|
||||
603
src/lib/game/data/enchantment-effects.ts
Executable file
603
src/lib/game/data/enchantment-effects.ts
Executable 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
411
src/lib/game/data/equipment.ts
Executable 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
242
src/lib/game/data/loot-drops.ts
Executable 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
215
src/lib/game/effects.ts
Executable 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
46
src/lib/game/formatting.ts
Executable 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')}`;
|
||||
}
|
||||
63
src/lib/game/navigation-slice.ts
Executable file
63
src/lib/game/navigation-slice.ts
Executable 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
797
src/lib/game/skill-evolution.ts
Executable 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
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
180
src/lib/game/study-slice.ts
Executable 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
448
src/lib/game/types.ts
Executable 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
362
src/lib/game/upgrade-effects.ts
Executable file
@@ -0,0 +1,362 @@
|
||||
// ─── Upgrade Effect System ─────────────────────────────────────────────────────
|
||||
// This module handles applying skill upgrade effects to game stats
|
||||
|
||||
import type { SkillUpgradeChoice, SkillUpgradeEffect } from './types';
|
||||
import { getUpgradesForSkillAtMilestone, SKILL_EVOLUTION_PATHS } from './skill-evolution';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ActiveUpgradeEffect {
|
||||
upgradeId: string;
|
||||
skillId: string;
|
||||
milestone: 5 | 10;
|
||||
effect: SkillUpgradeEffect;
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export interface ComputedEffects {
|
||||
// Mana effects
|
||||
maxManaMultiplier: number;
|
||||
maxManaBonus: number;
|
||||
regenMultiplier: number;
|
||||
regenBonus: number;
|
||||
clickManaMultiplier: number;
|
||||
clickManaBonus: number;
|
||||
meditationEfficiency: number;
|
||||
spellCostMultiplier: number;
|
||||
conversionEfficiency: number;
|
||||
|
||||
// Combat effects
|
||||
baseDamageMultiplier: number;
|
||||
baseDamageBonus: number;
|
||||
attackSpeedMultiplier: number;
|
||||
critChanceBonus: number;
|
||||
critDamageMultiplier: number;
|
||||
elementalDamageMultiplier: number;
|
||||
|
||||
// Study effects
|
||||
studySpeedMultiplier: number;
|
||||
studyCostMultiplier: number;
|
||||
progressRetention: number;
|
||||
instantStudyChance: number;
|
||||
freeStudyChance: number;
|
||||
|
||||
// Element effects
|
||||
elementCapMultiplier: number;
|
||||
elementCapBonus: number;
|
||||
conversionCostMultiplier: number;
|
||||
doubleCraftChance: number;
|
||||
|
||||
// Special values
|
||||
permanentRegenBonus: number;
|
||||
|
||||
// Special effect flags (for game logic to check)
|
||||
specials: Set<string>;
|
||||
|
||||
// All active upgrades for display
|
||||
activeUpgrades: ActiveUpgradeEffect[];
|
||||
}
|
||||
|
||||
// ─── Special Effect IDs ────────────────────────────────────────────────────────
|
||||
// These are the IDs used in the 'specialId' field of special effects
|
||||
|
||||
export const SPECIAL_EFFECTS = {
|
||||
// Mana Flow special effects
|
||||
MANA_CASCADE: 'manaCascade', // +0.1 regen per 100 max mana
|
||||
STEADY_STREAM: 'steadyStream', // Regen immune to incursion
|
||||
MANA_TORRENT: 'manaTorrent', // +50% regen when above 75% mana
|
||||
FLOW_SURGE: 'flowSurge', // Clicks restore 2x regen for 1 hour
|
||||
MANA_OVERFLOW: 'manaOverflow', // Raw mana can exceed max by 20%
|
||||
|
||||
// Mana Well special effects
|
||||
DESPERATE_WELLS: 'desperateWells', // +50% regen when below 25% mana
|
||||
MANA_ECHO: 'manaEcho', // 10% chance double mana from clicks
|
||||
EMERGENCY_RESERVE: 'emergencyReserve', // Keep 10% mana on new loop
|
||||
|
||||
// Combat special effects
|
||||
FIRST_STRIKE: 'firstStrike', // +15% damage on first attack each floor
|
||||
OVERPOWER: 'overpower', // +50% damage when mana above 80%
|
||||
BERSERKER: 'berserker', // +50% damage when below 50% mana
|
||||
COMBO_MASTER: 'comboMaster', // Every 5th attack deals 3x damage
|
||||
ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana
|
||||
|
||||
// Study special effects
|
||||
QUICK_GRASP: 'quickGrasp', // 5% chance double study progress per hour
|
||||
DEEP_CONCENTRATION: 'deepConcentration', // +20% study speed when mana > 90%
|
||||
QUICK_MASTERY: 'quickMastery', // -20% study time for final 3 levels
|
||||
PARALLEL_STUDY: 'parallelStudy', // Study 2 things at 50% speed
|
||||
STUDY_MOMENTUM: 'studyMomentum', // +5% study speed per consecutive hour
|
||||
KNOWLEDGE_ECHO: 'knowledgeEcho', // 10% chance instant study
|
||||
KNOWLEDGE_TRANSFER: 'knowledgeTransfer', // New skills start at 10% progress
|
||||
MENTAL_CLARITY: 'mentalClarity', // +10% study speed when mana > 75%
|
||||
STUDY_REFUND: 'studyRefund', // 25% mana back on study complete
|
||||
DEEP_UNDERSTANDING: 'deepUnderstanding', // +10% bonus from all skill levels
|
||||
STUDY_RUSH: 'studyRush', // First hour of study is 2x speed
|
||||
CHAIN_STUDY: 'chainStudy', // -5% cost per maxed skill
|
||||
|
||||
// Element special effects
|
||||
ELEMENTAL_AFFINITY: 'elementalAffinity', // New elements start with 10 capacity
|
||||
EXOTIC_MASTERY: 'exoticMastery', // +20% exotic element damage
|
||||
ELEMENTAL_RESONANCE: 'elementalResonance', // Using element spells restores 1 of that element
|
||||
MANA_CONDUIT: 'manaConduit', // Meditation regenerates elemental mana
|
||||
} as const;
|
||||
|
||||
// ─── Upgrade Definition Cache ─────────────────────────────────────────────────
|
||||
|
||||
// Cache all upgrades by ID for quick lookup
|
||||
const upgradeDefinitionsById: Map<string, SkillUpgradeChoice> = new Map();
|
||||
|
||||
// Build the cache on first access
|
||||
function buildUpgradeCache(): void {
|
||||
if (upgradeDefinitionsById.size > 0) return;
|
||||
|
||||
for (const [baseSkillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
||||
for (const tierDef of path.tiers) {
|
||||
for (const upgrade of tierDef.upgrades) {
|
||||
upgradeDefinitionsById.set(upgrade.id, upgrade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helper Functions ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get all selected upgrades with their full effect definitions
|
||||
*/
|
||||
export function getActiveUpgrades(
|
||||
skillUpgrades: Record<string, string[]>,
|
||||
skillTiers: Record<string, number>
|
||||
): ActiveUpgradeEffect[] {
|
||||
buildUpgradeCache();
|
||||
const result: ActiveUpgradeEffect[] = [];
|
||||
|
||||
for (const [skillId, upgradeIds] of Object.entries(skillUpgrades)) {
|
||||
for (const upgradeId of upgradeIds) {
|
||||
const upgradeDef = upgradeDefinitionsById.get(upgradeId);
|
||||
if (upgradeDef) {
|
||||
result.push({
|
||||
upgradeId,
|
||||
skillId,
|
||||
milestone: upgradeDef.milestone,
|
||||
effect: upgradeDef.effect,
|
||||
name: upgradeDef.name,
|
||||
desc: upgradeDef.desc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute all active effects from selected upgrades
|
||||
*/
|
||||
export function computeEffects(
|
||||
skillUpgrades: Record<string, string[]>,
|
||||
skillTiers: Record<string, number>
|
||||
): ComputedEffects {
|
||||
const activeUpgrades = getActiveUpgrades(skillUpgrades, skillTiers);
|
||||
|
||||
// Start with base values
|
||||
const effects: ComputedEffects = {
|
||||
maxManaMultiplier: 1,
|
||||
maxManaBonus: 0,
|
||||
regenMultiplier: 1,
|
||||
regenBonus: 0,
|
||||
clickManaMultiplier: 1,
|
||||
clickManaBonus: 0,
|
||||
meditationEfficiency: 1,
|
||||
spellCostMultiplier: 1,
|
||||
conversionEfficiency: 1,
|
||||
baseDamageMultiplier: 1,
|
||||
baseDamageBonus: 0,
|
||||
attackSpeedMultiplier: 1,
|
||||
critChanceBonus: 0,
|
||||
critDamageMultiplier: 1.5,
|
||||
elementalDamageMultiplier: 1,
|
||||
studySpeedMultiplier: 1,
|
||||
studyCostMultiplier: 1,
|
||||
progressRetention: 0,
|
||||
instantStudyChance: 0,
|
||||
freeStudyChance: 0,
|
||||
elementCapMultiplier: 1,
|
||||
elementCapBonus: 0,
|
||||
conversionCostMultiplier: 1,
|
||||
doubleCraftChance: 0,
|
||||
permanentRegenBonus: 0,
|
||||
specials: new Set<string>(),
|
||||
activeUpgrades,
|
||||
};
|
||||
|
||||
// Apply each upgrade effect
|
||||
for (const upgrade of activeUpgrades) {
|
||||
const { effect } = upgrade;
|
||||
|
||||
if (effect.type === 'multiplier' && effect.stat && effect.value !== undefined) {
|
||||
// Multiplier effects (multiply the stat)
|
||||
switch (effect.stat) {
|
||||
case 'maxMana':
|
||||
effects.maxManaMultiplier *= effect.value;
|
||||
break;
|
||||
case 'regen':
|
||||
effects.regenMultiplier *= effect.value;
|
||||
break;
|
||||
case 'clickMana':
|
||||
effects.clickManaMultiplier *= effect.value;
|
||||
break;
|
||||
case 'meditationEfficiency':
|
||||
effects.meditationEfficiency *= effect.value;
|
||||
break;
|
||||
case 'spellCost':
|
||||
effects.spellCostMultiplier *= effect.value;
|
||||
break;
|
||||
case 'conversionEfficiency':
|
||||
effects.conversionEfficiency *= effect.value;
|
||||
break;
|
||||
case 'baseDamage':
|
||||
effects.baseDamageMultiplier *= effect.value;
|
||||
break;
|
||||
case 'attackSpeed':
|
||||
effects.attackSpeedMultiplier *= effect.value;
|
||||
break;
|
||||
case 'elementalDamage':
|
||||
effects.elementalDamageMultiplier *= effect.value;
|
||||
break;
|
||||
case 'studySpeed':
|
||||
effects.studySpeedMultiplier *= effect.value;
|
||||
break;
|
||||
case 'elementCap':
|
||||
effects.elementCapMultiplier *= effect.value;
|
||||
break;
|
||||
case 'conversionCost':
|
||||
effects.conversionCostMultiplier *= effect.value;
|
||||
break;
|
||||
case 'costReduction':
|
||||
// For cost reduction, higher is better (less cost)
|
||||
// This is a multiplier on the reduction effectiveness
|
||||
effects.studyCostMultiplier /= effect.value;
|
||||
break;
|
||||
}
|
||||
} else if (effect.type === 'bonus' && effect.stat && effect.value !== undefined) {
|
||||
// Bonus effects (add to the stat)
|
||||
switch (effect.stat) {
|
||||
case 'maxMana':
|
||||
effects.maxManaBonus += effect.value;
|
||||
break;
|
||||
case 'regen':
|
||||
effects.regenBonus += effect.value;
|
||||
break;
|
||||
case 'clickMana':
|
||||
effects.clickManaBonus += effect.value;
|
||||
break;
|
||||
case 'baseDamage':
|
||||
effects.baseDamageBonus += effect.value;
|
||||
break;
|
||||
case 'elementCap':
|
||||
effects.elementCapBonus += effect.value;
|
||||
break;
|
||||
case 'permanentRegen':
|
||||
effects.permanentRegenBonus += effect.value;
|
||||
break;
|
||||
}
|
||||
} else if (effect.type === 'special' && effect.specialId) {
|
||||
// Special effects - add to the set for game logic to check
|
||||
effects.specials.add(effect.specialId);
|
||||
}
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a special effect is active
|
||||
*/
|
||||
export function hasSpecial(effects: ComputedEffects, specialId: string): boolean {
|
||||
return effects?.specials?.has(specialId) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute regen with special effects that depend on dynamic values
|
||||
*/
|
||||
export function computeDynamicRegen(
|
||||
effects: ComputedEffects,
|
||||
baseRegen: number,
|
||||
maxMana: number,
|
||||
currentMana: number,
|
||||
incursionStrength: number
|
||||
): number {
|
||||
let regen = baseRegen;
|
||||
|
||||
// Mana Cascade: +0.1 regen per 100 max mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CASCADE)) {
|
||||
regen += Math.floor(maxMana / 100) * 0.1;
|
||||
}
|
||||
|
||||
// Mana Torrent: +50% regen when above 75% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TORRENT) && currentMana > maxMana * 0.75) {
|
||||
regen *= 1.5;
|
||||
}
|
||||
|
||||
// Desperate Wells: +50% regen when below 25% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.DESPERATE_WELLS) && currentMana < maxMana * 0.25) {
|
||||
regen *= 1.5;
|
||||
}
|
||||
|
||||
// Steady Stream: Regen immune to incursion
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)) {
|
||||
return regen * effects.regenMultiplier;
|
||||
}
|
||||
|
||||
// Apply incursion penalty
|
||||
regen *= (1 - incursionStrength);
|
||||
|
||||
return regen * effects.regenMultiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute click mana with special effects
|
||||
*/
|
||||
export function computeDynamicClickMana(
|
||||
effects: ComputedEffects,
|
||||
baseClickMana: number
|
||||
): number {
|
||||
let clickMana = baseClickMana;
|
||||
|
||||
// Mana Echo: 10% chance to gain double mana from clicks
|
||||
// Note: The chance is handled in the click handler, this just returns the base
|
||||
// The click handler should check hasSpecial and apply the 10% chance
|
||||
|
||||
return Math.floor((clickMana + effects.clickManaBonus) * effects.clickManaMultiplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute damage with special effects
|
||||
*/
|
||||
export function computeDynamicDamage(
|
||||
effects: ComputedEffects,
|
||||
baseDamage: number,
|
||||
floorHPPct: number,
|
||||
currentMana: number,
|
||||
maxMana: number
|
||||
): number {
|
||||
let damage = baseDamage * effects.baseDamageMultiplier;
|
||||
|
||||
// Overpower: +50% damage when mana above 80%
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.OVERPOWER) && currentMana >= maxMana * 0.8) {
|
||||
damage *= 1.5;
|
||||
}
|
||||
|
||||
// Berserker: +50% damage when below 50% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && currentMana < maxMana * 0.5) {
|
||||
damage *= 1.5;
|
||||
}
|
||||
|
||||
// Combo Master: Every 5th attack deals 3x damage
|
||||
// Note: The hit counter is tracked in game state, this just returns the multiplier
|
||||
// The combat handler should check hasSpecial and the hit count
|
||||
|
||||
return damage + effects.baseDamageBonus;
|
||||
}
|
||||
372
src/lib/game/utils.ts
Executable file
372
src/lib/game/utils.ts
Executable 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user