- Add ActiveEffect, EffectType types to game.ts; activeEffects + effectiveArmor on EnemyState - Add SpellOnHitEffect + onHitEffect field to SpellDefinition - Wire onHitEffect to fire (burn), death (curse), lightning (armor_corrode), frost (freeze), soul (bypassArmor burn) - Add applyOnHitEffect() — applies on-hit effect on successful spell hit (spec §6.2) - Add processDoTPhase() — ticks all active effects after weapon/golem attacks (spec §6.3) - Add bypassArmor/bypassBarrier support in applyEnemyDefenses() (AC-13) - Export standalone applyEnemyDefenses from combat-tick.ts for DoT pipeline - Split DoT runtime into separate dot-runtime.ts (135 lines) to keep combat-actions.ts under 400 lines - Update all enemy generation sites with activeEffects/effectiveArmor defaults - Fix test helpers for new required fields All 921 tests pass (45 test files)
12 KiB
Pact System — Design Spec
Describes the Guardian pact system: ritual flow, boon types, pact slot system, pact persistence, discipline scaling, and how the Invoker gains elemental mana.
1. Objective
The Pact system is the Invoker attunement's core progression mechanic. After defeating a Guardian boss on every 10th floor, the player can sign a pact through a ritual process. Each signed pact grants permanent boons (stat multipliers) and unlocks elemental mana types. Pact slots limit how many pacts can be active simultaneously, and the Invoker's disciplines amplify pact power.
Design goals:
- Pacts are earned through combat achievement (defeating Guardians)
- Ritual time creates a meaningful time investment
- Multiple pacts provide multiplicative power but with interference penalties
- Boon variety ensures each pact feels distinct
- Pact affinity (from disciplines) reduces ritual time
2. Pact Ritual Flow
2.1 Step 1: Defeat the Guardian
- Every 10th floor (10, 20, 30, ...) has a Guardian boss room
- Defeating the Guardian adds the floor number to
defeatedGuardians[] - Only defeated Guardians are eligible for pact signing
2.2 Step 2: Start Ritual
startPactRitual(floor):
1. Validate guardian exists at floor
2. Check floor is in defeatedGuardians
3. Check floor is NOT already in signedPacts
4. Check signedPacts.length < pactSlots (slot available)
5. Check rawMana >= guardian.pactCost (enough raw mana)
6. Check pactRitualFloor === null (no other ritual in progress)
7. Deduct guardian.pactCost raw mana
8. Set pactRitualFloor = floor, pactRitualProgress = 0
2.3 Step 3: Progress Ritual
Each game tick:
processPactRitual():
pactAffinity = min(0.9, pactAffinityUpgrade × 0.1 + pactAffinityBonus)
requiredTime = guardian.pactTime × (1 - pactAffinity)
pactRitualProgress += HOURS_PER_TICK
if pactRitualProgress >= requiredTime → completePactRitual()
Pact affinity sources:
pactAffinityUpgrade: prestige upgrade level (each level = +0.1, capped at 0.9)pactAffinityBonus: discipline bonus from Pact Attunement discipline
2.4 Step 4: Pact Signed
completePactRitual():
1. Add floor to signedPacts[]
2. Remove floor from defeatedGuardians[]
3. Reset pactRitualFloor = null, pactRitualProgress = 0
4. For each manaType in guardian.unlocksMana:
manaStore.unlockElement(manaType, 0)
5. Log: "📜 Pact signed with {name}! You have gained their boons."
6. Log: "✨ {ManaType} mana unlocked!" for each new element
2.5 Cancellation
cancelPactRitual() resets pactRitualFloor = null, pactRitualProgress = 0.
The raw mana cost is not refunded on cancellation.
3. Guardian Boon Types
Each Guardian grants 2 boons from the following pool of 12 types:
| Boon Type | Effect |
|---|---|
maxMana |
Flat max raw mana bonus |
manaRegen |
Flat mana regen per hour bonus |
castingSpeed |
Spell cast speed multiplier |
elementalDamage |
Elemental damage multiplier |
rawDamage |
Raw damage multiplier |
critChance |
Critical hit chance bonus |
critDamage |
Critical hit damage multiplier |
spellEfficiency |
Spell efficiency bonus |
manaGain |
Mana gain multiplier |
insightGain |
Insight gain multiplier |
studySpeed |
Study speed multiplier |
prestigeInsight |
Prestige insight bonus |
3.1 Boon Application
for (const floor of signedPacts) {
const guardian = getGuardianForFloor(floor);
for (const boon of guardian.boons) {
let value = boon.value × guardianBoonMultiplier;
// Apply to corresponding bonus stat
}
}
The guardianBoonMultiplier starts at 1.0 and is increased by the Guardian's Boon
discipline and its perks (see §6).
4. Pact Slot System
4.1 Starting Value
pactSlots: 1 // in prestigeStore initial state
4.2 Upgrading
The pactBinding prestige upgrade adds +1 slot per level:
pactSlots: id === 'pactBinding' ? state.pactSlots + 1 : state.pactSlots
Note: The
pactBindingupgrade is referenced in the store logic but is not defined inPRESTIGE_DEFconstants. This is a known gap — the upgrade exists in code but has no definition, cost, or max level.
4.3 Slot Enforcement
A new pact ritual cannot be started if signedPacts.length >= pactSlots. The player
must choose which pacts to maintain.
5. Pact Persistence Through Prestige
5.1 What Persists
| Field | Persisted | Reset on New Loop |
|---|---|---|
signedPacts |
Yes (via Zustand persist) | Yes (reset to []) |
signedPactDetails |
Yes | No |
pactSlots |
Yes | No |
pactRitualFloor |
Yes | Yes (reset to null) |
pactRitualProgress |
Yes | Yes (reset to 0) |
defeatedGuardians |
No | Yes (reset to []) |
5.2 Current Behavior
In the current implementation, signedPacts is reset to [] on startNewLoop,
meaning pacts do NOT persist through prestige loops. The player must re-defeat
Guardians and re-sign pacts each loop. The signedPactDetails record persists
for historical tracking but does not confer active boons.
Design intent vs. implementation: The AGENTS.md states "Signed pacts persist through prestige (bounded by
pactSlots)." The current code resets them. This is a known discrepancy.
6. Invoker Discipline Scaling of Pact Power
6.1 Pact Affinity (Ritual Time Reduction)
From the Pact Attunement discipline:
pactAffinity = min(0.9, pactAffinityUpgrade × 0.1 + pactAffinityBonus)
requiredTime = guardian.pactTime × (1 - pactAffinity)
| pactAffinity | Time Reduction |
|---|---|
| 0.0 | 0% (full time) |
| 0.3 | 30% faster |
| 0.5 | 50% faster |
| 0.9 | 90% faster (cap) |
The pactAffinityBonus starts at +0.05 (base from discipline) and gains +0.05
every 100 XP from the pact-affinity-infinite perk (threshold 200).
6.2 Guardian Boon Multiplier (Boon Power)
From the Guardian's Boon discipline and cross-perks:
| Source | guardianBoonMultiplier Bonus |
|---|---|
| Guardian's Boon discipline (base) | +0.10 |
boon-1 perk (once @ 100 XP) |
+0.10 |
boon-2 perk (capped, 5 tiers) |
up to +0.25 |
pact-power-boost perk (capped, 5 tiers) |
up to +0.15 |
| Maximum total | +0.60 (multiplier = 1.60) |
6.3 Pact Multiplier (Damage and Insight)
From pact-utils.ts:
computePactMultiplier(signedPacts, pactInterferenceMitigation):
baseMult = Π guardian.damageMultiplier for each signed pact
if only 1 pact: return baseMult
numAdditional = signedPacts.length - 1
basePenalty = 0.5 × numAdditional
mitigationReduction = min(pactInterferenceMitigation, 5) × 0.1
effectivePenalty = max(0, basePenalty - mitigationReduction)
if pactInterferenceMitigation >= 5:
synergyBonus = (pactInterferenceMitigation - 5) × 0.1
return baseMult × (1 + synergyBonus)
return baseMult × (1 - effectivePenalty)
Example (2 pacts, floors 10+20):
- Floor 10 damage multiplier:
1.0 + 10 × 0.01 = 1.10 - Floor 20 damage multiplier:
1.0 + 20 × 0.01 = 1.20 baseMult = 1.10 × 1.20 = 1.32- With 0 mitigation:
1.32 × (1 - 0.5) = 0.66 - With 3 mitigation:
1.32 × (1 - 0.2) = 1.056 - With 5 mitigation:
1.32 × 1 = 1.32 - With 7 mitigation:
1.32 × 1.2 = 1.584
The same formula applies to computePactInsightMultiplier using
guardian.insightMultiplier (1.0 + floor × 0.005).
7. Invoker's Mana Gain from Pacts
7.1 Elemental Unlocks
The Invoker gains elemental mana types exclusively through pact signing. Each
guardian's unlocksMana is derived from resolveMultiUnlockChain(element):
| Guardian Floor | Element | Mana Types Unlocked |
|---|---|---|
| 10 | fire | fire |
| 20 | water | water |
| 30 | air | air |
| 40 | earth | earth |
| 50 | light | light |
| 60 | dark | dark |
| 70 | death | death |
| 80 | transference | transference |
| 90 | metal | fire, earth, metal |
| 100 | sand | earth, water, sand |
| 110 | lightning | fire, air, lightning |
| 120 | frost | air, water, frost |
| 130 | blackflame | fire, earth, metal |
| 140 | radiantflames | light, fire |
| 150 | miasma | air, death |
| 160 | shadowglass | earth, dark |
| 170+ | exotic | varies (see guardian-data.ts) |
7.2 No Automatic Conversion
The Invoker has conversionRate = 0. It does not automatically convert raw
mana to any elemental type. All elemental mana must come from:
- Pact unlocks (elemental types become available)
- Elemental regen disciplines (once the element type is unlocked)
- Equipment with mana regen enchantments
8. Guardian Data Summary
8.1 Tier 1 — Base Elements (Floors 10–80)
| Floor | Name | Element | Armor | Pact Cost | Pact Time | Boons |
|---|---|---|---|---|---|---|
| 10 | Ignis Prime | fire | 10% | hp×0.3+power×5+... | 3h | +5% Fire dmg, +50 max mana |
| 20 | Aqua Regia | water | 15% | same formula | 4h | +5% Water dmg, +0.5 mana regen |
| 30 | Ventus Rex | air | 18% | same formula | 5h | +5% Air dmg, +5% casting speed |
| 40 | Terra Firma | earth | 25% | same formula | 6h | +5% Earth dmg, +100 max mana |
| 50 | Lux Aeterna | light | 20% | same formula | 7h | +10% Light dmg, +10% insight gain |
| 60 | Umbra Mortis | dark | 22% | same formula | 8h | +10% Dark dmg, +15% crit damage |
| 70 | Mors Ultima | death | 25% | same formula | 9h | +10% Death dmg, +10% raw damage |
| 80 | Vinculum Arcana | transference | 20% | same formula | 10h | +150 max mana, +1.0 mana regen |
8.2 Tier 2 — Composite Elements (Floors 90–160)
| Floor | Element | Armor | Pact Time |
|---|---|---|---|
| 90 | metal | 30% | 11h |
| 100 | sand | 25% | 12h |
| 110 | lightning | 22% | 13h |
| 120 | frost | 28% | 14h |
| 130 | blackflame | 32% | 15h |
| 140 | radiantflames | 25% | 16h |
| 150 | miasma | 28% | 17h |
| 160 | shadowglass | 33% | 18h |
8.3 Tier 3 — Exotic Elements (Floors 170–240)
| Floor | Element | Armor | Pact Time |
|---|---|---|---|
| 170 | crystal | 35% | 19h |
| 180 | stellar | 30% | 20h |
| 190 | void | 35% | 21h |
| 200 | soul+stellar+void | 35% | 22h |
| 210 | soul+time+plasma | 32% | 23h |
| 220 | plasma | 28% | 24h |
| 230 | crystal+stellar+void | 40% | 25h |
| 240 | soul+time+plasma | 42% | 26h |
8.4 Tier 4+ — Procedural (Floors 250+)
Every 10 floors, with scaling armor, pact multiplier, damage multiplier, and insight multiplier. Dual-element combinations cycle through 9 pairings, then scale through 8 tiers of increasing complexity.
9. Acceptance Criteria
| # | Criterion |
|---|---|
| AC-1 | Pact ritual can only be started for defeated Guardians with an available pact slot and sufficient raw mana. |
| AC-2 | Ritual progress accumulates at HOURS_PER_TICK per tick; pact affinity reduces required time. |
| AC-3 | On completion, the floor is added to signedPacts, removed from defeatedGuardians, and mana types are unlocked. |
| AC-4 | Pact affinity is capped at 0.9 (90% time reduction). |
| AC-5 | Guardian boon multiplier from disciplines correctly increases boon values. |
| AC-6 | Pact multiplier formula applies interference penalties for multiple pacts, with mitigation reducing the penalty. |
| AC-7 | At 5+ mitigation, synergy bonus applies instead of penalty. |
| AC-8 | Starting pact slots = 1; each pactBinding upgrade adds +1 slot. |
| AC-9 | Invoker gains elemental mana types exclusively through pact signing. |
| AC-10 | Cancelling a ritual resets progress but does not refund the raw mana cost. |
| AC-11 | Both Invoker disciplines require at least one signed pact (requires: ['signed_pact']). |
10. Files Reference
| File | Role |
|---|---|
src/lib/game/stores/prestigeStore.ts |
Pact ritual state, slot management, start/complete/cancel |
src/lib/game/stores/pipelines/pact-ritual.ts |
Per-tick ritual processing |
src/lib/game/utils/pact-utils.ts |
Pact multiplier, insight multiplier, interference formulas |
src/lib/game/data/guardian-data.ts |
Static guardian definitions (floors 10–240) |
src/lib/game/data/guardian-encounters.ts |
Procedural guardian lookup (250+) |
src/lib/game/data/disciplines/invoker.ts |
Invoker disciplines (2) |
src/lib/game/utils/guardian-utils.ts |
Element unlock chain resolution |
src/components/game/tabs/GuardianPactsTab.tsx |
Pact signing UI |
src/components/game/tabs/guardian-pacts-components.tsx |
Pact UI sub-components |