diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index c030ca5..8748590 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # Circular Dependencies -Generated: 2026-06-11T07:06:06.140Z +Generated: 2026-06-11T07:08:59.875Z Found: 3 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 1ea3a41..2166e6c 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-11T07:06:04.118Z", + "generated": "2026-06-11T07:08:57.908Z", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." }, diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 61a7f51..6b5ab0f 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -356,6 +356,7 @@ Mana-Loop/ │ │ │ │ ├── fabricator-wizard-recipes.ts │ │ │ │ ├── guardian-data.ts │ │ │ │ ├── guardian-encounters.ts +│ │ │ │ ├── guardian-procedural.ts │ │ │ │ └── loot-drops.ts │ │ │ ├── effects/ │ │ │ │ ├── discipline-effects.ts diff --git a/src/lib/game/data/guardian-encounters.ts b/src/lib/game/data/guardian-encounters.ts index f633db8..19754da 100644 --- a/src/lib/game/data/guardian-encounters.ts +++ b/src/lib/game/data/guardian-encounters.ts @@ -5,17 +5,13 @@ // Tier 1: Base Elements (static, floors 10–80, guardian-data.ts) // Tier 2: Composite Elements (static, floors 90–160, guardian-data.ts) // Tier 3: Exotic Elements (static, floors 170–240, guardian-data.ts) -// Tier 4: Dual Element Pairs (dynamic, floors 250–280, this file) -// Tier 5: Dual Comp+Components (dynamic, floors 290–330, this file) -// Tier 6: Exotic+Components (dynamic, floors 340–380, this file) -// Tier 7: Exotic+Comp+Components (dynamic, floors 390–430, this file) -// Tier 8: Full Fusion (dynamic, floors 440+, this file) +// Tier 4+: Procedural (dynamic, floors 250+, guardian-procedural.ts) // // All lookups go through getGuardianForFloor() which merges static + procedural. -import type { GuardianDef, GuardianBoon } from '../types'; +import type { GuardianDef } from '../types'; import { STATIC_GUARDIANS } from './guardian-data'; -import { resolveMultiUnlockChain } from '../utils/guardian-utils'; +import { getProceduralGuardian } from './guardian-procedural'; // ─── Name Generation ──────────────────────────────────────────────────────────── @@ -71,247 +67,7 @@ export function getGuardianHP(floor: number): number { return Math.floor(base * Math.pow(floor / 10, exponent)); } -// ─── Tier Helpers ─────────────────────────────────────────────────────────────── - -const COMPOSITE_ELEMENTS = ['metal', 'sand', 'lightning', 'frost', 'blackflame', 'radiantflames', 'miasma', 'shadowglass']; -const EXOTIC_ELEMENTS = ['crystal', 'stellar', 'void', 'soul', 'time', 'plasma']; - -/** Generate boon elements (max 3) */ -function makeBoons(elements: string[], floor: number): GuardianBoon[] { - return elements.slice(0, 3).map((el) => ({ - type: 'elementalDamage' as const, - value: 10 + Math.floor(floor / 20) * 5, - desc: `+${10 + Math.floor(floor / 20) * 5}% ${el} damage`, - })); -} - -function getTier(floor: number): number { - if (floor >= 440) return 8; - if (floor >= 390) return 7; - if (floor >= 340) return 6; - if (floor >= 290) return 5; - if (floor >= 250) return 4; - return 0; // Static tiers -} - -// ═══════════════════════════════════════════════════════════════════════════════ -// TIER 4: Dual Element Pairs (Floors 250–280) -// ═══════════════════════════════════════════════════════════════════════════════ - -const DUAL_PAIRS: [string, string][] = [ - ['fire', 'water'], - ['fire', 'air'], - ['water', 'earth'], - ['light', 'dark'], - ['death', 'light'], - ['fire', 'death'], - ['water', 'dark'], - ['air', 'light'], - ['earth', 'death'], -]; - -function getTier4Guardian(floor: number): GuardianDef { - const idx = Math.floor((floor - 250) / 10) % DUAL_PAIRS.length; - const [el1, el2] = DUAL_PAIRS[idx]; - const elements = [el1, el2]; - const hpVal = getGuardianHP(floor); - const armor = Math.min(0.5, 0.30 + (floor - 250) * 0.003); - - return { - name: '', - element: elements, - hp: hpVal, - pact: 7.5 + (floor - 250) * 0.05, - color: blendColors(el1, el2), - armor, - boons: makeBoons(elements, floor), - pactCost: Math.floor(hpVal * 0.3 + hpVal * armor * 0.5), - pactTime: 20 + Math.floor((floor - 250) / 10), - uniquePerk: `Dual-aspect: ${el1} and ${el2} spells gain +20% effectiveness`, - power: Math.floor(hpVal * 0.5), - effects: [ - { type: `${el1}_boost`, value: 0.15 }, - { type: `${el2}_boost`, value: 0.15 }, - ], - signingCost: { - mana: Math.floor(hpVal * 0.3 + hpVal * armor * 0.5), - time: 20 + Math.floor((floor - 250) / 10), - }, - unlocksMana: resolveMultiUnlockChain(elements), - damageMultiplier: 3.5 + (floor - 250) * 0.02, - insightMultiplier: 3.0 + (floor - 250) * 0.01, - }; -} - -// ═══════════════════════════════════════════════════════════════════════════════ -// TIER 5: Dual Composite + Components (Floors 290–330) -// Pairs of composites with all their base components. -// ═══════════════════════════════════════════════════════════════════════════════ - -const DUAL_COMP_PAIRS: [string, string][] = [ - ['metal', 'sand'], - ['metal', 'lightning'], - ['sand', 'lightning'], - ['frost', 'blackflame'], - ['radiantflames', 'miasma'], - ['shadowglass', 'frost'], -]; - -function getTier5Guardian(floor: number): GuardianDef { - const idx = Math.floor((floor - 290) / 10) % DUAL_COMP_PAIRS.length; - const [comp1, comp2] = DUAL_COMP_PAIRS[idx]; - const elements = resolveMultiUnlockChain([comp1, comp2]); - const hpVal = getGuardianHP(floor); - const armor = Math.min(0.55, 0.35 + (floor - 290) * 0.003); - - return { - name: '', - element: elements, - hp: hpVal, - pact: 9.0 + (floor - 290) * 0.05, - color: blendColors(comp1, comp2), - armor, - boons: makeBoons([comp1, comp2], floor), - pactCost: Math.floor(hpVal * 0.3 + hpVal * armor * 0.5), - pactTime: 24 + Math.floor((floor - 290) / 10), - uniquePerk: `Fusion twin-aspect: ${comp1} and ${comp2} spells gain +25% effectiveness`, - power: Math.floor(hpVal * 0.55), - effects: [ - { type: 'armor_pierce', value: 0.2 }, - { type: 'chain', value: 1 }, - ], - signingCost: { - mana: Math.floor(hpVal * 0.3 + hpVal * armor * 0.5), - time: 24 + Math.floor((floor - 290) / 10), - }, - unlocksMana: resolveMultiUnlockChain(elements), - damageMultiplier: 4.0 + (floor - 290) * 0.02, - insightMultiplier: 3.5 + (floor - 290) * 0.01, - }; -} - -// ═══════════════════════════════════════════════════════════════════════════════ -// TIER 6: Exotic + Components (Floors 340–380) -// Each exotic element paired with all its component base elements. -// ═══════════════════════════════════════════════════════════════════════════════ - -function getTier6Guardian(floor: number): GuardianDef { - const exoticIdx = Math.floor((floor - 340) / 10) % EXOTIC_ELEMENTS.length; - const exoticEl = EXOTIC_ELEMENTS[exoticIdx]; - const chain = resolveMultiUnlockChain([exoticEl]); - const hpVal = getGuardianHP(floor); - const armor = Math.min(0.6, 0.40 + (floor - 340) * 0.003); - - return { - name: '', - element: chain, - hp: hpVal, - pact: 10.5 + (floor - 340) * 0.05, - color: '#B8A9C9', - armor, - boons: [makeBoons([exoticEl], floor)[0]], - pactCost: Math.floor(hpVal * 0.35 + hpVal * armor * 0.5), - pactTime: 28 + Math.floor((floor - 340) / 10), - uniquePerk: `Exotic resonance: ${exoticEl} spells gain +30% effectiveness`, - power: Math.floor(hpVal * 0.6), - effects: [ - { type: 'resist_ignore', value: 0.2 }, - { type: 'reflect', value: 0.1 }, - ], - signingCost: { - mana: Math.floor(hpVal * 0.35 + hpVal * armor * 0.5), - time: 28 + Math.floor((floor - 340) / 10), - }, - unlocksMana: chain, - damageMultiplier: 4.5 + (floor - 340) * 0.02, - insightMultiplier: 4.0 + (floor - 340) * 0.01, - }; -} - -// ═══════════════════════════════════════════════════════════════════════════════ -// TIER 7: Exotic + Composite + Components (Floors 390–430) -// One exotic + one composite + all base elements. -// ═══════════════════════════════════════════════════════════════════════════════ - -function getTier7Guardian(floor: number): GuardianDef { - const exoticIdx = Math.floor((floor - 390) / 10) % EXOTIC_ELEMENTS.length; - const compIdx = Math.floor((floor - 390) / 10) % COMPOSITE_ELEMENTS.length; - const exoticEl = EXOTIC_ELEMENTS[exoticIdx]; - const compEl = COMPOSITE_ELEMENTS[compIdx]; - const elements = resolveMultiUnlockChain([exoticEl, compEl]); - const hpVal = getGuardianHP(floor); - const armor = Math.min(0.65, 0.45 + (floor - 390) * 0.003); - - return { - name: '', - element: elements, - hp: hpVal, - pact: 12.0 + (floor - 390) * 0.05, - color: '#9B72AA', - armor, - boons: makeBoons([exoticEl, compEl], floor), - pactCost: Math.floor(hpVal * 0.4 + hpVal * armor * 0.5), - pactTime: 32 + Math.floor((floor - 390) / 10), - uniquePerk: `Primordial fusion: ${exoticEl} and ${compEl} spells gain +25% effectiveness`, - power: Math.floor(hpVal * 0.65), - effects: [ - { type: 'resist_ignore', value: 0.25 }, - { type: 'armor_pierce', value: 0.2 }, - { type: 'chain', value: 1 }, - ], - signingCost: { - mana: Math.floor(hpVal * 0.4 + hpVal * armor * 0.5), - time: 32 + Math.floor((floor - 390) / 10), - }, - unlocksMana: elements, - damageMultiplier: 5.0 + (floor - 390) * 0.02, - insightMultiplier: 4.5 + (floor - 390) * 0.01, - }; -} - -// ═══════════════════════════════════════════════════════════════════════════════ -// TIER 8: Full Fusion — 1 Exotic + 2 Composite + All Components (Floors 440+) -// ═══════════════════════════════════════════════════════════════════════════════ - -function getTier8Guardian(floor: number): GuardianDef { - const exoticIdx = (Math.floor((floor - 440) / 10)) % EXOTIC_ELEMENTS.length; - const exoticEl = EXOTIC_ELEMENTS[exoticIdx]; - // Pick 2 different composites - const comp1 = COMPOSITE_ELEMENTS[Math.floor((floor - 440) / 10) % COMPOSITE_ELEMENTS.length]; - const comp2 = COMPOSITE_ELEMENTS[(Math.floor((floor - 440) / 10) + 1) % COMPOSITE_ELEMENTS.length]; - const elements = resolveMultiUnlockChain([exoticEl, comp1, comp2]); - const hpVal = getGuardianHP(floor); - const armor = Math.min(0.7, 0.50 + (floor - 440) * 0.002); - - return { - name: '', - element: elements, - hp: hpVal, - pact: 14.0 + (floor - 440) * 0.05, - color: '#7B5E9A', - armor, - boons: makeBoons([exoticEl, comp1, comp2], floor), - pactCost: Math.floor(hpVal * 0.45 + hpVal * armor * 0.5), - pactTime: 36 + Math.floor((floor - 440) / 10), - uniquePerk: `Cosmic convergence: All exotic, composite, and base spells gain +15% effectiveness`, - power: Math.floor(hpVal * 0.7), - effects: [ - { type: 'resist_ignore', value: 0.3 }, - { type: 'armor_pierce', value: 0.25 }, - { type: 'chain', value: 2 }, - { type: 'reflect', value: 0.1 }, - ], - signingCost: { - mana: Math.floor(hpVal * 0.45 + hpVal * armor * 0.5), - time: 36 + Math.floor((floor - 440) / 10), - }, - unlocksMana: elements, - damageMultiplier: 5.5 + (floor - 440) * 0.02, - insightMultiplier: 5.0 + (floor - 440) * 0.01, - }; -} - -// ─── Blending Colors for Dual/Multi-Element Guardians ────────────────────────── +// ─── Color Blending ───────────────────────────────────────────────────────────── const ELEMENT_COLORS: Record = { fire: '#FF6B35', water: '#4ECDC4', air: '#00D4FF', earth: '#F4A261', @@ -323,7 +79,7 @@ const ELEMENT_COLORS: Record = { soul: '#E8D5F5', time: '#C5B99A', plasma: '#FF6B9D', }; -function blendColors(el1: string, el2: string): string { +export function blendColors(el1: string, el2: string): string { const c1 = ELEMENT_COLORS[el1] || '#888888'; const c2 = ELEMENT_COLORS[el2] || '#888888'; try { @@ -336,29 +92,6 @@ function blendColors(el1: string, el2: string): string { } } -// ─── Procedural Guardian Generator ───────────────────────────────────────────── - -function getProceduralGuardian(floor: number): GuardianDef | null { - if (floor < 250 || floor % 10 !== 0) return null; - - const tier = getTier(floor); - let g: GuardianDef; - - switch (tier) { - case 4: g = getTier4Guardian(floor); break; - case 5: g = getTier5Guardian(floor); break; - case 6: g = getTier6Guardian(floor); break; - case 7: g = getTier7Guardian(floor); break; - case 8: g = getTier8Guardian(floor); break; - default: return null; - } - - if (!g.name) { - g.name = generateGuardianName(g.element, floor); - } - return g; -} - // ─── Unified Guardian System ──────────────────────────────────────────────────── /** Get the guardian for any floor. Returns null if no guardian at that floor. */ @@ -373,15 +106,10 @@ export function getGuardianForFloor(floor: number): GuardianDef | null { /** All guardian floors — dynamically computed. */ export function getAllGuardianFloors(): number[] { - // Static floors from guardian-data.ts const staticFloors = Object.keys(STATIC_GUARDIANS).map(Number); - // Procedural floors: every 10th floor from 250 to 490 const proceduralFloors: number[] = []; - for (let f = 250; f <= 490; f += 10) { - proceduralFloors.push(f); - } - const all = new Set([...staticFloors, ...proceduralFloors]); - return Array.from(all).sort((a, b) => a - b); + for (let f = 250; f <= 490; f += 10) proceduralFloors.push(f); + return Array.from(new Set([...staticFloors, ...proceduralFloors])).sort((a, b) => a - b); } /** Check if a floor is a guardian floor (every 10th floor with a defined guardian). */ diff --git a/src/lib/game/data/guardian-procedural.ts b/src/lib/game/data/guardian-procedural.ts new file mode 100644 index 0000000..ce8fd35 --- /dev/null +++ b/src/lib/game/data/guardian-procedural.ts @@ -0,0 +1,164 @@ +// ─── Procedural Guardian Generators (Tiers 4–8) ───────────────────────────────── +// Extracted from guardian-encounters.ts to keep files under the 400-line limit. +// +// Tier 4: Dual Element Pairs (floors 250–280) +// Tier 5: Dual Comp+Components (floors 290–330) +// Tier 6: Exotic+Components (floors 340–380) +// Tier 7: Exotic+Comp+Components (floors 390–430) +// Tier 8: Full Fusion (floors 440+) + +import type { GuardianDef, GuardianBoon } from '../types'; +import { resolveMultiUnlockChain } from '../utils/guardian-utils'; +import { generateGuardianName, getGuardianHP, blendColors } from './guardian-encounters'; + +// ─── Shared Helpers ───────────────────────────────────────────────────────────── + +const COMPOSITE_ELEMENTS = ['metal', 'sand', 'lightning', 'frost', 'blackflame', 'radiantflames', 'miasma', 'shadowglass']; +const EXOTIC_ELEMENTS = ['crystal', 'stellar', 'void', 'soul', 'time', 'plasma']; + +function makeBoons(elements: string[], floor: number): GuardianBoon[] { + return elements.slice(0, 3).map((el) => ({ + type: 'elementalDamage' as const, + value: 10 + Math.floor(floor / 20) * 5, + desc: `+${10 + Math.floor(floor / 20) * 5}% ${el} damage`, + })); +} + +function defensiveStats(hpVal: number, baseMult: number, floorOffset: number, barrierMax: number, barrierBase: number) { + const shield = Math.floor(hpVal * baseMult + floorOffset); + const barrier = Math.min(barrierMax, barrierBase + floorOffset * 0.0002); + return { shield, shieldRegen: Math.floor(shield * 0.02), barrier, barrierRegen: Math.floor(barrier * 0.01), healthRegen: Math.floor(hpVal * baseMult * 0.125), healthRegenIsPercent: false }; +} + +// ─── Tier 4: Dual Element Pairs (Floors 250–280) ──────────────────────────────── + +const DUAL_PAIRS: [string, string][] = [ + ['fire', 'water'], ['fire', 'air'], ['water', 'earth'], ['light', 'dark'], + ['death', 'light'], ['fire', 'death'], ['water', 'dark'], ['air', 'light'], ['earth', 'death'], +]; + +export function getTier4Guardian(floor: number): GuardianDef { + const [el1, el2] = DUAL_PAIRS[Math.floor((floor - 250) / 10) % DUAL_PAIRS.length]; + const hpVal = getGuardianHP(floor); + const armor = Math.min(0.5, 0.30 + (floor - 250) * 0.003); + const def = defensiveStats(hpVal, 0.04, (floor - 250) * 2, 0.15, 0.05); + const pc = Math.floor(hpVal * 0.3 + hpVal * armor * 0.5 + def.shield * 2 + hpVal * def.barrier * 0.3); + return { + name: '', element: [el1, el2], hp: hpVal, pact: 7.5 + (floor - 250) * 0.05, + color: blendColors(el1, el2), armor, ...def, + boons: makeBoons([el1, el2], floor), pactCost: pc, pactTime: 20 + Math.floor((floor - 250) / 10), + uniquePerk: `Dual-aspect: ${el1} and ${el2} spells gain +20% effectiveness`, + power: Math.floor(hpVal * 0.5), + effects: [{ type: `${el1}_boost`, value: 0.15 }, { type: `${el2}_boost`, value: 0.15 }], + signingCost: { mana: pc, time: 20 + Math.floor((floor - 250) / 10) }, + unlocksMana: resolveMultiUnlockChain([el1, el2]), + damageMultiplier: 3.5 + (floor - 250) * 0.02, insightMultiplier: 3.0 + (floor - 250) * 0.01, + }; +} + +// ─── Tier 5: Dual Composite + Components (Floors 290–330) ─────────────────────── + +const DUAL_COMP_PAIRS: [string, string][] = [ + ['metal', 'sand'], ['metal', 'lightning'], ['sand', 'lightning'], + ['frost', 'blackflame'], ['radiantflames', 'miasma'], ['shadowglass', 'frost'], +]; + +export function getTier5Guardian(floor: number): GuardianDef { + const [comp1, comp2] = DUAL_COMP_PAIRS[Math.floor((floor - 290) / 10) % DUAL_COMP_PAIRS.length]; + const elements = resolveMultiUnlockChain([comp1, comp2]); + const hpVal = getGuardianHP(floor); + const armor = Math.min(0.55, 0.35 + (floor - 290) * 0.003); + const def = defensiveStats(hpVal, 0.05, (floor - 290) * 2.5, 0.18, 0.07); + const pc = Math.floor(hpVal * 0.3 + hpVal * armor * 0.5 + def.shield * 2 + hpVal * def.barrier * 0.3); + return { + name: '', element: elements, hp: hpVal, pact: 9.0 + (floor - 290) * 0.05, + color: blendColors(comp1, comp2), armor, ...def, + boons: makeBoons([comp1, comp2], floor), pactCost: pc, pactTime: 24 + Math.floor((floor - 290) / 10), + uniquePerk: `Fusion twin-aspect: ${comp1} and ${comp2} spells gain +25% effectiveness`, + power: Math.floor(hpVal * 0.55), + effects: [{ type: 'armor_pierce', value: 0.2 }, { type: 'chain', value: 1 }], + signingCost: { mana: pc, time: 24 + Math.floor((floor - 290) / 10) }, + unlocksMana: resolveMultiUnlockChain(elements), + damageMultiplier: 4.0 + (floor - 290) * 0.02, insightMultiplier: 3.5 + (floor - 290) * 0.01, + }; +} + +// ─── Tier 6: Exotic + Components (Floors 340–380) ─────────────────────────────── + +export function getTier6Guardian(floor: number): GuardianDef { + const exoticEl = EXOTIC_ELEMENTS[Math.floor((floor - 340) / 10) % EXOTIC_ELEMENTS.length]; + const chain = resolveMultiUnlockChain([exoticEl]); + const hpVal = getGuardianHP(floor); + const armor = Math.min(0.6, 0.40 + (floor - 340) * 0.003); + const def = defensiveStats(hpVal, 0.06, (floor - 340) * 3, 0.20, 0.08); + const pc = Math.floor(hpVal * 0.35 + hpVal * armor * 0.5 + def.shield * 2 + hpVal * def.barrier * 0.3); + return { + name: '', element: chain, hp: hpVal, pact: 10.5 + (floor - 340) * 0.05, + color: '#B8A9C9', armor, ...def, + boons: [makeBoons([exoticEl], floor)[0]], pactCost: pc, pactTime: 28 + Math.floor((floor - 340) / 10), + uniquePerk: `Exotic resonance: ${exoticEl} spells gain +30% effectiveness`, + power: Math.floor(hpVal * 0.6), + effects: [{ type: 'resist_ignore', value: 0.2 }, { type: 'reflect', value: 0.1 }], + signingCost: { mana: pc, time: 28 + Math.floor((floor - 340) / 10) }, + unlocksMana: chain, + damageMultiplier: 4.5 + (floor - 340) * 0.02, insightMultiplier: 4.0 + (floor - 340) * 0.01, + }; +} + +// ─── Tier 7: Exotic + Composite + Components (Floors 390–430) ─────────────────── + +export function getTier7Guardian(floor: number): GuardianDef { + const exoticEl = EXOTIC_ELEMENTS[Math.floor((floor - 390) / 10) % EXOTIC_ELEMENTS.length]; + const compEl = COMPOSITE_ELEMENTS[Math.floor((floor - 390) / 10) % COMPOSITE_ELEMENTS.length]; + const elements = resolveMultiUnlockChain([exoticEl, compEl]); + const hpVal = getGuardianHP(floor); + const armor = Math.min(0.65, 0.45 + (floor - 390) * 0.003); + const def = defensiveStats(hpVal, 0.07, (floor - 390) * 3.5, 0.22, 0.10); + const pc = Math.floor(hpVal * 0.4 + hpVal * armor * 0.5 + def.shield * 2 + hpVal * def.barrier * 0.3); + return { + name: '', element: elements, hp: hpVal, pact: 12.0 + (floor - 390) * 0.05, + color: '#9B72AA', armor, ...def, + boons: makeBoons([exoticEl, compEl], floor), pactCost: pc, pactTime: 32 + Math.floor((floor - 390) / 10), + uniquePerk: `Primordial fusion: ${exoticEl} and ${compEl} spells gain +25% effectiveness`, + power: Math.floor(hpVal * 0.65), + effects: [{ type: 'resist_ignore', value: 0.25 }, { type: 'armor_pierce', value: 0.2 }, { type: 'chain', value: 1 }], + signingCost: { mana: pc, time: 32 + Math.floor((floor - 390) / 10) }, + unlocksMana: elements, + damageMultiplier: 5.0 + (floor - 390) * 0.02, insightMultiplier: 4.5 + (floor - 390) * 0.01, + }; +} + +// ─── Tier 8: Full Fusion — 1 Exotic + 2 Composite + All Components (Floors 440+) + +export function getTier8Guardian(floor: number): GuardianDef { + const exoticEl = EXOTIC_ELEMENTS[Math.floor((floor - 440) / 10) % EXOTIC_ELEMENTS.length]; + const comp1 = COMPOSITE_ELEMENTS[Math.floor((floor - 440) / 10) % COMPOSITE_ELEMENTS.length]; + const comp2 = COMPOSITE_ELEMENTS[(Math.floor((floor - 440) / 10) + 1) % COMPOSITE_ELEMENTS.length]; + const elements = resolveMultiUnlockChain([exoticEl, comp1, comp2]); + const hpVal = getGuardianHP(floor); + const armor = Math.min(0.7, 0.50 + (floor - 440) * 0.002); + const def = defensiveStats(hpVal, 0.08, (floor - 440) * 4, 0.25, 0.12); + const pc = Math.floor(hpVal * 0.45 + hpVal * armor * 0.5 + def.shield * 2 + hpVal * def.barrier * 0.3); + return { + name: '', element: elements, hp: hpVal, pact: 14.0 + (floor - 440) * 0.05, + color: '#7B5E9A', armor, ...def, + boons: makeBoons([exoticEl, comp1, comp2], floor), pactCost: pc, pactTime: 36 + Math.floor((floor - 440) / 10), + uniquePerk: `Cosmic convergence: All exotic, composite, and base spells gain +15% effectiveness`, + power: Math.floor(hpVal * 0.7), + effects: [{ type: 'resist_ignore', value: 0.3 }, { type: 'armor_pierce', value: 0.25 }, { type: 'chain', value: 2 }, { type: 'reflect', value: 0.1 }], + signingCost: { mana: pc, time: 36 + Math.floor((floor - 440) / 10) }, + unlocksMana: elements, + damageMultiplier: 5.5 + (floor - 440) * 0.02, insightMultiplier: 5.0 + (floor - 440) * 0.01, + }; +} + +// ─── Procedural Guardian Lookup ───────────────────────────────────────────────── + +export function getProceduralGuardian(floor: number): GuardianDef | null { + if (floor < 250 || floor % 10 !== 0) return null; + const tier = floor >= 440 ? 8 : floor >= 390 ? 7 : floor >= 340 ? 6 : floor >= 290 ? 5 : floor >= 250 ? 4 : 0; + if (tier === 0) return null; + const g = tier === 4 ? getTier4Guardian(floor) : tier === 5 ? getTier5Guardian(floor) : tier === 6 ? getTier6Guardian(floor) : tier === 7 ? getTier7Guardian(floor) : getTier8Guardian(floor); + if (!g.name) g.name = generateGuardianName(g.element, floor); + return g; +} diff --git a/src/lib/game/hooks/useGameDerived.ts b/src/lib/game/hooks/useGameDerived.ts index 5d53bc3..8e49bdf 100644 --- a/src/lib/game/hooks/useGameDerived.ts +++ b/src/lib/game/hooks/useGameDerived.ts @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { useGameStore } from '../stores/gameStore'; +import { useCraftingStore } from '../stores/craftingStore'; import { useManaStore } from '../stores/manaStore'; import { useCombatStore } from '../stores/combatStore'; import { usePrestigeStore } from '../stores/prestigeStore'; @@ -122,8 +123,8 @@ export function useCombatStats() { const currentFloor = useCombatStore((s) => s.currentFloor); const activeSpell = useCombatStore((s) => s.activeSpell); const { upgradeEffects } = useManaStats(); - const equipmentInstances = useGameStore((s) => s.crafting.equipmentInstances); - const equippedInstances = useGameStore((s) => s.crafting.equippedInstances); + const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); + const equippedInstances = useCraftingStore((s) => s.equippedInstances); const floorElem = useMemo( () => getFloorElement(currentFloor), diff --git a/src/lib/game/stores/craftingStore.ts b/src/lib/game/stores/craftingStore.ts index efa9a3d..620c4b9 100644 --- a/src/lib/game/stores/craftingStore.ts +++ b/src/lib/game/stores/craftingStore.ts @@ -10,6 +10,7 @@ import { useCombatStore } from './combatStore'; import { useUIStore } from './uiStore'; import { getEnchantingEfficiencyBonus } from '../effects/discipline-effects'; +import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects'; import * as ApplicationActions from '../crafting-actions/application-actions'; import * as PreparationActions from '../crafting-actions/preparation-actions'; import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '../crafting-actions/equipment-actions'; @@ -19,6 +20,7 @@ import { createDefaultCraftingState } from './crafting-initial-state'; import { craftMaterial as craftMaterialAction } from '../crafting-actions/crafting-material-actions'; import { processEquipmentCraftingTick } from './crafting-equipment-tick'; import { startCraftingEquipment, cancelEquipmentCrafting, startFabricatorCrafting } from './pipelines/equipment-crafting'; +import { computeEffects } from '../effects/upgrade-effects'; export const useCraftingStore = create()( persist( @@ -65,6 +67,10 @@ export const useCraftingStore = create()( // Update currentAction in combatStore useCombatStore.setState({ currentAction: 'design' }); } else if (!state.designProgress2) { + // Check for Enchant Mastery before allowing second design slot + const computedEffects = computeEffects(); + const hasEnchantMastery = hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_MASTERY); + if (!hasEnchantMastery) return false; updates = { designProgress2: { designId: CraftingUtils.generateDesignId(), diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index 7a57b06..61e1dac 100644 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -317,7 +317,7 @@ export const useGameStore = create()( // the delta (9× additional) to avoid double-counting. const boostedRegen = baseRegen * 10; const netBoostedRegen = Math.max(0, boostedRegen * (1 - incursionStrength) * meditationMultiplier - conversionResult.totalRawDrain); - const regenDelta = netBoostedRegen - netRawRegen; + const regenDelta = Math.max(0, netBoostedRegen - netRawRegen); rawMana = Math.min(rawMana + regenDelta * HOURS_PER_TICK, maxMana); for (const [elem, entry] of Object.entries(conversionResult.rates)) { if (entry.paused || entry.finalRate <= 0 || !elements[elem]) continue; @@ -351,6 +351,9 @@ export const useGameStore = create()( if (enchantingResult.writes.attunement) { writes.attunement = { ...(writes.attunement || {}), ...enchantingResult.writes.attunement }; } + if (enchantingResult.writes.crafting) { + writes.crafting = { ...(writes.crafting || {}), ...enchantingResult.writes.crafting }; + } } // Phase 3: Write diff --git a/src/lib/game/stores/pipelines/enchanting-tick.ts b/src/lib/game/stores/pipelines/enchanting-tick.ts index 8237623..ddf43a1 100644 --- a/src/lib/game/stores/pipelines/enchanting-tick.ts +++ b/src/lib/game/stores/pipelines/enchanting-tick.ts @@ -1,13 +1,16 @@ // ─── Enchanting Tick Handlers ───────────────────────────────────────────────── // Design → Prepare → Application tick processing for the enchanting pipeline. // Extracted from gameStore.ts to keep the coordinator under the 400-line limit. +// +// IMPORTANT: This function reads from the ctx snapshot and returns writes. +// It must NOT call useCraftingStore.setState() directly — that would bypass +// the tick pipeline's snapshot-and-batch-write pattern and cause race conditions. import { HOURS_PER_TICK } from '../../constants'; import type { ComputedEffects } from '../../effects/upgrade-effects.types'; import { calculateDesignProgress, createCompletedDesignFromProgress } from '../../crafting-design'; import { calculatePreparationTick, completePreparation } from '../../crafting-prep'; import { calculateApplicationTick, applyEnchantments, updateEnchanterAttunement } from '../../crafting-apply'; -import { useCraftingStore } from '../craftingStore'; import { getEnchantingEfficiencyBonus } from '../../effects/discipline-effects'; import type { TickContext, TickWrites } from '../tick-pipeline'; @@ -26,6 +29,11 @@ export function processEnchantingTicks( const writes: Partial = {}; const currentAction = ctx.combat.currentAction; + // Helper to merge crafting writes + const mergeCrafting = (partial: Partial) => { + writes.crafting = { ...(writes.crafting || {}), ...partial } as typeof writes.crafting; + }; + // ── Phase 1: Design ────────────────────────────────────────────────────── if (currentAction === 'design') { const designProgress = ctx.crafting.designProgress; @@ -54,19 +62,29 @@ export function processEnchantingTicks( }, getEnchantingEfficiencyBonus(), ); - useCraftingStore.getState().saveDesign(completedDesign); + // Return write instead of calling store directly + mergeCrafting({ + enchantmentDesigns: [...ctx.crafting.enchantmentDesigns, completedDesign], + designProgress: designProgress ? null : undefined, + designProgress2: designProgress2 ? null : undefined, + }); addLog('Design "' + completedDesign.name + '" completed!'); writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' }; } else { + // Return write instead of calling store directly if (designProgress) { - useCraftingStore.getState().setDesignProgress({ - ...designProgress, - progress: designResult.progress, + mergeCrafting({ + designProgress: { + ...designProgress, + progress: designResult.progress, + }, }); } else if (designProgress2) { - useCraftingStore.getState().setDesignProgress2({ - ...designProgress2, - progress: designResult.progress, + mergeCrafting({ + designProgress2: { + ...designProgress2, + progress: designResult.progress, + }, }); } } @@ -81,7 +99,7 @@ export function processEnchantingTicks( } else { const instance = ctx.crafting.equipmentInstances[prepProgress.equipmentInstanceId]; if (!instance) { - useCraftingStore.getState().setPreparationProgress(null); + mergeCrafting({ preparationProgress: null }); writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' }; addLog('Preparation failed: equipment not found.'); } else { @@ -97,20 +115,21 @@ export function processEnchantingTicks( } if (prepResult.isComplete) { const completionResult = completePreparation(instance); - useCraftingStore.setState((s) => ({ - equipmentInstances: { - ...s.equipmentInstances, - [prepProgress.equipmentInstanceId]: completionResult.updatedInstance, - }, + const newInstances = { ...ctx.crafting.equipmentInstances }; + newInstances[prepProgress.equipmentInstanceId] = completionResult.updatedInstance; + mergeCrafting({ + equipmentInstances: newInstances, preparationProgress: null, - })); + }); writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' }; addLog(completionResult.logMessage); } else { - useCraftingStore.getState().setPreparationProgress({ - ...prepProgress, - progress: prepResult.progress, - manaCostPaid: prepResult.manaCostPaid, + mergeCrafting({ + preparationProgress: { + ...prepProgress, + progress: prepResult.progress, + manaCostPaid: prepResult.manaCostPaid, + }, }); } } @@ -128,7 +147,7 @@ export function processEnchantingTicks( const instance = ctx.crafting.equipmentInstances[appProgress.equipmentInstanceId]; const design = ctx.crafting.enchantmentDesigns.find((d) => d.id === appProgress.designId); if (!instance || !design) { - useCraftingStore.getState().setApplicationProgress(null); + mergeCrafting({ applicationProgress: null }); writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' }; addLog('Enchantment failed: equipment or design not found.'); } else { @@ -146,13 +165,12 @@ export function processEnchantingTicks( } if (appResult.isComplete) { const applyResult = applyEnchantments(instance, design, effects); - useCraftingStore.setState((s) => ({ - equipmentInstances: { - ...s.equipmentInstances, - [appProgress.equipmentInstanceId]: applyResult.updatedInstance, - }, + const newInstances = { ...ctx.crafting.equipmentInstances }; + newInstances[appProgress.equipmentInstanceId] = applyResult.updatedInstance; + mergeCrafting({ + equipmentInstances: newInstances, applicationProgress: null, - })); + }); const updatedAttunements = updateEnchanterAttunement( ctx.attunement.attunements, applyResult.xpGained, @@ -164,10 +182,12 @@ export function processEnchantingTicks( addLog('Free enchantment triggered! No mana consumed this tick.'); } } else { - useCraftingStore.getState().setApplicationProgress({ - ...appProgress, - progress: appResult.progress, - manaSpent: appResult.manaSpent, + mergeCrafting({ + applicationProgress: { + ...appProgress, + progress: appResult.progress, + manaSpent: appResult.manaSpent, + }, }); } } diff --git a/src/lib/game/stores/prestigeStore.ts b/src/lib/game/stores/prestigeStore.ts index ef28928..c1429ca 100644 --- a/src/lib/game/stores/prestigeStore.ts +++ b/src/lib/game/stores/prestigeStore.ts @@ -144,6 +144,15 @@ export const usePrestigeStore = create()( }, cancelPactRitual: () => { + const state = get(); + if (state.pactRitualFloor !== null) { + const guardian = getGuardianForFloor(state.pactRitualFloor); + if (guardian) { + useManaStore.setState((s) => ({ + rawMana: s.rawMana + guardian.pactCost, + })); + } + } set({ pactRitualFloor: null, pactRitualProgress: 0, diff --git a/src/lib/game/utils/spire-utils.ts b/src/lib/game/utils/spire-utils.ts index 707ab15..3d361ec 100644 --- a/src/lib/game/utils/spire-utils.ts +++ b/src/lib/game/utils/spire-utils.ts @@ -245,22 +245,24 @@ function generateSpeedRoom(floor: number, element: string, baseHP: number): Floo // ─── Enemy Stat Scaling ─────────────────────────────────────────────────────── -export function getSpireEnemyArmor(floor: number): number { +export function getSpireEnemyArmor(floor: number, seed: number = floor * 31337): number { if (floor < 10) return 0; + const rng = makeSeededRandom(seed); const baseChance = Math.min(0.5, (floor - 10) * 0.01); - if (Math.random() > baseChance) return 0; + if (rng() > baseChance) return 0; const minArmor = 0.05; const maxArmor = 0.30; const progress = Math.min(1, (floor - 10) / 90); - return minArmor + (maxArmor - minArmor) * progress * Math.random(); + return minArmor + (maxArmor - minArmor) * progress * rng(); } -export function getSpireEnemyBarrier(floor: number, element: string): number { +export function getSpireEnemyBarrier(floor: number, element: string, seed: number = floor * 5773): number { if (floor < 15) return 0; + const rng = makeSeededRandom(seed); const barrierElements = ['light', 'water', 'earth']; const baseChance = barrierElements.includes(element) ? 0.12 : 0.06; const floorBonus = Math.min(0.2, (floor - 15) * 0.003); - if (Math.random() > Math.min(0.35, baseChance + floorBonus)) return 0; + if (rng() > Math.min(0.35, baseChance + floorBonus)) return 0; const progress = Math.min(1, (floor - 15) / 85); return 0.1 + progress * 0.2; }