diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 8407feb..213226e 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,4 +1,4 @@ # Circular Dependencies -Generated: 2026-05-29T13:42:14.414Z +Generated: 2026-05-29T15:18:18.868Z No circular dependencies found. ✅ diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index a7840df..b03c45f 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-29T13:42:12.691Z", + "generated": "2026-05-29T15:18:17.066Z", "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." }, @@ -408,11 +408,13 @@ "data/golems/types.ts" ], "data/guardian-data.ts": [ - "types.ts" + "types.ts", + "utils/guardian-utils.ts" ], "data/guardian-encounters.ts": [ "data/guardian-data.ts", - "types.ts" + "types.ts", + "utils/guardian-utils.ts" ], "data/loot-drops.ts": [ "types/game.ts" @@ -702,6 +704,9 @@ "data/guardian-encounters.ts" ], "utils/formatting.ts": [], + "utils/guardian-utils.ts": [ + "constants/elements.ts" + ], "utils/index.ts": [ "utils/combat-utils.ts", "utils/floor-utils.ts", diff --git a/src/components/game/tabs/SpireSummaryTab.tsx b/src/components/game/tabs/SpireSummaryTab.tsx index c690845..7ba4357 100644 --- a/src/components/game/tabs/SpireSummaryTab.tsx +++ b/src/components/game/tabs/SpireSummaryTab.tsx @@ -180,12 +180,27 @@ function NextGuardianCard({ nextGuardian, nextGuardianData }: { nextGuardian: nu > {nextGuardianData.element.join(' + ')} - HP: {fmt(nextGuardianData.hp)} + Health: {fmt(nextGuardianData.hp)} {nextGuardianData.armor && ( Armor: {Math.round(nextGuardianData.armor * 100)}% )} + {nextGuardianData.shield && nextGuardianData.shield > 0 && ( + + Shield: {fmt(nextGuardianData.shield)} + + )} + {nextGuardianData.barrier && nextGuardianData.barrier > 0 && ( + + Barrier: {Math.round(nextGuardianData.barrier * 100)}% + + )} + {nextGuardianData.healthRegen && nextGuardianData.healthRegen > 0 && ( + + Regen: {nextGuardianData.healthRegenIsPercent ? nextGuardianData.healthRegen + '%/tick' : nextGuardianData.healthRegen + '/tick'} + + )} @@ -289,7 +304,7 @@ function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; gu > {guardian.element.join(' + ')} - HP: {fmt(guardian.hp)} + Health: {fmt(guardian.hp)} diff --git a/src/components/game/tabs/guardian-pacts-components.tsx b/src/components/game/tabs/guardian-pacts-components.tsx index 9adc48a..5bdce10 100644 --- a/src/components/game/tabs/guardian-pacts-components.tsx +++ b/src/components/game/tabs/guardian-pacts-components.tsx @@ -5,7 +5,7 @@ import { ELEMENTS } from '@/lib/game/constants'; import type { GuardianDef, GuardianBoon } from '@/lib/game/types'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { Shield, Swords, Clock, Sparkles, Check, Lock, ChevronRight } from 'lucide-react'; +import { Shield, Swords, Clock, Sparkles, Check, Lock, ChevronRight, Heart, Hexagon } from 'lucide-react'; import clsx from 'clsx'; import { DebugName } from '@/components/game/debug/debug-context'; @@ -152,20 +152,48 @@ GuardianCard.displayName = 'GuardianCard'; // ─── Guardian Stats ────────────────────────────────────────────────────────── function GuardianStats({ guardian }: { guardian: GuardianDef }) { + const hasShield = !!(guardian.shield && guardian.shield > 0); + const hasBarrier = !!(guardian.barrier && guardian.barrier > 0); + const hasHealthRegen = !!(guardian.healthRegen && guardian.healthRegen > 0); + return ( -
-
- - HP: {guardian.hp.toLocaleString()} -
-
- - PWR: {guardian.power.toLocaleString()} -
-
- - ARM: {Math.round((guardian.armor ?? 0) * 100)}% +
+
+
+ + Health: {guardian.hp.toLocaleString()} +
+
+ + Power: {guardian.power.toLocaleString()} +
+
+ + Armor: {Math.round((guardian.armor ?? 0) * 100)}% +
+ {(hasShield || hasBarrier || hasHealthRegen) && ( +
+ {hasShield && ( +
+ + Shield: {guardian.shield!.toLocaleString()} +
+ )} + {hasBarrier && ( +
+ + Barrier: {Math.round(guardian.barrier! * 100)}% +
+ )} + {hasHealthRegen && ( +
+ + Regen: {guardian.healthRegenIsPercent ? guardian.healthRegen + '%/tick' : guardian.healthRegen + '/tick'} +
+ )} +
+ )}
); } diff --git a/src/lib/game/data/guardian-data.ts b/src/lib/game/data/guardian-data.ts index b754ec2..a7f8f83 100644 --- a/src/lib/game/data/guardian-data.ts +++ b/src/lib/game/data/guardian-data.ts @@ -21,8 +21,8 @@ function hp(floor: number): number { return Math.floor(base * Math.pow(floor / 10, exponent)); } -function pactCost(hpVal: number, power: number, armor: number): number { - return Math.floor(hpVal * 0.3 + power * 5 + hpVal * armor * 0.5); +function pactCost(hpVal: number, power: number, armor: number, shield: number, barrier: number): number { + return Math.floor(hpVal * 0.3 + power * 5 + hpVal * armor * 0.5 + shield * 2 + hpVal * barrier * 0.3); } function mk( @@ -35,11 +35,21 @@ function mk( boons: GuardianDef['boons'], uniquePerk: string, effects: GuardianDef['effects'], + defensive?: { + shield?: number; + shieldRegen?: number; + barrier?: number; + barrierRegen?: number; + healthRegen?: number; + healthRegenIsPercent?: boolean; + }, ): GuardianDef { const hpVal = hp(floor); const power = Math.floor(hpVal * 0.5); const arm = armor; - const pc = pactCost(hpVal, power, arm); + const shield = defensive?.shield ?? 0; + const barrier = defensive?.barrier ?? 0; + const pc = pactCost(hpVal, power, arm, shield, barrier); const pt = 2 + Math.floor(floor / 10); return { @@ -49,6 +59,12 @@ function mk( pact: pactMult, color, armor: arm, + shield, + shieldRegen: defensive?.shieldRegen ?? 0, + barrier, + barrierRegen: defensive?.barrierRegen ?? 0, + healthRegen: defensive?.healthRegen ?? 0, + healthRegenIsPercent: defensive?.healthRegenIsPercent ?? false, boons, pactCost: pc, pactTime: pt, @@ -82,6 +98,7 @@ const TIER1: Record = { ], 'Water spells deal +15% damage', [{ type: 'armor_pierce', value: 0.15 }], + { healthRegen: 10, healthRegenIsPercent: false }, ), 30: mk(30, 'Ventus Rex', ['air'], '#00D4FF', 0.18, 2.0, [ @@ -98,6 +115,7 @@ const TIER1: Record = { ], 'Earth spells deal +25% damage to guardians', [{ type: 'armor_pierce', value: 0.2 }], + { shield: 200, shieldRegen: 5 }, ), 50: mk(50, 'Lux Aeterna', ['light'], '#FFD700', 0.20, 2.5, [ @@ -106,6 +124,7 @@ const TIER1: Record = { ], 'Light spells reveal enemy weaknesses (+20% damage)', [{ type: 'crit_chance', value: 0.1 }], + { barrier: 0.05, barrierRegen: 0.01 }, ), 60: mk(60, 'Umbra Mortis', ['dark'], '#9B59B6', 0.22, 2.75, [ @@ -114,6 +133,7 @@ const TIER1: Record = { ], 'Dark spells deal +25% damage to armored enemies', [{ type: 'crit_damage', value: 0.15 }], + { healthRegen: 5, healthRegenIsPercent: true }, ), 70: mk(70, 'Mors Ultima', ['death'], '#778CA3', 0.25, 3.0, [ @@ -122,6 +142,7 @@ const TIER1: Record = { ], 'Death spells execute enemies below 20% HP', [{ type: 'raw_damage', value: 0.1 }], + { shield: 400, shieldRegen: 10 }, ), 80: mk(80, 'Vinculum Arcana', ['transference'], '#1ABC9C', 0.20, 3.25, [ @@ -130,6 +151,7 @@ const TIER1: Record = { ], 'Transference spells have 25% reduced cost', [{ type: 'cost_reduction', value: 0.25 }], + { barrier: 0.08, barrierRegen: 0.02, healthRegen: 3, healthRegenIsPercent: true }, ), }; @@ -145,6 +167,7 @@ const TIER2: Record = { ], 'Metal spells pierce 20% armor', [{ type: 'armor_pierce', value: 0.2 }], + { shield: 600, shieldRegen: 15, healthRegen: 4, healthRegenIsPercent: true }, ), 100: mk(100, '', ['sand'], '#D4AC0D', 0.25, 3.75, [ @@ -153,6 +176,7 @@ const TIER2: Record = { ], 'Sand spells slow enemies by 25%', [{ type: 'slow', value: 0.25 }], + { barrier: 0.10, barrierRegen: 0.03, healthRegen: 5, healthRegenIsPercent: true }, ), 110: mk(110, '', ['lightning'], '#FFEB3B', 0.22, 4.0, [ @@ -161,6 +185,7 @@ const TIER2: Record = { ], 'Lightning spells chain to 2 additional targets', [{ type: 'chain', value: 2 }], + { shield: 800, shieldRegen: 20, barrier: 0.05, barrierRegen: 0.01 }, ), }; @@ -177,6 +202,7 @@ const TIER3: Record = { ], 'Tri-aspect: Metal, Fire, and Earth spells gain +10% effectiveness', [{ type: 'armor_pierce', value: 0.25 }, { type: 'burn', value: 0.1 }], + { shield: 1000, shieldRegen: 25, barrier: 0.05, barrierRegen: 0.01 }, ), 140: mk(140, '', ['sand', 'earth', 'water'], '#C9B896', 0.30, 4.75, [ @@ -186,6 +212,7 @@ const TIER3: Record = { ], 'Tri-aspect: Sand, Earth, and Water spells gain +10% effectiveness', [{ type: 'slow', value: 0.3 }, { type: 'armor_pierce', value: 0.15 }], + { barrier: 0.12, barrierRegen: 0.03, healthRegen: 6, healthRegenIsPercent: true }, ), 150: mk(150, '', ['lightning', 'fire', 'air'], '#FFE066', 0.28, 5.0, [ @@ -195,6 +222,7 @@ const TIER3: Record = { ], 'Tri-aspect: Lightning, Fire, and Air spells gain +10% effectiveness', [{ type: 'chain', value: 2 }, { type: 'cast_speed', value: 0.1 }], + { shield: 1200, shieldRegen: 30, healthRegen: 5, healthRegenIsPercent: true }, ), 160: mk(160, '', ['metal', 'lightning', 'fire', 'earth', 'air'], '#E8C872', 0.35, 5.25, [ @@ -204,6 +232,7 @@ const TIER3: Record = { ], 'Fused aspects: Lightning spells gain +20% armor pierce; Metal spells chain once', [{ type: 'armor_pierce', value: 0.3 }, { type: 'chain', value: 1 }], + { shield: 1500, shieldRegen: 40, barrier: 0.08, barrierRegen: 0.02, healthRegen: 7, healthRegenIsPercent: true }, ), }; @@ -220,6 +249,7 @@ const TIER4: Record = { ], 'Crystal spells reflect 15% damage back to attackers', [{ type: 'reflect', value: 0.15 }], + { shield: 2000, shieldRegen: 50, barrier: 0.10, barrierRegen: 0.03 }, ), 180: mk(180, '', ['stellar'], '#F0E68C', 0.30, 6.0, [ @@ -228,6 +258,7 @@ const TIER4: Record = { ], 'Stellar spells deal +30% damage at night', [{ type: 'night_bonus', value: 0.3 }], + { barrier: 0.15, barrierRegen: 0.04, healthRegen: 8, healthRegenIsPercent: true }, ), 190: mk(190, '', ['void'], '#4A235A', 0.35, 6.5, [ @@ -237,6 +268,7 @@ const TIER4: Record = { ], 'Void spells ignore 40% of all resistances', [{ type: 'resist_ignore', value: 0.4 }], + { shield: 2500, shieldRegen: 60, barrier: 0.10, barrierRegen: 0.02, healthRegen: 6, healthRegenIsPercent: true }, ), 200: mk(200, '', ['crystal', 'stellar', 'void'], '#B39DDB', 0.40, 7.0, [ @@ -246,6 +278,7 @@ const TIER4: Record = { ], 'Exotic convergence: All exotic spells gain +15% effectiveness', [{ type: 'reflect', value: 0.1 }, { type: 'resist_ignore', value: 0.1 }], + { shield: 3000, shieldRegen: 80, barrier: 0.12, barrierRegen: 0.03, healthRegen: 10, healthRegenIsPercent: true }, ), }; diff --git a/src/lib/game/stores/combat-state.types.ts b/src/lib/game/stores/combat-state.types.ts index ea63114..7f1059d 100644 --- a/src/lib/game/stores/combat-state.types.ts +++ b/src/lib/game/stores/combat-state.types.ts @@ -38,6 +38,12 @@ export interface CombatState { comboHitCount: number; floorHitCount: number; + // Guardian defensive state (shield, barrier, regen) + guardianShield: number; + guardianShieldMax: number; + guardianBarrier: number; + guardianBarrierMax: number; + // Spells spells: Record; diff --git a/src/lib/game/stores/combatStore.ts b/src/lib/game/stores/combatStore.ts index d57938c..74b6e0d 100644 --- a/src/lib/game/stores/combatStore.ts +++ b/src/lib/game/stores/combatStore.ts @@ -10,6 +10,7 @@ import { generateFloorState } from '../utils/room-utils'; import { generateSpireFloorState } from '../utils/spire-utils'; import { addActivityLogEntry } from '../utils/activity-log'; import { processCombatTick, makeInitialSpells } from './combat-actions'; +import { getGuardianForFloor } from '../data/guardian-encounters'; import type { CombatStore } from './combat-state.types'; export const useCombatStore = create()( @@ -46,6 +47,12 @@ export const useCombatStore = create()( comboHitCount: 0, floorHitCount: 0, + // Guardian defensive state + guardianShield: 0, + guardianShieldMax: 0, + guardianBarrier: 0, + guardianBarrierMax: 0, + // Spells spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 }, @@ -253,6 +260,22 @@ export const useCombatStore = create()( set((state) => ({ totalCraftsCompleted: state.totalCraftsCompleted + 1 })); }, + resetGuardianDefensiveState: () => { + set({ guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0 }); + }, + + initGuardianDefensiveState: () => { + const state = get(); + const guardian = getGuardianForFloor(state.currentFloor); + if (!guardian) return; + set({ + guardianShield: guardian.shield ?? 0, + guardianShieldMax: guardian.shield ?? 0, + guardianBarrier: guardian.barrier ?? 0, + guardianBarrierMax: guardian.barrier ?? 0, + }); + }, + processCombatTick: ( rawMana: number, elements: Record, @@ -316,6 +339,10 @@ export const useCombatStore = create()( totalSpellsCast: state.totalSpellsCast, totalDamageDealt: state.totalDamageDealt, totalCraftsCompleted: state.totalCraftsCompleted, + guardianShield: state.guardianShield, + guardianShieldMax: state.guardianShieldMax, + guardianBarrier: state.guardianBarrier, + guardianBarrierMax: state.guardianBarrierMax, }), } ) diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index 01a5a32..beba3f9 100644 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -1,7 +1,4 @@ -// ─── Game Store (Coordinator) ────────────────────────────────────── -// Manages: day, hour, incursionStrength, containmentWards -// Orchestrates tick across all stores via read → compute → write pipeline. - +// Game Store — coordinator, tick pipeline, time/incursion import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { HOURS_PER_TICK, MAX_DAY } from '../constants'; @@ -52,7 +49,6 @@ export const useGameStore = create()( ...initialState, initGame: () => { - // Wire discipline store ↔ combat store callbacks (breaks circular dependency) useDisciplineStore.getState().setPracticingCallbacks({ onStartPracticing: () => useCombatStore.getState().startPracticing(), onStopPracticing: () => useCombatStore.getState().stopPracticing(), @@ -62,7 +58,6 @@ export const useGameStore = create()( tick: () => { try { - // ── Phase 1: Read — snapshot all store states once ────────────────── const ctx = buildTickContext({ game: get(), ui: useUIStore.getState(), @@ -76,7 +71,6 @@ export const useGameStore = create()( if (ctx.ui.gameOver || ctx.ui.paused) return; - // Shared setters object — used by every applyTickWrites call below // eslint-disable-next-line @typescript-eslint/no-explicit-any const storeSetters = { setGame: set, @@ -90,11 +84,9 @@ export const useGameStore = create()( addLogs: (msgs: string[]) => msgs.forEach((m) => useUIStore.getState().addLog(m)), }; - // ── Phase 2: Compute — derive all updates ─────────────────────────── const writes: TickWrites = { logs: [] }; const addLog = (msg: string) => writes.logs.push(msg); - // Compute equipment and discipline effects const steadyHandLevel = ctx.prestige.prestigeUpgrades.steadyHand || 0; const enchantmentPowerMultiplier = 1 + steadyHandLevel * 0.15; const equipmentEffects = computeEquipmentEffects( @@ -120,7 +112,6 @@ export const useGameStore = create()( disciplineEffects, ) * (1 + (disciplineEffects.multipliers.regenMultiplier || 0)); - // Time progression let hour = ctx.game.hour + HOURS_PER_TICK; let day = ctx.game.day; if (hour >= 24) { @@ -128,7 +119,6 @@ export const useGameStore = create()( day += 1; } - // Shared insight params — reused for both loop-end and victory const insightParams = { maxFloorReached: ctx.combat.maxFloorReached, totalManaGathered: ctx.mana.totalManaGathered, @@ -136,11 +126,9 @@ export const useGameStore = create()( prestigeUpgrades: ctx.prestige.prestigeUpgrades, }; - // Check for loop end if (day > MAX_DAY) { const insightGained = calcInsight(insightParams, disciplineEffects); - - addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`); + addLog('The loop ends. Gained ' + insightGained + ' Insight.'); writes.ui = { ...(writes.ui || {}), gameOver: true, victory: false }; writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained }; writes.game = { day, hour }; @@ -148,11 +136,9 @@ export const useGameStore = create()( return; } - // Check for victory (3× insight multiplier) if (ctx.combat.maxFloorReached >= 100 && ctx.prestige.signedPacts.includes(100)) { const insightGained = calcInsight(insightParams, disciplineEffects) * 3; - - addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`); + addLog('VICTORY! The Awakened One falls! Gained ' + insightGained + ' Insight!'); writes.ui = { ...(writes.ui || {}), gameOver: true, victory: true }; writes.prestige = { ...(writes.prestige || {}), loopInsight: insightGained }; applyTickWrites(writes, storeSetters); @@ -161,7 +147,6 @@ export const useGameStore = create()( const incursionStrength = getIncursionStrength(day, hour); - // Meditation bonus tracking let meditateTicks = ctx.mana.meditateTicks; let meditationMultiplier = 1; @@ -172,7 +157,6 @@ export const useGameStore = create()( meditateTicks = 0; } - // Calculate total attunement conversion and apply to element pools let totalConversionPerTick = 0; let elements = { ...ctx.mana.elements }; Object.entries(ctx.attunement.attunements).forEach(([id, state]) => { @@ -195,11 +179,9 @@ export const useGameStore = create()( const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick); - // Mana regeneration let rawMana = Math.min(ctx.mana.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana); let totalManaGathered = ctx.mana.totalManaGathered; - // Convert action if (ctx.combat.currentAction === 'convert') { const convertResult = useManaStore.getState().processConvertAction(rawMana); if (convertResult) { @@ -208,7 +190,6 @@ export const useGameStore = create()( } } - // Pact ritual const pactResult = processPactRitual( ctx.prestige.pactRitualFloor, ctx.prestige.pactRitualProgress, @@ -222,7 +203,6 @@ export const useGameStore = create()( } pactResult.logs.forEach(l => addLog(l)); - // Discipline tick const disciplineResult = useDisciplineStore.getState().processTick({ rawMana, elements, @@ -230,69 +210,50 @@ export const useGameStore = create()( rawMana = disciplineResult.rawMana; elements = disciplineResult.elements; - // Apply discipline conversions: drain source mana, add to target element for (const [targetElem, conv] of Object.entries(disciplineEffects.conversions)) { const conversionAmount = conv.rate * HOURS_PER_TICK; - // Check that all source mana types are available (unlocked and have enough) let canConvert = true; for (const srcType of conv.sourceManaTypes) { if (srcType === 'raw') { - if (rawMana < conversionAmount) { - canConvert = false; - break; - } + if (rawMana < conversionAmount) { canConvert = false; break; } } else if (!elements[srcType] || !elements[srcType].unlocked || elements[srcType].current < conversionAmount) { - canConvert = false; - break; + canConvert = false; break; } } if (!canConvert) continue; - // Drain source mana types for (const srcType of conv.sourceManaTypes) { if (srcType === 'raw') { rawMana -= conversionAmount; } else if (elements[srcType]) { - elements[srcType] = { - ...elements[srcType], - current: elements[srcType].current - conversionAmount, - }; + elements[srcType] = { ...elements[srcType], current: elements[srcType].current - conversionAmount }; } } - // Add to target element if (elements[targetElem]) { elements[targetElem] = { ...elements[targetElem], - current: Math.min( - elements[targetElem].max, - elements[targetElem].current + conversionAmount, - ), + current: Math.min(elements[targetElem].max, elements[targetElem].current + conversionAmount), }; } } - // Unlock enchantment effects from newly unlocked discipline perks if (disciplineResult.unlockedEffects.length > 0) { useCraftingStore.getState().unlockEffects(disciplineResult.unlockedEffects); for (const effectId of disciplineResult.unlockedEffects) { - addLog(`✨ Discipline insight unlocked: ${effectId}`); + addLog('Discipline insight unlocked: ' + effectId); } } - - // Unlock fabricator recipes from newly unlocked discipline perks if (disciplineResult.unlockedRecipes.length > 0) { useCraftingStore.getState().unlockRecipes(disciplineResult.unlockedRecipes); for (const recipeId of disciplineResult.unlockedRecipes) { - addLog(`🔨 Fabricator recipe unlocked: ${recipeId}`); + addLog('Fabricator recipe unlocked: ' + recipeId); } } - // Apply per-element capacity bonuses from disciplines and equipment const perElementCapBonuses = mergePerElementCapBonuses( disciplineEffects.bonuses, equipmentEffects.bonuses, ); useManaStore.getState().computeElementMaxWithBonuses(perElementCapBonuses); - // Sync updated max/baseMax from mana store into tick elements snapshot const manaStateAfter = useManaStore.getState(); for (const [ek, es] of Object.entries(manaStateAfter.elements)) { if (elements[ek]) { @@ -310,10 +271,11 @@ export const useGameStore = create()( (floor, wasGuardian) => { if (wasGuardian) { const defeatedGuardian = getGuardianForFloor(floor); - addLog(`\u2694\ufe0f ${defeatedGuardian?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`); + addLog((defeatedGuardian?.name || 'Unknown') + ' defeated! Visit the Grimoire to sign a pact.'); } else if (floor % 5 === 0) { - addLog(`🏰 Floor ${floor} cleared!`); + addLog('Floor ' + floor + ' cleared!'); } + useCombatStore.setState({ guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0 }); }, (damage) => { let dmg = damage; @@ -323,6 +285,46 @@ export const useGameStore = create()( if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) { dmg *= 1.5; } + + const guardian = getGuardianForFloor(ctx.combat.currentFloor); + if (guardian && (guardian.shield || guardian.barrier || guardian.healthRegen)) { + let shield = ctx.combat.guardianShield; + let shieldMax = ctx.combat.guardianShieldMax; + let barrier = ctx.combat.guardianBarrier; + let barrierMax = ctx.combat.guardianBarrierMax; + + if (guardian.shieldRegen && shield < shieldMax) { + shield = Math.min(shieldMax, shield + guardian.shieldRegen * HOURS_PER_TICK); + } + if (guardian.barrierRegen && barrier < barrierMax) { + barrier = Math.min(barrierMax, barrier + guardian.barrierRegen * HOURS_PER_TICK); + } + + if (shield > 0 && dmg > 0) { + const absorb = Math.min(shield, dmg); + shield -= absorb; + dmg -= absorb; + } + + if (barrier > 0 && dmg > 0) { + dmg *= (1 - barrier); + } + + if (guardian.healthRegen && guardian.healthRegen > 0) { + const healAmount = guardian.healthRegenIsPercent + ? Math.floor(ctx.combat.floorMaxHP * guardian.healthRegen / 100 * HOURS_PER_TICK) + : Math.floor(guardian.healthRegen * HOURS_PER_TICK); + dmg -= healAmount; + } + + useCombatStore.setState({ + guardianShield: shield, + guardianShieldMax: shieldMax, + guardianBarrier: barrier, + guardianBarrierMax: barrierMax, + }); + } + return { rawMana, elements, modifiedDamage: dmg }; }, ctx.prestige.signedPacts, @@ -347,7 +349,6 @@ export const useGameStore = create()( }; } - // Equipment crafting tick — advance progress and complete when done if (ctx.combat.currentAction === 'craft') { const craftingResult = useCraftingStore.getState().processEquipmentCraftingTick(); if (craftingResult.logMessage) { @@ -355,7 +356,7 @@ export const useGameStore = create()( } } - // ── Phase 3: Write — batch all state updates ───────────────────────── + // Phase 3: Write writes.game = { day, hour, incursionStrength }; writes.mana = { rawMana, @@ -366,10 +367,9 @@ export const useGameStore = create()( applyTickWrites(writes, storeSetters); } catch (error: unknown) { - // Log error to UI store if available, otherwise console error try { const msg = error instanceof Error ? error.message : String(error); - useUIStore.getState().addLog(`⚠️ Tick error: ${msg}`); + useUIStore.getState().addLog('Tick error: ' + msg); } catch { console.error('Tick error:', error); } diff --git a/src/lib/game/types/attunements.ts b/src/lib/game/types/attunements.ts index 059d817..9a7093b 100644 --- a/src/lib/game/types/attunements.ts +++ b/src/lib/game/types/attunements.ts @@ -49,6 +49,12 @@ export interface GuardianDef { pactTime: number; // Hours required for pact ritual uniquePerk: string; // Description of unique perk armor?: number; // Damage reduction (0-1, e.g., 0.2 = 20% reduction) + shield?: number; // Flat damage absorption pool (absorbs damage before HP) + shieldRegen?: number; // Shield regeneration per tick (flat amount) + barrier?: number; // Percentage-based damage reduction (0-1, e.g., 0.1 = 10%) that absorbs damage before HP + barrierRegen?: number; // Barrier regeneration per tick (percentage of max barrier) + healthRegen?: number; // Health regeneration per tick (flat amount, can be percentage-based with healthRegenIsPercent) + healthRegenIsPercent?: boolean; // If true, healthRegen is % of max HP per tick; if false, flat HP per tick power: number; // Combat power for display effects: { type: string; value: number }[]; // Passive combat effects signingCost: { mana: number; time: number }; // Pact ritual cost & time