b506f0bcc3
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)
357 lines
12 KiB
Markdown
357 lines
12 KiB
Markdown
# 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 |
|