fix: resolve 7 medium-priority bugs from audit #372
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s
- #371: Replace Math.random() with seeded PRNG in getSpireEnemyArmor/Barrier - #370: Add mana refund when cancelling pact ritual in cancelPactRitual - #367: Add ENCHANT_MASTERY check for design slot 2 in crafting store - #364: Fix useGameDerived to read crafting data from useCraftingStore - #363: Clamp recovery room regen delta to prevent negative mana loss - #365: Add shield/barrier/healthRegen fields to all procedural guardians - #362: Refactor enchanting tick pipeline to return writes instead of direct store calls Extracted procedural guardian generators into guardian-procedural.ts to stay under 400-line limit. All 1158 tests pass.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# 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.
|
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
|
1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_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.",
|
"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."
|
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── fabricator-wizard-recipes.ts
|
│ │ │ │ ├── fabricator-wizard-recipes.ts
|
||||||
│ │ │ │ ├── guardian-data.ts
|
│ │ │ │ ├── guardian-data.ts
|
||||||
│ │ │ │ ├── guardian-encounters.ts
|
│ │ │ │ ├── guardian-encounters.ts
|
||||||
|
│ │ │ │ ├── guardian-procedural.ts
|
||||||
│ │ │ │ └── loot-drops.ts
|
│ │ │ │ └── loot-drops.ts
|
||||||
│ │ │ ├── effects/
|
│ │ │ ├── effects/
|
||||||
│ │ │ │ ├── discipline-effects.ts
|
│ │ │ │ ├── discipline-effects.ts
|
||||||
|
|||||||
@@ -5,17 +5,13 @@
|
|||||||
// Tier 1: Base Elements (static, floors 10–80, guardian-data.ts)
|
// Tier 1: Base Elements (static, floors 10–80, guardian-data.ts)
|
||||||
// Tier 2: Composite Elements (static, floors 90–160, 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 3: Exotic Elements (static, floors 170–240, guardian-data.ts)
|
||||||
// Tier 4: Dual Element Pairs (dynamic, floors 250–280, this file)
|
// Tier 4+: Procedural (dynamic, floors 250+, guardian-procedural.ts)
|
||||||
// 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)
|
|
||||||
//
|
//
|
||||||
// All lookups go through getGuardianForFloor() which merges static + procedural.
|
// 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 { STATIC_GUARDIANS } from './guardian-data';
|
||||||
import { resolveMultiUnlockChain } from '../utils/guardian-utils';
|
import { getProceduralGuardian } from './guardian-procedural';
|
||||||
|
|
||||||
// ─── Name Generation ────────────────────────────────────────────────────────────
|
// ─── Name Generation ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -71,247 +67,7 @@ export function getGuardianHP(floor: number): number {
|
|||||||
return Math.floor(base * Math.pow(floor / 10, exponent));
|
return Math.floor(base * Math.pow(floor / 10, exponent));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tier Helpers ───────────────────────────────────────────────────────────────
|
// ─── Color Blending ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
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 ──────────────────────────
|
|
||||||
|
|
||||||
const ELEMENT_COLORS: Record<string, string> = {
|
const ELEMENT_COLORS: Record<string, string> = {
|
||||||
fire: '#FF6B35', water: '#4ECDC4', air: '#00D4FF', earth: '#F4A261',
|
fire: '#FF6B35', water: '#4ECDC4', air: '#00D4FF', earth: '#F4A261',
|
||||||
@@ -323,7 +79,7 @@ const ELEMENT_COLORS: Record<string, string> = {
|
|||||||
soul: '#E8D5F5', time: '#C5B99A', plasma: '#FF6B9D',
|
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 c1 = ELEMENT_COLORS[el1] || '#888888';
|
||||||
const c2 = ELEMENT_COLORS[el2] || '#888888';
|
const c2 = ELEMENT_COLORS[el2] || '#888888';
|
||||||
try {
|
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 ────────────────────────────────────────────────────
|
// ─── Unified Guardian System ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Get the guardian for any floor. Returns null if no guardian at that floor. */
|
/** 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. */
|
/** All guardian floors — dynamically computed. */
|
||||||
export function getAllGuardianFloors(): number[] {
|
export function getAllGuardianFloors(): number[] {
|
||||||
// Static floors from guardian-data.ts
|
|
||||||
const staticFloors = Object.keys(STATIC_GUARDIANS).map(Number);
|
const staticFloors = Object.keys(STATIC_GUARDIANS).map(Number);
|
||||||
// Procedural floors: every 10th floor from 250 to 490
|
|
||||||
const proceduralFloors: number[] = [];
|
const proceduralFloors: number[] = [];
|
||||||
for (let f = 250; f <= 490; f += 10) {
|
for (let f = 250; f <= 490; f += 10) proceduralFloors.push(f);
|
||||||
proceduralFloors.push(f);
|
return Array.from(new Set([...staticFloors, ...proceduralFloors])).sort((a, b) => a - b);
|
||||||
}
|
|
||||||
const all = new Set([...staticFloors, ...proceduralFloors]);
|
|
||||||
return Array.from(all).sort((a, b) => a - b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if a floor is a guardian floor (every 10th floor with a defined guardian). */
|
/** Check if a floor is a guardian floor (every 10th floor with a defined guardian). */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useGameStore } from '../stores/gameStore';
|
import { useGameStore } from '../stores/gameStore';
|
||||||
|
import { useCraftingStore } from '../stores/craftingStore';
|
||||||
import { useManaStore } from '../stores/manaStore';
|
import { useManaStore } from '../stores/manaStore';
|
||||||
import { useCombatStore } from '../stores/combatStore';
|
import { useCombatStore } from '../stores/combatStore';
|
||||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||||
@@ -122,8 +123,8 @@ export function useCombatStats() {
|
|||||||
const currentFloor = useCombatStore((s) => s.currentFloor);
|
const currentFloor = useCombatStore((s) => s.currentFloor);
|
||||||
const activeSpell = useCombatStore((s) => s.activeSpell);
|
const activeSpell = useCombatStore((s) => s.activeSpell);
|
||||||
const { upgradeEffects } = useManaStats();
|
const { upgradeEffects } = useManaStats();
|
||||||
const equipmentInstances = useGameStore((s) => s.crafting.equipmentInstances);
|
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||||
const equippedInstances = useGameStore((s) => s.crafting.equippedInstances);
|
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||||
|
|
||||||
const floorElem = useMemo(
|
const floorElem = useMemo(
|
||||||
() => getFloorElement(currentFloor),
|
() => getFloorElement(currentFloor),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useCombatStore } from './combatStore';
|
|||||||
|
|
||||||
import { useUIStore } from './uiStore';
|
import { useUIStore } from './uiStore';
|
||||||
import { getEnchantingEfficiencyBonus } from '../effects/discipline-effects';
|
import { getEnchantingEfficiencyBonus } from '../effects/discipline-effects';
|
||||||
|
import { hasSpecial, SPECIAL_EFFECTS } from '../effects/special-effects';
|
||||||
import * as ApplicationActions from '../crafting-actions/application-actions';
|
import * as ApplicationActions from '../crafting-actions/application-actions';
|
||||||
import * as PreparationActions from '../crafting-actions/preparation-actions';
|
import * as PreparationActions from '../crafting-actions/preparation-actions';
|
||||||
import { equipItem as equipItemAction, unequipItem as unequipItemAction } from '../crafting-actions/equipment-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 { craftMaterial as craftMaterialAction } from '../crafting-actions/crafting-material-actions';
|
||||||
import { processEquipmentCraftingTick } from './crafting-equipment-tick';
|
import { processEquipmentCraftingTick } from './crafting-equipment-tick';
|
||||||
import { startCraftingEquipment, cancelEquipmentCrafting, startFabricatorCrafting } from './pipelines/equipment-crafting';
|
import { startCraftingEquipment, cancelEquipmentCrafting, startFabricatorCrafting } from './pipelines/equipment-crafting';
|
||||||
|
import { computeEffects } from '../effects/upgrade-effects';
|
||||||
|
|
||||||
export const useCraftingStore = create<CraftingStore>()(
|
export const useCraftingStore = create<CraftingStore>()(
|
||||||
persist(
|
persist(
|
||||||
@@ -65,6 +67,10 @@ export const useCraftingStore = create<CraftingStore>()(
|
|||||||
// Update currentAction in combatStore
|
// Update currentAction in combatStore
|
||||||
useCombatStore.setState({ currentAction: 'design' });
|
useCombatStore.setState({ currentAction: 'design' });
|
||||||
} else if (!state.designProgress2) {
|
} 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 = {
|
updates = {
|
||||||
designProgress2: {
|
designProgress2: {
|
||||||
designId: CraftingUtils.generateDesignId(),
|
designId: CraftingUtils.generateDesignId(),
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
// the delta (9× additional) to avoid double-counting.
|
// the delta (9× additional) to avoid double-counting.
|
||||||
const boostedRegen = baseRegen * 10;
|
const boostedRegen = baseRegen * 10;
|
||||||
const netBoostedRegen = Math.max(0, boostedRegen * (1 - incursionStrength) * meditationMultiplier - conversionResult.totalRawDrain);
|
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);
|
rawMana = Math.min(rawMana + regenDelta * HOURS_PER_TICK, maxMana);
|
||||||
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
|
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
|
||||||
if (entry.paused || entry.finalRate <= 0 || !elements[elem]) continue;
|
if (entry.paused || entry.finalRate <= 0 || !elements[elem]) continue;
|
||||||
@@ -351,6 +351,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
if (enchantingResult.writes.attunement) {
|
if (enchantingResult.writes.attunement) {
|
||||||
writes.attunement = { ...(writes.attunement || {}), ...enchantingResult.writes.attunement };
|
writes.attunement = { ...(writes.attunement || {}), ...enchantingResult.writes.attunement };
|
||||||
}
|
}
|
||||||
|
if (enchantingResult.writes.crafting) {
|
||||||
|
writes.crafting = { ...(writes.crafting || {}), ...enchantingResult.writes.crafting };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3: Write
|
// Phase 3: Write
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
// ─── Enchanting Tick Handlers ─────────────────────────────────────────────────
|
// ─── Enchanting Tick Handlers ─────────────────────────────────────────────────
|
||||||
// Design → Prepare → Application tick processing for the enchanting pipeline.
|
// Design → Prepare → Application tick processing for the enchanting pipeline.
|
||||||
// Extracted from gameStore.ts to keep the coordinator under the 400-line limit.
|
// 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 { HOURS_PER_TICK } from '../../constants';
|
||||||
import type { ComputedEffects } from '../../effects/upgrade-effects.types';
|
import type { ComputedEffects } from '../../effects/upgrade-effects.types';
|
||||||
import { calculateDesignProgress, createCompletedDesignFromProgress } from '../../crafting-design';
|
import { calculateDesignProgress, createCompletedDesignFromProgress } from '../../crafting-design';
|
||||||
import { calculatePreparationTick, completePreparation } from '../../crafting-prep';
|
import { calculatePreparationTick, completePreparation } from '../../crafting-prep';
|
||||||
import { calculateApplicationTick, applyEnchantments, updateEnchanterAttunement } from '../../crafting-apply';
|
import { calculateApplicationTick, applyEnchantments, updateEnchanterAttunement } from '../../crafting-apply';
|
||||||
import { useCraftingStore } from '../craftingStore';
|
|
||||||
import { getEnchantingEfficiencyBonus } from '../../effects/discipline-effects';
|
import { getEnchantingEfficiencyBonus } from '../../effects/discipline-effects';
|
||||||
import type { TickContext, TickWrites } from '../tick-pipeline';
|
import type { TickContext, TickWrites } from '../tick-pipeline';
|
||||||
|
|
||||||
@@ -26,6 +29,11 @@ export function processEnchantingTicks(
|
|||||||
const writes: Partial<TickWrites> = {};
|
const writes: Partial<TickWrites> = {};
|
||||||
const currentAction = ctx.combat.currentAction;
|
const currentAction = ctx.combat.currentAction;
|
||||||
|
|
||||||
|
// Helper to merge crafting writes
|
||||||
|
const mergeCrafting = (partial: Partial<typeof ctx.crafting>) => {
|
||||||
|
writes.crafting = { ...(writes.crafting || {}), ...partial } as typeof writes.crafting;
|
||||||
|
};
|
||||||
|
|
||||||
// ── Phase 1: Design ──────────────────────────────────────────────────────
|
// ── Phase 1: Design ──────────────────────────────────────────────────────
|
||||||
if (currentAction === 'design') {
|
if (currentAction === 'design') {
|
||||||
const designProgress = ctx.crafting.designProgress;
|
const designProgress = ctx.crafting.designProgress;
|
||||||
@@ -54,19 +62,29 @@ export function processEnchantingTicks(
|
|||||||
},
|
},
|
||||||
getEnchantingEfficiencyBonus(),
|
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!');
|
addLog('Design "' + completedDesign.name + '" completed!');
|
||||||
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
||||||
} else {
|
} else {
|
||||||
|
// Return write instead of calling store directly
|
||||||
if (designProgress) {
|
if (designProgress) {
|
||||||
useCraftingStore.getState().setDesignProgress({
|
mergeCrafting({
|
||||||
|
designProgress: {
|
||||||
...designProgress,
|
...designProgress,
|
||||||
progress: designResult.progress,
|
progress: designResult.progress,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else if (designProgress2) {
|
} else if (designProgress2) {
|
||||||
useCraftingStore.getState().setDesignProgress2({
|
mergeCrafting({
|
||||||
|
designProgress2: {
|
||||||
...designProgress2,
|
...designProgress2,
|
||||||
progress: designResult.progress,
|
progress: designResult.progress,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +99,7 @@ export function processEnchantingTicks(
|
|||||||
} else {
|
} else {
|
||||||
const instance = ctx.crafting.equipmentInstances[prepProgress.equipmentInstanceId];
|
const instance = ctx.crafting.equipmentInstances[prepProgress.equipmentInstanceId];
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
useCraftingStore.getState().setPreparationProgress(null);
|
mergeCrafting({ preparationProgress: null });
|
||||||
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
||||||
addLog('Preparation failed: equipment not found.');
|
addLog('Preparation failed: equipment not found.');
|
||||||
} else {
|
} else {
|
||||||
@@ -97,20 +115,21 @@ export function processEnchantingTicks(
|
|||||||
}
|
}
|
||||||
if (prepResult.isComplete) {
|
if (prepResult.isComplete) {
|
||||||
const completionResult = completePreparation(instance);
|
const completionResult = completePreparation(instance);
|
||||||
useCraftingStore.setState((s) => ({
|
const newInstances = { ...ctx.crafting.equipmentInstances };
|
||||||
equipmentInstances: {
|
newInstances[prepProgress.equipmentInstanceId] = completionResult.updatedInstance;
|
||||||
...s.equipmentInstances,
|
mergeCrafting({
|
||||||
[prepProgress.equipmentInstanceId]: completionResult.updatedInstance,
|
equipmentInstances: newInstances,
|
||||||
},
|
|
||||||
preparationProgress: null,
|
preparationProgress: null,
|
||||||
}));
|
});
|
||||||
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
||||||
addLog(completionResult.logMessage);
|
addLog(completionResult.logMessage);
|
||||||
} else {
|
} else {
|
||||||
useCraftingStore.getState().setPreparationProgress({
|
mergeCrafting({
|
||||||
|
preparationProgress: {
|
||||||
...prepProgress,
|
...prepProgress,
|
||||||
progress: prepResult.progress,
|
progress: prepResult.progress,
|
||||||
manaCostPaid: prepResult.manaCostPaid,
|
manaCostPaid: prepResult.manaCostPaid,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +147,7 @@ export function processEnchantingTicks(
|
|||||||
const instance = ctx.crafting.equipmentInstances[appProgress.equipmentInstanceId];
|
const instance = ctx.crafting.equipmentInstances[appProgress.equipmentInstanceId];
|
||||||
const design = ctx.crafting.enchantmentDesigns.find((d) => d.id === appProgress.designId);
|
const design = ctx.crafting.enchantmentDesigns.find((d) => d.id === appProgress.designId);
|
||||||
if (!instance || !design) {
|
if (!instance || !design) {
|
||||||
useCraftingStore.getState().setApplicationProgress(null);
|
mergeCrafting({ applicationProgress: null });
|
||||||
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
writes.combat = { ...(writes.combat || {}), currentAction: 'meditate' };
|
||||||
addLog('Enchantment failed: equipment or design not found.');
|
addLog('Enchantment failed: equipment or design not found.');
|
||||||
} else {
|
} else {
|
||||||
@@ -146,13 +165,12 @@ export function processEnchantingTicks(
|
|||||||
}
|
}
|
||||||
if (appResult.isComplete) {
|
if (appResult.isComplete) {
|
||||||
const applyResult = applyEnchantments(instance, design, effects);
|
const applyResult = applyEnchantments(instance, design, effects);
|
||||||
useCraftingStore.setState((s) => ({
|
const newInstances = { ...ctx.crafting.equipmentInstances };
|
||||||
equipmentInstances: {
|
newInstances[appProgress.equipmentInstanceId] = applyResult.updatedInstance;
|
||||||
...s.equipmentInstances,
|
mergeCrafting({
|
||||||
[appProgress.equipmentInstanceId]: applyResult.updatedInstance,
|
equipmentInstances: newInstances,
|
||||||
},
|
|
||||||
applicationProgress: null,
|
applicationProgress: null,
|
||||||
}));
|
});
|
||||||
const updatedAttunements = updateEnchanterAttunement(
|
const updatedAttunements = updateEnchanterAttunement(
|
||||||
ctx.attunement.attunements,
|
ctx.attunement.attunements,
|
||||||
applyResult.xpGained,
|
applyResult.xpGained,
|
||||||
@@ -164,10 +182,12 @@ export function processEnchantingTicks(
|
|||||||
addLog('Free enchantment triggered! No mana consumed this tick.');
|
addLog('Free enchantment triggered! No mana consumed this tick.');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
useCraftingStore.getState().setApplicationProgress({
|
mergeCrafting({
|
||||||
|
applicationProgress: {
|
||||||
...appProgress,
|
...appProgress,
|
||||||
progress: appResult.progress,
|
progress: appResult.progress,
|
||||||
manaSpent: appResult.manaSpent,
|
manaSpent: appResult.manaSpent,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,15 @@ export const usePrestigeStore = create<PrestigeStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
cancelPactRitual: () => {
|
cancelPactRitual: () => {
|
||||||
|
const state = get();
|
||||||
|
if (state.pactRitualFloor !== null) {
|
||||||
|
const guardian = getGuardianForFloor(state.pactRitualFloor);
|
||||||
|
if (guardian) {
|
||||||
|
useManaStore.setState((s) => ({
|
||||||
|
rawMana: s.rawMana + guardian.pactCost,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
set({
|
set({
|
||||||
pactRitualFloor: null,
|
pactRitualFloor: null,
|
||||||
pactRitualProgress: 0,
|
pactRitualProgress: 0,
|
||||||
|
|||||||
@@ -245,22 +245,24 @@ function generateSpeedRoom(floor: number, element: string, baseHP: number): Floo
|
|||||||
|
|
||||||
// ─── Enemy Stat Scaling ───────────────────────────────────────────────────────
|
// ─── Enemy Stat Scaling ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function getSpireEnemyArmor(floor: number): number {
|
export function getSpireEnemyArmor(floor: number, seed: number = floor * 31337): number {
|
||||||
if (floor < 10) return 0;
|
if (floor < 10) return 0;
|
||||||
|
const rng = makeSeededRandom(seed);
|
||||||
const baseChance = Math.min(0.5, (floor - 10) * 0.01);
|
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 minArmor = 0.05;
|
||||||
const maxArmor = 0.30;
|
const maxArmor = 0.30;
|
||||||
const progress = Math.min(1, (floor - 10) / 90);
|
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;
|
if (floor < 15) return 0;
|
||||||
|
const rng = makeSeededRandom(seed);
|
||||||
const barrierElements = ['light', 'water', 'earth'];
|
const barrierElements = ['light', 'water', 'earth'];
|
||||||
const baseChance = barrierElements.includes(element) ? 0.12 : 0.06;
|
const baseChance = barrierElements.includes(element) ? 0.12 : 0.06;
|
||||||
const floorBonus = Math.min(0.2, (floor - 15) * 0.003);
|
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);
|
const progress = Math.min(1, (floor - 15) / 85);
|
||||||
return 0.1 + progress * 0.2;
|
return 0.1 + progress * 0.2;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user