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

- 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:
2026-06-03 18:38:01 +02:00
parent a2cdf6d21c
commit b506f0bcc3
30 changed files with 3272 additions and 71 deletions
@@ -0,0 +1,333 @@
# Golemancy System — Design Spec
> Describes the Fabricator attunement's combat system: golem types, loadout
> configuration, summoning lifecycle, maintenance costs, room duration, combat
> behavior, and discipline interactions.
>
> **⚠ Spec-defined, implementation pending.** This spec is based on
> `docs/specs/spire-combat-spec.md` §9 and represents the intended design.
> The current code has golem data defined but disconnected from the combat pipeline.
---
## 1. Objective
Golemancy is the Fabricator attunement's combat contribution. The player configures
a golem loadout outside the spire, then golems are automatically summoned at each
room entry, fight alongside the player, and disappear after a fixed number of rooms
or if their maintenance cost cannot be met.
**Design goals:**
- Golems provide parallel combat damage independent of the player's spells
- Different golem types offer tactical variety (single-target, AoE, fast, tanky)
- Maintenance cost and room duration create resource management decisions
- Hybrid golems require dual-attunement investment (Enchanter 5 + Fabricator 5)
- Golem loadout configuration outside spire allows strategic planning
---
## 2. Golem Slot Formula
Golem slots come from **two sources** that add together:
### 2.1 From Attunement Level
```
attunementSlots = floor(fabricatorLevel / 2)
```
| Fabricator Level | Slots |
|---|---|
| 1 | 0 |
| 23 | 1 |
| 45 | 2 |
| 67 | 3 |
| 89 | 4 |
| 10 | 5 |
### 2.2 From Discipline
The Golem Crafting discipline provides:
- Base `golemCapacity`: +2
- Perk `golem-2` (capped, threshold 500, maxTier 2): +1 per tier = up to +2
**Maximum total golem slots: 5 (attunement) + 2 (discipline) = 7**
> **Note:** The AGENTS.md states `floor(fabricatorLevel / 2)` with max 5 at level 10.
> The discipline-based capacity is additive on top of this.
---
## 3. Golem Loadout Configuration
The player configures a **golem loadout** from the Golemancy tab before entering
the spire. The loadout defines which golems to attempt to summon and in what order.
This configuration persists across rooms but not across spire runs.
The loadout is a prioritized list of golem IDs. On each room entry, the system
iterates the loadout in order, attempting to summon each golem.
---
## 4. All 10 Golem Types
### 4.1 Base Golems (1)
| Field | Earth Golem |
|---|---|
| **ID** | `earthGolem` |
| **Tier** | 1 |
| **Element** | Earth |
| **Damage** | 8 |
| **Attack Speed** | 1.5/hr |
| **HP** (display) | 50 |
| **Armor Pierce** | 15% |
| **AoE** | No |
| **Max Room Duration** | 3 |
| **Summon Cost** | 10 earth |
| **Maintenance Cost** | 0.5 earth/hr |
| **Unlock** | Fabricator level 2 |
### 4.2 Elemental Golems (3)
| Field | Steel Golem | Crystal Golem | Sand Golem |
|---|---|---|---|
| **ID** | `steelGolem` | `crystalGolem` | `sandGolem` |
| **Tier** | 2 | 3 | 2 |
| **Element** | Metal | Crystal | Sand |
| **Damage** | 12 | 18 | 10 |
| **Attack Speed** | 1.2/hr | 1.0/hr | 2.0/hr |
| **HP** (display) | 60 | 40 | 45 |
| **Armor Pierce** | 35% | 25% | 15% |
| **AoE** | No | No | **Yes (2 targets)** |
| **Max Room Duration** | 3 | 4 | 3 |
| **Summon Cost** | 8 metal + 5 earth | 6 crystal + 3 earth | 10 sand + 4 earth |
| **Maintenance Cost** | 0.6 metal + 0.2 earth/hr | 0.4 crystal + 0.2 earth/hr | 0.6 sand + 0.25 earth/hr |
| **Unlock** | Metal mana unlocked | Crystal mana unlocked | Sand mana unlocked |
### 4.3 Hybrid Golems (6) — Require Enchanter 5 + Fabricator 5
| Field | Lava Golem | Galvanic Golem | Obsidian Golem |
|---|---|---|---|
| **ID** | `lavaGolem` | `galvanicGolem` | `obsidianGolem` |
| **Tier** | 3 | 3 | 4 |
| **Elements** | Earth + Fire | Metal + Lightning | Earth + Dark |
| **Damage** | 15 | 10 | 25 |
| **Attack Speed** | 1.0/hr | 3.5/hr | 0.8/hr |
| **HP** (display) | 70 | 45 | 55 |
| **Armor Pierce** | 20% | 45% | 50% |
| **AoE** | **Yes (2 targets)** | No | No |
| **Max Room Duration** | 4 | 4 | 5 |
| **Summon Cost** | 15 earth + 12 fire | 12 metal + 8 lightning | 18 earth + 10 dark |
| **Maintenance Cost** | 0.6 earth + 0.7 fire/hr | 0.4 metal + 0.7 lightning/hr | 0.5 earth + 0.6 dark/hr |
| **Special** | Burn DoT | Lightning Speed | Devastating Strike |
| **Unlock** | Enchanter 5 + Fabricator 5 | Enchanter 5 + Fabricator 5 | Enchanter 5 + Fabricator 5 |
| Field | Prism Golem | Quicksilver Golem | Voidstone Golem |
|---|---|---|---|
| **ID** | `prismGolem` | `quicksilverGolem` | `voidstoneGolem` |
| **Tier** | 4 | 3 | 4 |
| **Elements** | Crystal + Light | Metal + Water | Earth + Void |
| **Damage** | 28 | 14 | **40** |
| **Attack Speed** | 2.0/hr | **4.0/hr** | 0.6/hr |
| **HP** (display) | 60 | 55 | **100** |
| **Armor Pierce** | 45% | 35% | **60%** |
| **AoE** | **Yes (3 targets)** | No | **Yes (3 targets)** |
| **Max Room Duration** | 5 | 4 | 5 |
| **Summon Cost** | 16 crystal + 10 light | 10 metal + 8 water | 22 earth + 14 void |
| **Maintenance Cost** | 0.6 crystal + 0.6 light/hr | 0.4 metal + 0.4 water/hr | 0.5 earth + 0.9 void/hr |
| **Special** | Piercing Beams | Flow (evasion) | Void Infusion |
| **Unlock** | Enchanter 5 + Fabricator 5 | Enchanter 5 + Fabricator 5 | Enchanter 5 + Fabricator 5 |
### 4.4 Summary Table
| Golem | Tier | DMG | SPD | HP | Pierce | AoE | Targets | Rooms | Unlock |
|---|---|---|---|---|---|---|---|---|---|
| Earth | 1 | 8 | 1.5 | 50 | 15% | No | 1 | 3 | Fabricator Lv2 |
| Steel | 2 | 12 | 1.2 | 60 | 35% | No | 1 | 3 | Metal mana |
| Crystal | 3 | 18 | 1.0 | 40 | 25% | No | 1 | 4 | Crystal mana |
| Sand | 2 | 10 | 2.0 | 45 | 15% | Yes | 2 | 3 | Sand mana |
| Lava | 3 | 15 | 1.0 | 70 | 20% | Yes | 2 | 4 | Ench5+Fab5 |
| Galvanic | 3 | 10 | 3.5 | 45 | 45% | No | 1 | 4 | Ench5+Fab5 |
| Obsidian | 4 | 25 | 0.8 | 55 | 50% | No | 1 | 5 | Ench5+Fab5 |
| Prism | 4 | 28 | 2.0 | 60 | 45% | Yes | 3 | 5 | Ench5+Fab5 |
| Quicksilver | 3 | 14 | 4.0 | 55 | 35% | No | 1 | 4 | Ench5+Fab5 |
| Voidstone | 4 | 40 | 0.6 | 100 | 60% | Yes | 3 | 5 | Ench5+Fab5 |
---
## 5. Summoning on Room Entry
When the player enters a new combat room:
```
onRoomEntry():
for each golem in golemLoadout:
if player has enough mana of golem.summonCostType >= golem.summonCost:
deductMana(golem.summonCost, golem.summonCostType)
activeGolems.push({
...golemDef,
roomsRemaining: golemDef.maxRoomDuration,
attackProgress: 0,
})
activityLog("${golem.name} summoned")
else:
activityLog("Not enough mana to summon ${golem.name} — skipped")
```
**Key rules:**
- Golems that cannot be summoned (insufficient mana) are **not re-attempted** within the same room
- Failed golems will be attempted again on the next room entry
- Summoning order follows the loadout priority list
---
## 6. Golem Combat
Each active golem attacks on its own `attackProgress` timer, identical to swords:
```
golemProgress += HOURS_PER_TICK × golem.attackSpeed
while golemProgress >= 1:
dmg = golem.baseDamage
if golem.element:
dmg ×= getElementalBonus(golem.element, enemy.element)
applyGolemEffects(golem, dmg, enemy)
applyDamageToRoom(dmg)
golemProgress -= 1
```
**Key rules:**
- Golems ignore Executioner and Berserker discipline specials
- AoE golems distribute damage across multiple targets
- Elemental matchup applies if the golem has an element
---
## 7. Maintenance Cost
Each tick, each active golem checks its maintenance cost:
```
tickGolemMaintenance(golem):
if player mana[golem.maintenanceCostType] >= golem.maintenanceCost × HOURS_PER_TICK:
deductMana(golem.maintenanceCost × HOURS_PER_TICK, golem.maintenanceCostType)
else:
dismiss(golem)
activityLog("${golem.name} dismissed — insufficient ${golem.maintenanceCostType} mana")
```
**Key rules:**
- A dismissed golem is **not re-summoned mid-room**
- It will be re-attempted on the next room entry if mana has recovered
- Maintenance is checked every tick, not just on room transitions
---
## 8. Room Duration Limit
```
onRoomCleared():
for each activeGolem:
activeGolem.roomsRemaining -= 1
if activeGolem.roomsRemaining <= 0:
dismiss(golem)
activityLog("${golem.name} has faded after ${maxRoomDuration} rooms")
```
**Key rules:**
- Room duration ticks down on room **clear**, not on room **entry**
- Golems persist through the full room they were summoned in
- When `roomsRemaining` reaches 0, the golem is dismissed
---
## 9. Golem Data Shape
```typescript
interface GolemDefinition {
id: string;
name: string;
tier: number; // 14 (determines general power)
baseDamage: number;
attackSpeed: number; // attacks per in-game hour
element?: ElementType; // optional elemental type for matchup
maxRoomDuration: number; // rooms before disappearing
summonCost: number;
summonCostType: ElementType | 'raw';
maintenanceCost: number; // per in-game hour
maintenanceCostType: ElementType | 'raw';
onHitEffect?: GolemHitEffect; // DoT, AoE, etc.
armorPierce?: number; // 0-1, bypasses this fraction of enemy armor
aoe?: boolean;
aoeTargets?: number;
}
```
---
## 10. Discipline Interactions
### 10.1 Golem Crafting Discipline
| Perk | Effect |
|---|---|
| `golem-1` (once @ 200 XP) | Unlocks golem summoning ability |
| `golem-2` (capped @ 500, maxTier 2) | +1 Golem Capacity per tier (max +2) |
### 10.2 Fabricator Level
Directly determines base golem slots: `floor(fabricatorLevel / 2)`.
### 10.3 Dual Attunement Requirement
All 6 hybrid golems require **Enchanter 5 + Fabricator 5**. This means the player
must have both attunements active and leveled to at least 5 to access the most
powerful golem types.
---
## 11. Known Gaps / Implementation Status
| Feature | Status |
|---|---|
| Golem data definitions | ✅ Complete (10 golems in `data/golems/`) |
| Golem loadout UI | ✅ Partial (GolemancyTab exists) |
| Summoning on room entry | ❌ Not wired into combat tick |
| Maintenance cost per tick | ❌ Not wired into combat tick |
| Room duration tracking | ❌ Not wired into room clear |
| Golem combat (attack timer) | ❌ Not wired into combat tick |
| Golemancy combat pipeline | ❌ `golem-combat-actions.ts` exists but disconnected |
---
## 12. Acceptance Criteria
| # | Criterion |
|---|---|
| AC-1 | Golem slots = `floor(fabricatorLevel / 2)` + discipline bonus. |
| AC-2 | Golems are summoned on room entry if mana allows; failed summons are skipped for that room. |
| AC-3 | Each golem attacks on its own timer using its `attackSpeed` stat. |
| AC-4 | Elemental matchup applies to golem attacks when the golem has an element. |
| AC-5 | AoE golems distribute damage across `aoeTargets` enemies. |
| AC-6 | Maintenance cost is deducted each tick; golems dismiss if cost cannot be met. |
| AC-7 | Dismissed golems are not re-summoned mid-room. |
| AC-8 | Room duration ticks down on room clear, not entry. |
| AC-9 | Golems disappear after `maxRoomDuration` rooms. |
| AC-10 | Hybrid golems require Enchanter 5 + Fabricator 5. |
| AC-11 | Golem loadout is configured outside the spire and persists across rooms. |
| AC-12 | Golem HP is display-only; golems don't take damage from enemies. |
---
## 13. Files Reference
| File | Role |
|---|---|
| `src/lib/game/data/golems/golems-data.ts` | All 10 golem definitions |
| `src/lib/game/data/golems/types.ts` | Golem type definitions |
| `src/lib/game/data/disciplines/fabricator.ts` | Golem Crafting discipline |
| `src/lib/game/stores/golem-combat-actions.ts` | Golem combat actions (disconnected) |
| `src/lib/game/stores/pipelines/golem-combat.ts` | Golem combat pipeline (disconnected) |
| `src/components/game/tabs/GolemancyTab.tsx` | Golemancy UI |
| `docs/specs/spire-combat-spec.md` §9 | Authoritative golemancy spec |