Previously the pact affinity cast speed bonus was only applied to invocation
spells. Per spec §6.2 and §10.2, it should apply to ALL spell casts (active,
equipment, and invocation).
Changes:
- invocation-utils.ts: Added computeAttackSpeedMultFromPactAffinity() helper
that combines prestige upgrade + discipline bonus and applies the diminishing
returns formula
- gameStore.ts: Compute total pact affinity (prestige + discipline) and apply
cast speed bonus to attackSpeedMult passed to processCombatTick
- combat-actions.ts: Use attackSpeedMult directly for active/equipment spell cast
progress (bonus already baked in)
- combat-invocation.ts: Use attackSpeedMult directly for invocation cast progress
(bonus already baked in), removed redundant pactAffinity parameter
Implements the full Invocation System as per spec §13 with all 22 acceptance criteria:
- Invocation charge meter with passive fill and active drain
- Auto-activation when charge reaches 100 with living enemies
- Guardian selection by elemental bonus × tierMultiplier scoring
- Spell selection from guardian's full element spellbook (not limited to learned)
- Step-down to affordable spells, auto-end when none affordable
- Charge drain during invocation with spell cost and discipline scaling
- Pact affinity cast speed bonus (diminishing returns, max 50%)
- guardian-invocation discipline with 4 perks (efficiency/speed/sustain/mastery)
- Cost multiplier (base 0.1, min 0.05) and drain multiplier (base 1.0, min 0.7)
- Signal recharged on spire exit and reset on descent
- Invocation Panel UI in SpireCombatPage with charge meter and status
- Compact invocation status indicator in SpireCombatControls
Files changed:
- data/disciplines/invoker.ts: Added guardian-invocation discipline definition
- effects/discipline-effects.ts: Added invocationChargeRateBonus, drainRateMultiplier, invocationCostReduction stat keys
- utils/invocation-utils.ts: NEW - All invocation utility functions (guardian selection, spell selection, charge rate, cost/drain multipliers)
- stores/combat-state.types.ts: Added invocationCharge and activeInvocation fields
- stores/combatStore.ts: Added invocation state defaults, resetInvocationState action, partialize, spire exit reset
- stores/combat-invocation.ts: NEW - Extracted invocation tick processing (charge fill/drain, casting, auto-activate/end)
- stores/combat-melee.ts: NEW - Extracted melee combat processing (keeps combat-actions.ts under 400 lines)
- stores/combat-actions.ts: Integrated invocation and melee modules
- __tests__/invocation-system.test.ts: NEW - 39 comprehensive tests
- SpireCombatPage.tsx: Added InvocationPanel between SpireHeader and RoomDisplay
- SpireCombatControls.tsx: Added compact invocation status indicator
All 1235 tests pass (including 39 new invocation tests).
- New spec at docs/specs/attunements/invoker/systems/invocation-system-spec.md
- Invocation Charge meter (0-100) fills passively, drains while invoking
- Channeled guardian auto-casts spells from their elements at reduced cost
- Spell selection picks best affordable spell, steps down as mana depletes
- No guardian swap mid-invocation; ends when charge depleted or room cleared
- Pact Affinity extended with diminishing-returns cast speed bonus (max 50%)
- New guardian-invocation discipline with 4 perks (efficiency, speed, sustain, mastery)
Defense-in-depth fix across 2 files:
1. combat-descent-actions.ts: Added castProgress: 0 and weaponCastProgress: {}
to the isDescentComplete set() call so the meter resets immediately.
2. combat-actions.ts: Added isDescentComplete to the early-exit guard in
processCombatTick so combat processing stops entirely once descent is
complete, preventing phantom spell casts from stale castProgress values.
All 1196 tests pass.
Bug 1 - No auto-resume: Replaced 'if (disc.autoPaused) continue' with a
check that tests whether sufficient mana is now available. If so, clears
autoPaused and processes the tick normally. Mana regen runs before
discipline processing, so updated mana values are already available.
Bug 2 - Bonuses while paused: Added '&& !disc.autoPaused' filter in
discipline-effects.ts so paused disciplines no longer contribute stat
bonuses, conversion rates, or perk effects.
All 1196 tests pass.
- Remove isConversionDiscipline guard in discipline-slice.ts:processTick
that was skipping mana drain for all 24 conversion disciplines
- Update test to verify conversion disciplines DO drain mana
- Add regression tests for auto-pause and XP accrual on conversion disciplines
- Fix misleading comments in elemental-regen.ts and elemental-regen-advanced.ts
All 1196 tests pass.
- Wrap library XP grant in progress < required guard so completed rooms don't grant XP every tick
- Add early return in treasure room tick when progress >= required to skip loot processing
- Initialize non-combat rooms (library, treasure, recovery) during descent in onEnterRoomDescend
- Add regression test file: non-combat-room-reward-guards.test.ts (8 tests)
Update Section 2 (Cost Formula), cost table, Section 8 display example,
and Section 11 formula summary to reflect the new linear cost formulas:
- rawCost: 10^(d+1) → 2×distance
- componentCost: 10×(d+1) → 3×distance
These changes keep the spec in sync with the fix for issue #378.
- Changed rawCost from exponential 10^(d+1) to linear 2*distance
- Base (d=1): 100 → 2
- Composite (d=2): 1,000 → 4
- Exotic (d=3): 10,000 → 6
- Time (d=4): 100,000 → 8
- Changed componentCost from 10*(d+1) to 3*distance
- Composite (d=2): 30 → 6 per component
- Exotic (d=3): 40 → 9 per component
- Time (d=4): 50 → 12 per component
- Updated test comments and expectations in conversion-pause-bug-regression.test.ts
and mana-conversion-component-deduction.test.ts to match new values
Root cause: The exponential rawCost formula produced values 100-10000x too high,
making mana conversion permanently paused since drain (rate × cost) always exceeded
even late-game raw regen (~20-50/hr). The new linear formula allows conversions to
be sustainable at all game stages.
Fixes#378
- Removed auto-unlock lines in gameStore.ts element regen loop and recovery room boost loop
- Added unlocked guard to skip locked elements during conversion processing
- Added regression test (bug-377) verifying only transference stays unlocked
Fixes#377
- Modified generateGuardianName() in guardian-encounters.ts to add a cycleOffset (floor/50) that shifts prefix selection for the same elements at different 100-floor ranges
- For multi-element names, also shift the title by cycleOffset to further reduce collision chance
- Added regression test guardian-names-unique.test.ts with 7 test cases covering:
* Previously colliding floors (170/370 crystal, 200/230 exotic convergence, 210/240 astral convergence)
* All static guardian floors (10-240) uniqueness
* All procedural guardian floors (250-490) uniqueness
* Full range (10-490) uniqueness
* Determinism check
* Title format validation
Fixes#376
- Wrap GameOverScreen in ErrorBoundary in page.tsx to prevent blank page on render errors
- Add defensive Number.isFinite checks in GameOverScreen for all numeric props
- Add regression test for day 30 → game-over flow (day30-blank-page.test.ts)
Fixes#375
- playwright.config.ts: change baseURL from dev site to localhost:3000
- combat-happy-path.spec.ts: fix climb button location (LeftPanel, not spire tab), fix descent via store, handle game-over from day overflow, reduce tick counts to avoid day 30 limit
- fabricator-happy-path.spec.ts: set currentAction to meditate before crafting (required by startFabricatorCrafting)
- playtest.spec.ts: rewrite from scratch — use localhost, window.__TEST__ bridge (not window.__debug), current tab names (no grimoire/element tabs), split into 3 files under 400-line limit
- playtest-basic-ui.spec.ts: sections 1-3 (basic UI, stats, spire)
- playtest-tabs.spec.ts: sections 4-11 (all tab navigation tests)
- playtest-debug.spec.ts: sections 12-14 (debug tab, bridge, stress test)
- EnchantmentDesigner now reads selection state directly from useCraftingStore
instead of receiving it as props, so state survives tab unmount/remount
- EnchanterSubTab no longer uses local useState for selectedEquipmentType,
selectedEffects, designName, selectedDesign — all sourced from store
- Removed unused EnchantmentDesignerProps interface from types
- All 1158 existing tests pass, no new type errors introduced
Fixes#366
- #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.
- #354: unlockAttunement now uses _get() instead of undefined 'state' variable
- #353: startPreparing now deducts raw mana from the mana store after validation
- #352: processGolemAttacks/processBasicAttack accept current mana as params instead of initializing to 0/{}
- Updated golem-combat-actions.test.ts to pass new currentRawMana/currentElements params
- Added regression tests for all 3 bugs (16 new tests, all passing)
Issue #346: Added floorHP > 0 guard to primary and equipment spell casting
while loops in combat-actions.ts. Previously spells continued casting and
draining mana after all enemies were dead.
Issue #345 Bug A: Populated equipmentSpellStates from equipped gear in
gameStore.ts combat tick setup using getActiveEquipmentSpells(). Previously
equipment spell enchantments (e.g., spell_manaBolt on casters) never fired
during combat because equipmentSpellStates was always empty.
Issue #345 Bug B: Added deductWeaponEnchantCosts() helper in combat-damage.ts
and wired it into the melee loop in combat-actions.ts. Weapon enchant spells
(fireBlade, frostBlade, lightningBlade, voidBlade) now properly deduct mana
per melee hit instead of providing free elemental bonus damage.
All 1141 tests pass (65 test files), no regressions. Added 5 new regression tests.
Files changed:
- combat-actions.ts: +floorHP>0 guards, weapon enchant cost deduction
- combat-damage.ts: +deductWeaponEnchantCosts() helper
- gameStore.ts: +equipment spell state population from equipped gear
- spell-cast-floorhp-guard.test.ts: new regression test file
Two root causes fixed:
1. gameStore.ts: computeRegen now receives actual attunements instead of empty {}, so rawGrossRegen includes attunement contributions (was ~2/hr, now correct 3000+/hr)
2. gameStore.ts buildConversionParams: use rawManaRegen with level scaling (1.5^(level-1)) instead of conversionRate for per-element grossRegen
3. LeftPanel.tsx: same grossRegen fix + include attunement regen in rawGrossRegen display
4. ElementStatsSection.tsx: same grossRegen fix
5. useGameDerived.ts: pass actual attunements to computeRegen for baseRegen calculation
Added regression test: conversion-pause-bug-regression.test.ts (7 tests)
- processTick() now calls calculateManaDrain() for non-conversion disciplines
- Insufficient mana triggers auto-paused state (skips XP accrual)
- Conversion disciplines (sourceManaTypes) correctly skip pool drain
- Auto-paused disciplines can be re-activated when mana is restored
- Updated 7 tests across 3 files to reflect drain model
- Use formatHour() instead of raw {hour} to display time in HH:MM format
(e.g. '15:00' instead of '14.999999999999998')
- Export formatHour from stores barrel index
- Fixes#336
- Add missing elements to mana store migration: when loading an old save
that doesn't have all elements from ELEMENTS (e.g. saves from before
composite/exotic elements were added), the migration now creates the
missing elements with proper default state
- Harden unlockElement action to handle the case where an element doesn't
exist in the store (creates a valid element state instead of corrupting
with { unlocked: true } missing current/max/baseMax)
- Add regression tests (14 tests) for Earth element unlock and migration
scenarios including old saves with missing elements
Fixes#338, #339
- Add activeCraftingSubTab state + setActiveCraftingSubTab action to
craftingStore (persisted to localStorage via zustand persist middleware)
- Add CraftingAttunement type to craftingStore.types and re-export from
stores/index.ts barrel
- Update CraftingTab.tsx to read/write active sub-tab from store instead
of local useState, so selection survives component remount
- Add default 'fabricator' value in craft-initial-state.ts
- Add getTwoHandedBlocker() helper to EquipmentSlotGrid that detects when
mainHand has a two-handed weapon and returns the weapon name
- Render offHand slot with Lock icon + 'Blocked: <weapon name>' label +
reduced opacity when blocked, distinct from normal 'Empty' dashed-border slot
- Add regression test verifying all 4 two-handed types are correctly flagged
and non-two-handed types remain unblocked (11 tests)
DISC-2: Removed old pool-drain model from discipline-slice.ts processTick()
- Disciplines no longer drain rawMana or element pools
- canProceedDiscipline() no longer checks mana sufficiency
- Removed auto-pause on insufficient mana from processTick()
- Removed drain display and auto-paused message from DisciplineCard.tsx
- Removed auto-paused log from gameStore.ts tick()
DISC-4: Audited crafting pipeline — no composite crafting logic remains
- craftComposite already removed from manaStore.ts (comment only)
- No other composite crafting references found
DISC-5: Added collapsible formula reference to Conversion Stats section
- Shows unified formula, multipliers, cost formulas, and constraints
DISC-6: Added per-element net regen summary line
- Shows 'Net Fire Regen: +0.50/hr − 0.15/hr = +0.35/hr' per element
DISC-7: Created dedicated 'Conversion Stats' section in Stats tab
- Renamed from 'Conversion Breakdown' to dedicated section header
DISC-8: Added detailed per-element regen breakdown to ManaDisplay
- Each element card now expandable to show produced rate and downstream drains
- New ElementRegenBreakdown type and elementRegenBreakdown prop
Tests: Updated 4 test files to reflect new no-drain behavior
- All 1090 tests pass
- D2: Move spell_iceShard to basic-spells.ts at cost 75 (canonical), remove duplicate from frost-spells.ts
- D7: disenchantEquipment now adds 'Ready for Enchantment' tag and resets rarity to 'common'
- D8: disenchantEquipment now credits recovered mana to raw mana pool
- D14: Wire enchantPower stat from discipline effects into efficiencyBonus via new getEnchantingEfficiencyBonus() helper
- D3: Fix spec file reference from crafting-attunements.ts to data/attunements.ts
- D1/D6: Add missing metalSpellFocus to spec capacity table
- D-SLOT-01: Verified slot cap of 7 matches spec §2.2 (no change needed)
- D-COMB-03: Implement AoE damage distribution for Sand/Shadowglass frames
- D-COMB-01: Reconcile armor pierce formula to spire-combat spec §9.4 (dmg × (1 + armorPierce))
- D-CIRC-01: Fix Simple Logic Circuit summon cost from raw to earth mana
- D-ENCHANT-03: Add dual_attunement unlockRequirement to all golem enchantments
- D-CORE-01/02: Add Guardian Core runtime override mechanism for guardian-specific mana
Also increased test timeouts for module import tests that timeout in full suite runs.