feat: implement DoT/debuff runtime system (spec §6, AC-12, AC-13)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
- 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)
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
# 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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
pactSlots: 1 // in prestigeStore initial state
|
||||
```
|
||||
|
||||
### 4.2 Upgrading
|
||||
|
||||
The `pactBinding` prestige upgrade adds +1 slot per level:
|
||||
```typescript
|
||||
pactSlots: id === 'pactBinding' ? state.pactSlots + 1 : state.pactSlots
|
||||
```
|
||||
|
||||
> **Note:** The `pactBinding` upgrade is referenced in the store logic but is **not
|
||||
> defined** in `PRESTIGE_DEF` constants. 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`:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
1. Pact unlocks (elemental types become available)
|
||||
2. Elemental regen disciplines (once the element type is unlocked)
|
||||
3. 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 |
|
||||
Reference in New Issue
Block a user