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)
334 lines
12 KiB
Markdown
334 lines
12 KiB
Markdown
# 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 |
|
||
| 2–3 | 1 |
|
||
| 4–5 | 2 |
|
||
| 6–7 | 3 |
|
||
| 8–9 | 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; // 1–4 (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 |
|