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:
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-02T14:39:46.904Z
|
Generated: 2026-06-03T13:40:52.900Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-06-02T14:39:44.895Z",
|
"generated": "2026-06-03T13:40:50.953Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
@@ -533,22 +533,33 @@
|
|||||||
"data/guardian-encounters.ts",
|
"data/guardian-encounters.ts",
|
||||||
"effects/discipline-effects.ts",
|
"effects/discipline-effects.ts",
|
||||||
"stores/combat-state.types.ts",
|
"stores/combat-state.types.ts",
|
||||||
|
"stores/golem-combat-actions.ts",
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"utils/index.ts"
|
"utils/index.ts"
|
||||||
],
|
],
|
||||||
|
"stores/combat-descent-actions.ts": [
|
||||||
|
"data/guardian-encounters.ts",
|
||||||
|
"stores/combat-state.types.ts",
|
||||||
|
"stores/discipline-slice.ts",
|
||||||
|
"stores/golem-combat-actions.ts",
|
||||||
|
"stores/manaStore.ts",
|
||||||
|
"stores/prestigeStore.ts",
|
||||||
|
"types.ts",
|
||||||
|
"utils/spire-utils.ts"
|
||||||
|
],
|
||||||
"stores/combat-state.types.ts": [
|
"stores/combat-state.types.ts": [
|
||||||
"types.ts"
|
"types.ts"
|
||||||
],
|
],
|
||||||
"stores/combatStore.ts": [
|
"stores/combatStore.ts": [
|
||||||
"data/guardian-encounters.ts",
|
"data/guardian-encounters.ts",
|
||||||
"stores/combat-actions.ts",
|
"stores/combat-actions.ts",
|
||||||
|
"stores/combat-descent-actions.ts",
|
||||||
"stores/combat-state.types.ts",
|
"stores/combat-state.types.ts",
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"utils/activity-log.ts",
|
"utils/activity-log.ts",
|
||||||
"utils/index.ts",
|
"utils/index.ts",
|
||||||
"utils/room-utils.ts",
|
"utils/room-utils.ts",
|
||||||
"utils/safe-persist.ts",
|
"utils/safe-persist.ts"
|
||||||
"utils/spire-utils.ts"
|
|
||||||
],
|
],
|
||||||
"stores/crafting-equipment-tick.ts": [
|
"stores/crafting-equipment-tick.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
@@ -661,15 +672,23 @@
|
|||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/pipelines/combat-tick.ts",
|
"stores/pipelines/combat-tick.ts",
|
||||||
"stores/pipelines/enchanting-tick.ts",
|
"stores/pipelines/enchanting-tick.ts",
|
||||||
|
"stores/pipelines/golem-combat.ts",
|
||||||
"stores/pipelines/pact-ritual.ts",
|
"stores/pipelines/pact-ritual.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
"stores/tick-pipeline.ts",
|
"stores/tick-pipeline.ts",
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
|
"types.ts",
|
||||||
"utils/element-cap-bonus.ts",
|
"utils/element-cap-bonus.ts",
|
||||||
"utils/index.ts",
|
"utils/index.ts",
|
||||||
"utils/safe-persist.ts"
|
"utils/safe-persist.ts"
|
||||||
],
|
],
|
||||||
"stores/gameStore.types.ts": [],
|
"stores/gameStore.types.ts": [],
|
||||||
|
"stores/golem-combat-actions.ts": [
|
||||||
|
"constants.ts",
|
||||||
|
"data/golems/index.ts",
|
||||||
|
"types.ts",
|
||||||
|
"utils/index.ts"
|
||||||
|
],
|
||||||
"stores/index.ts": [
|
"stores/index.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
"stores/attunementStore.ts",
|
"stores/attunementStore.ts",
|
||||||
@@ -696,7 +715,9 @@
|
|||||||
"constants.ts",
|
"constants.ts",
|
||||||
"data/guardian-encounters.ts",
|
"data/guardian-encounters.ts",
|
||||||
"effects/special-effects.ts",
|
"effects/special-effects.ts",
|
||||||
"effects/upgrade-effects.types.ts"
|
"effects/upgrade-effects.types.ts",
|
||||||
|
"stores/golem-combat-actions.ts",
|
||||||
|
"types.ts"
|
||||||
],
|
],
|
||||||
"stores/pipelines/enchanting-tick.ts": [
|
"stores/pipelines/enchanting-tick.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
@@ -715,6 +736,12 @@
|
|||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/uiStore.ts"
|
"stores/uiStore.ts"
|
||||||
],
|
],
|
||||||
|
"stores/pipelines/golem-combat.ts": [
|
||||||
|
"stores/combatStore.ts",
|
||||||
|
"stores/golem-combat-actions.ts",
|
||||||
|
"stores/manaStore.ts",
|
||||||
|
"types.ts"
|
||||||
|
],
|
||||||
"stores/pipelines/pact-ritual.ts": [
|
"stores/pipelines/pact-ritual.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
"data/guardian-encounters.ts",
|
"data/guardian-encounters.ts",
|
||||||
|
|||||||
@@ -12,6 +12,21 @@ Mana-Loop/
|
|||||||
│ └── pre-commit
|
│ └── pre-commit
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── specs/
|
│ ├── specs/
|
||||||
|
│ │ ├── attunements/
|
||||||
|
│ │ │ ├── enchanter/
|
||||||
|
│ │ │ │ ├── systems/
|
||||||
|
│ │ │ │ │ └── enchanting-spec.md
|
||||||
|
│ │ │ │ └── enchanter-spec.md
|
||||||
|
│ │ │ ├── fabricator/
|
||||||
|
│ │ │ │ ├── systems/
|
||||||
|
│ │ │ │ │ ├── golemancy-spec.md
|
||||||
|
│ │ │ │ │ └── item-fabrication-spec.md
|
||||||
|
│ │ │ │ └── fabricator-spec.md
|
||||||
|
│ │ │ ├── invoker/
|
||||||
|
│ │ │ │ ├── systems/
|
||||||
|
│ │ │ │ │ └── pact-system-spec.md
|
||||||
|
│ │ │ │ └── invoker-spec.md
|
||||||
|
│ │ │ └── attunement-system-spec.md
|
||||||
│ │ ├── spire-climbing-spec.md
|
│ │ ├── spire-climbing-spec.md
|
||||||
│ │ └── spire-combat-spec.md
|
│ │ └── spire-combat-spec.md
|
||||||
│ ├── GAME_BRIEFING.md
|
│ ├── GAME_BRIEFING.md
|
||||||
@@ -347,6 +362,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── craftingStore.types.ts
|
│ │ │ │ ├── craftingStore.types.ts
|
||||||
│ │ │ │ ├── debugBridge.ts
|
│ │ │ │ ├── debugBridge.ts
|
||||||
│ │ │ │ ├── discipline-slice.ts
|
│ │ │ │ ├── discipline-slice.ts
|
||||||
|
│ │ │ │ ├── dot-runtime.ts
|
||||||
│ │ │ │ ├── gameActions.ts
|
│ │ │ │ ├── gameActions.ts
|
||||||
│ │ │ │ ├── gameHooks.ts
|
│ │ │ │ ├── gameHooks.ts
|
||||||
│ │ │ │ ├── gameLoopActions.ts
|
│ │ │ │ ├── gameLoopActions.ts
|
||||||
|
|||||||
@@ -0,0 +1,348 @@
|
|||||||
|
# Attunement System — Design Spec
|
||||||
|
|
||||||
|
> Describes the three-attunement class system: Enchanter, Invoker, and Fabricator.
|
||||||
|
> Covers slot assignments, unlock conditions, leveling, regen/conversion scaling,
|
||||||
|
> discipline pool gating, and interaction with mana conversion and the incursion system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objective
|
||||||
|
|
||||||
|
Attunements are class-like specializations that gate access to discipline pools and
|
||||||
|
unique capabilities. A player can have multiple attunements active simultaneously,
|
||||||
|
each contributing raw mana regen and (for Enchanter and Fabricator) automatic mana
|
||||||
|
conversion. Attunements level up independently through attunement-specific XP sources,
|
||||||
|
scaling their regen and conversion rates exponentially.
|
||||||
|
|
||||||
|
**Design goals:**
|
||||||
|
- Three distinct attunements with unique identities and roles
|
||||||
|
- Attunements unlock over time, expanding the player's options
|
||||||
|
- Leveling provides meaningful exponential scaling without being mandatory
|
||||||
|
- Discipline pool access is gated behind attunement unlock status
|
||||||
|
- Invoker's lack of primary mana creates a distinct pact-dependent playstyle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The Three Attunements
|
||||||
|
|
||||||
|
### 2.1 Enchanter (Right Hand) — Starting Attunement
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **ID** | `enchanter` |
|
||||||
|
| **Slot** | `rightHand` |
|
||||||
|
| **Icon** | `✨` |
|
||||||
|
| **Color** | `#1ABC9C` (Teal) |
|
||||||
|
| **Primary Mana** | `transference` |
|
||||||
|
| **Raw Mana Regen** | +0.5/hour (base) |
|
||||||
|
| **Conversion Rate** | 0.2 raw→transference/hour (base) |
|
||||||
|
| **Unlock** | Starting (unlocked by default) |
|
||||||
|
| **Capabilities** | `['enchanting']` |
|
||||||
|
| **Skill Categories** | `['enchant', 'effectResearch']` |
|
||||||
|
|
||||||
|
**Disciplines:** 10 disciplines across 4 files (core: 4, utility: 2, spells: 3, special: 1)
|
||||||
|
|
||||||
|
### 2.2 Invoker (Chest) — Locked
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **ID** | `invoker` |
|
||||||
|
| **Slot** | `chest` |
|
||||||
|
| **Icon** | `💜` |
|
||||||
|
| **Color** | `#9B59B6` (Purple) |
|
||||||
|
| **Primary Mana** | None (gains elemental mana from pacts) |
|
||||||
|
| **Raw Mana Regen** | +0.3/hour (base) |
|
||||||
|
| **Conversion Rate** | None (0 at all levels) |
|
||||||
|
| **Unlock** | Defeat first Guardian |
|
||||||
|
| **Capabilities** | `['pacts', 'guardianPowers', 'elementalMastery']` |
|
||||||
|
| **Skill Categories** | `['invocation', 'pact']` |
|
||||||
|
|
||||||
|
**Disciplines:** 2 disciplines
|
||||||
|
|
||||||
|
### 2.3 Fabricator (Left Hand) — Locked
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **ID** | `fabricator` |
|
||||||
|
| **Slot** | `leftHand` |
|
||||||
|
| **Icon** | `⚒️` |
|
||||||
|
| **Color** | `#F4A261` (Earth) |
|
||||||
|
| **Primary Mana** | `earth` |
|
||||||
|
| **Raw Mana Regen** | +0.4/hour (base) |
|
||||||
|
| **Conversion Rate** | 0.25 raw→earth/hour (base) |
|
||||||
|
| **Unlock** | Prove crafting worth |
|
||||||
|
| **Capabilities** | `['golemCrafting', 'gearCrafting', 'earthShaping']` |
|
||||||
|
| **Skill Categories** | `['fabrication', 'golemancy']` |
|
||||||
|
|
||||||
|
**Disciplines:** 5 disciplines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Unlock Conditions
|
||||||
|
|
||||||
|
| Attunement | Condition | Implementation |
|
||||||
|
|---|---|---|
|
||||||
|
| **Enchanter** | Starting | Present in initial state: `{ active: true, level: 1, experience: 0 }` |
|
||||||
|
| **Invoker** | Defeat first Guardian | Descriptive: `"Defeat your first guardian and choose the path of the Invoker"` |
|
||||||
|
| **Fabricator** | Prove crafting worth | Descriptive: `"Prove your worth as a crafter"` |
|
||||||
|
|
||||||
|
Unlocking is performed via `debugUnlockAttunement(attunementId)` in the store, which
|
||||||
|
initializes the attunement at `{ active: true, level: 1, experience: 0 }`. The
|
||||||
|
conditions are currently descriptive strings rather than hard-coded mechanical checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Attunement Leveling
|
||||||
|
|
||||||
|
### 4.1 XP Thresholds
|
||||||
|
|
||||||
|
```
|
||||||
|
Level 1: 0 XP (starting)
|
||||||
|
Level 2: 1,000 XP
|
||||||
|
Level ≥ 3: Math.floor(1000 * Math.pow(2, level - 2) * 1.25)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Level | XP Threshold | Cumulative XP |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 0 | 0 |
|
||||||
|
| 2 | 1,000 | 1,000 |
|
||||||
|
| 3 | 2,500 | 3,500 |
|
||||||
|
| 4 | 5,000 | 8,500 |
|
||||||
|
| 5 | 10,000 | 18,500 |
|
||||||
|
| 6 | 20,000 | 38,500 |
|
||||||
|
| 7 | 40,000 | 78,500 |
|
||||||
|
| 8 | 80,000 | 158,500 |
|
||||||
|
| 9 | 160,000 | 318,500 |
|
||||||
|
| 10 | 320,000 | 638,500 |
|
||||||
|
|
||||||
|
**Max Level:** `MAX_ATTUNEMENT_LEVEL = 10`
|
||||||
|
|
||||||
|
### 4.2 Level-Up Mechanism
|
||||||
|
|
||||||
|
```
|
||||||
|
addAttunementXP(attunementId, amount):
|
||||||
|
state.experience += amount
|
||||||
|
while state.experience >= xpForNextLevel && level < MAX:
|
||||||
|
state.experience -= xpForNextLevel
|
||||||
|
level += 1
|
||||||
|
log("Attunement leveled up!")
|
||||||
|
```
|
||||||
|
|
||||||
|
XP does **not** roll over beyond the threshold check — the threshold amount is
|
||||||
|
subtracted and any remainder carries into the next level.
|
||||||
|
|
||||||
|
### 4.3 Regen and Conversion Rate Scaling
|
||||||
|
|
||||||
|
Both raw mana regen and conversion rate use the same exponential formula:
|
||||||
|
|
||||||
|
```
|
||||||
|
scaledValue = baseValue × 1.5^(level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Effective raw mana regen by level (per attunement):**
|
||||||
|
|
||||||
|
| Level | Enchanter (0.5) | Invoker (0.3) | Fabricator (0.4) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | 0.500/hr | 0.300/hr | 0.400/hr |
|
||||||
|
| 2 | 0.750/hr | 0.450/hr | 0.600/hr |
|
||||||
|
| 3 | 1.125/hr | 0.675/hr | 0.900/hr |
|
||||||
|
| 4 | 1.688/hr | 1.013/hr | 1.350/hr |
|
||||||
|
| 5 | 2.531/hr | 1.519/hr | 2.025/hr |
|
||||||
|
| 6 | 3.797/hr | 2.278/hr | 3.038/hr |
|
||||||
|
| 7 | 5.695/hr | 3.417/hr | 4.556/hr |
|
||||||
|
| 8 | 8.543/hr | 5.126/hr | 6.834/hr |
|
||||||
|
| 9 | 12.814/hr | 7.689/hr | 10.252/hr |
|
||||||
|
| 10 | 19.221/hr | 11.533/hr | 15.377/hr |
|
||||||
|
|
||||||
|
**Effective conversion rate by level:**
|
||||||
|
|
||||||
|
| Level | Enchanter (0.2) | Fabricator (0.25) |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 0.200/hr | 0.250/hr |
|
||||||
|
| 2 | 0.300/hr | 0.375/hr |
|
||||||
|
| 3 | 0.450/hr | 0.563/hr |
|
||||||
|
| 4 | 0.675/hr | 0.844/hr |
|
||||||
|
| 5 | 1.013/hr | 1.266/hr |
|
||||||
|
| 6 | 1.519/hr | 1.898/hr |
|
||||||
|
| 7 | 2.278/hr | 2.848/hr |
|
||||||
|
| 8 | 3.417/hr | 4.271/hr |
|
||||||
|
| 9 | 5.126/hr | 6.407/hr |
|
||||||
|
| 10 | 7.689/hr | 9.610/hr |
|
||||||
|
|
||||||
|
Invoker has `conversionRate = 0` at all levels — no auto-conversion.
|
||||||
|
|
||||||
|
**Total regen** = sum of `baseRegen × 1.5^(level-1)` across all active attunements.
|
||||||
|
**Total conversion drain** = sum of `baseConversionRate × 1.5^(level-1)` across active attunements
|
||||||
|
that have a non-zero conversion rate. This drain is applied to the raw mana pool.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Attunement XP Gain Sources
|
||||||
|
|
||||||
|
### 5.1 Enchanting → Enchanter XP
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
calculateEnchantingXP(capacityUsed: number): number {
|
||||||
|
return Math.max(1, Math.floor(capacityUsed / 10));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 1 Enchanter XP per 10 capacity used (floored), minimum 1 XP per enchant.
|
||||||
|
|
||||||
|
### 5.2 Other Sources
|
||||||
|
|
||||||
|
The `addAttunementXP(attunementId, amount)` store action is the generic mechanism.
|
||||||
|
Any system can call it to award XP to any attunement. In the codebase as-is,
|
||||||
|
only enchanting has an explicit calculation function. Invoker and Fabricator XP
|
||||||
|
gain is expected to be called from their respective systems (pact signing and
|
||||||
|
item fabrication) but explicit calculation functions are not yet defined.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Discipline Pool Gating
|
||||||
|
|
||||||
|
### 6.1 Skill Categories
|
||||||
|
|
||||||
|
Attunements gate discipline access through **skill categories**:
|
||||||
|
|
||||||
|
| Category | Disciplines |
|
||||||
|
|---|---|
|
||||||
|
| Always available | `mana`, `study`, `research` |
|
||||||
|
| Enchanter | `enchant`, `effectResearch` |
|
||||||
|
| Invoker | `invocation`, `pact` |
|
||||||
|
| Fabricator | `fabrication`, `golemancy` |
|
||||||
|
|
||||||
|
The function `getAvailableSkillCategories()` iterates all **active** attunements,
|
||||||
|
collects their `skillCategories` into a Set, and returns the deduplicated array.
|
||||||
|
|
||||||
|
### 6.2 Discipline Pool Counts per Attunement
|
||||||
|
|
||||||
|
| Attunement | File | Count |
|
||||||
|
|---|---|---|
|
||||||
|
| Enchanter Core | `enchanter.ts` | 4 |
|
||||||
|
| Enchanter Utility | `enchanter-utility.ts` | 2 |
|
||||||
|
| Enchanter Spells | `enchanter-spells.ts` | 3 |
|
||||||
|
| Enchanter Special | `enchanter-special.ts` | 1 |
|
||||||
|
| Invoker | `invoker.ts` | 2 |
|
||||||
|
| Fabricator | `fabricator.ts` | 5 |
|
||||||
|
| **Attunement-gated total** | | **17** |
|
||||||
|
|
||||||
|
The remaining 47 disciplines are available regardless of attunement status (base,
|
||||||
|
elemental, elemental-regen, elemental-regen-advanced pools).
|
||||||
|
|
||||||
|
### 6.3 Capability Gating
|
||||||
|
|
||||||
|
Each attunement grants `capabilities` that unlock specific game systems:
|
||||||
|
|
||||||
|
| Capability | System |
|
||||||
|
|---|---|
|
||||||
|
| `enchanting` | Enchantment Design/Prepare/Apply pipeline |
|
||||||
|
| `pacts` | Guardian pact signing and boon system |
|
||||||
|
| `guardianPowers` | Guardian power access |
|
||||||
|
| `elementalMastery` | Element mastery bonuses |
|
||||||
|
| `golemCrafting` | Golem summoning (Golemancy) |
|
||||||
|
| `gearCrafting` | Gear fabrication recipes |
|
||||||
|
| `earthShaping` | Earth mana shaping |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Mana Conversion Interaction
|
||||||
|
|
||||||
|
### 7.1 Conversion Flow
|
||||||
|
|
||||||
|
Each tick, the mana system:
|
||||||
|
|
||||||
|
1. Computes total raw regen (base + attunement regen + discipline bonus + equipment) × temporalEcho × meditationMultiplier
|
||||||
|
2. Subtracts incursion reduction: `× (1 - incursionStrength)`
|
||||||
|
3. Computes total conversion drain: sum of all active attunement conversion rates
|
||||||
|
4. Applies: `rawMana += totalRegen - totalConversionDrain` (per tick)
|
||||||
|
5. For each attunement with conversion: adds `conversionRate × HOURS_PER_TICK` to the target element
|
||||||
|
|
||||||
|
### 7.2 Invoker's Unique Position
|
||||||
|
|
||||||
|
The Invoker has **no automatic conversion** — `conversionRate = 0`. Instead, it gains
|
||||||
|
elemental mana types exclusively by signing Guardian pacts. Each guardian's
|
||||||
|
`unlocksMana` array is resolved through `resolveMultiUnlockChain(element)`, which
|
||||||
|
unlocks the guardian's element and all base components.
|
||||||
|
|
||||||
|
Example: Signing a Metal guardian (floor 90) unlocks `fire`, `earth`, and `metal`.
|
||||||
|
|
||||||
|
### 7.3 Conversion and Incursion
|
||||||
|
|
||||||
|
Incursion reduces net raw mana regeneration:
|
||||||
|
```
|
||||||
|
effectiveRegen = max(0, baseRegen × (1 - incursionStrength) × meditationMult - totalConversionPerTick)
|
||||||
|
```
|
||||||
|
|
||||||
|
As incursion strength approaches 95% (day 30), conversion drains can exceed regen,
|
||||||
|
causing raw mana to decrease. Since conversion is contingent on available raw mana,
|
||||||
|
attunement conversion effectively stalls during peak incursion if the raw pool is
|
||||||
|
insufficient.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Puzzle Room Interaction
|
||||||
|
|
||||||
|
From `spire-climbing-spec.md` §4.3, puzzle rooms appear on every 7th floor and have
|
||||||
|
per-attunement variants:
|
||||||
|
|
||||||
|
| Room Type | Description |
|
||||||
|
|---|---|
|
||||||
|
| `enchanter_trial` | Enchanter-themed puzzle challenge |
|
||||||
|
| `fabricator_trial` | Fabricator-themed puzzle challenge |
|
||||||
|
| `invoker_trial` | Invoker-themed puzzle challenge |
|
||||||
|
| `hybrid_enchanter_fabricator` | Dual attunement challenge |
|
||||||
|
| `hybrid_enchanter_invoker` | Dual attunement challenge |
|
||||||
|
| `hybrid_fabricator_invoker` | Dual attunement challenge |
|
||||||
|
|
||||||
|
Progress scales at 1.5–2% per tick base, with attunement bonus of 2.5–3% per
|
||||||
|
relevant attunement level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. State Fields
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AttunementState {
|
||||||
|
id: string;
|
||||||
|
active: boolean;
|
||||||
|
level: number; // 1–10
|
||||||
|
experience: number; // current XP toward next level
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state (prestige):
|
||||||
|
attunements: {
|
||||||
|
enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Acceptance Criteria
|
||||||
|
|
||||||
|
| # | Criterion |
|
||||||
|
|---|---|
|
||||||
|
| AC-1 | Enchanter is the only active attunement at game start (level 1, 0 XP). |
|
||||||
|
| AC-2 | Invoker and Fabricator are locked until unlocked; their unlock conditions are displayed in the Attunements tab. |
|
||||||
|
| AC-3 | Attunement XP accumulates and triggers level-ups at the correct thresholds; each level requires the exact XP specified in the formula. |
|
||||||
|
| AC-4 | Regen and conversion rates scale by `1.5^(level-1)` — a level 10 Enchanter converts at 7.69 raw→transference/hour. |
|
||||||
|
| AC-5 | Both raw regen and conversion from all active attunements are summed and applied each tick. |
|
||||||
|
| AC-6 | Invoker has no automatic mana conversion at any level. |
|
||||||
|
| AC-7 | Enchanting awards Enchanter XP at 1 per 10 capacity used (minimum 1). |
|
||||||
|
| AC-8 | Attunement skill categories correctly gate discipline pool access — Enchanter disciplines require Enchanter to be active. |
|
||||||
|
| AC-9 | Attunement tab shows unlocked/locked visual distinction, XP progress bar, level badge, and all attunement capabilities. |
|
||||||
|
| AC-10 | Puzzle rooms on every 7th floor use per-attunement room types with the correct progress scaling. |
|
||||||
|
| AC-11 | Incursion correctly reduces net raw mana regeneration, potentially stalling conversion at peak incursion. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Files Reference
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `src/lib/game/data/attunements.ts` | Attunement definitions (the 3 attunements) |
|
||||||
|
| `src/lib/game/stores/attunementStore.ts` | Attunement state, leveling, XP, unlock |
|
||||||
|
| `src/lib/game/types/attunements.ts` | Attunement type definitions |
|
||||||
|
| `src/components/game/tabs/AttunementsTab.tsx` | Attunement UI display |
|
||||||
|
| `src/lib/game/stores/manaStore.ts` | Mana regen, conversion, incursion effects |
|
||||||
|
| `docs/specs/spire-climbing-spec.md` | Puzzle room types per attunement |
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
# Enchanter Attunement — Design Spec
|
||||||
|
|
||||||
|
> Describes the Enchanter attunement: identity, unlock flow, mana behavior, full
|
||||||
|
> discipline list with stats/perks, systems unlocked, and attunement level interactions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objective
|
||||||
|
|
||||||
|
The Enchanter is the starting attunement and the gateway to the enchanting system.
|
||||||
|
It provides access to Transference-based disciplines that unlock enchantment
|
||||||
|
effects, boost enchantment power, and provide study/utility bonuses. The Enchanter
|
||||||
|
is always the first attunement a player uses, and it remains relevant throughout
|
||||||
|
all stages of the game through its 10 disciplines and the deep enchanting pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Identity
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **ID** | `enchanter` |
|
||||||
|
| **Slot** | `rightHand` |
|
||||||
|
| **Icon** | `✨` |
|
||||||
|
| **Color** | `#1ABC9C` (Teal) |
|
||||||
|
| **Primary Mana** | `transference` |
|
||||||
|
| **Raw Mana Regen** | +0.5/hour (base, scales with `1.5^(level-1)`) |
|
||||||
|
| **Conversion Rate** | 0.2 raw→transference/hour (base, scales with `1.5^(level-1)`) |
|
||||||
|
| **Unlock** | Starting attunement (unlocked by default) |
|
||||||
|
| **Capabilities** | `['enchanting']` |
|
||||||
|
| **Skill Categories** | `['enchant', 'effectResearch']` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Unlock Condition and Flow
|
||||||
|
|
||||||
|
The Enchanter is **always unlocked** — it is present in the initial game state:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
attunements: {
|
||||||
|
enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
No unlock flow is required. The player begins the game with Enchanter active.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Raw Mana Regen Contribution
|
||||||
|
|
||||||
|
Base regen: **+0.5/hour** (at level 1). Scales exponentially:
|
||||||
|
|
||||||
|
```
|
||||||
|
effectiveRegen = 0.5 × 1.5^(level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Level | Raw Regen |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 0.500/hr |
|
||||||
|
| 5 | 2.531/hr |
|
||||||
|
| 10 | 19.221/hr |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Mana Conversion Behavior
|
||||||
|
|
||||||
|
The Enchanter is the **only attunement that converts raw mana to Transference**:
|
||||||
|
|
||||||
|
```
|
||||||
|
effectiveConversionRate = 0.2 × 1.5^(level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an automatic per-hour conversion. Each tick:
|
||||||
|
- `0.2 × 1.5^(level-1) × HOURS_PER_TICK` raw mana is consumed
|
||||||
|
- The same amount is added to the Transference mana pool
|
||||||
|
|
||||||
|
At level 10, the Enchanter converts **7.69 raw→transference/hour**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Disciplines
|
||||||
|
|
||||||
|
The Enchanter's discipline pool contains **10 disciplines** across 4 files.
|
||||||
|
|
||||||
|
### 6.1 Core Disciplines (`enchanter.ts`) — 4 disciplines
|
||||||
|
|
||||||
|
#### Enchantment Crafting (`enchant-crafting`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 8 |
|
||||||
|
| **Stat Bonus** | `enchantPower` +8 (base) |
|
||||||
|
| **Scaling Factor** | 60 |
|
||||||
|
| **Difficulty Factor** | 120 |
|
||||||
|
| **Drain Base** | 3 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Bonus |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `enchant-1` | `infinite` | 150 | +5 enchantPower per tier (repeats every 150 XP) |
|
||||||
|
| `enchant-2` | `capped` | 300 | +10 enchantPower per tier, interval 200 XP, max 3 tiers |
|
||||||
|
|
||||||
|
#### Mana Channeling (`mana-channeling`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 12 |
|
||||||
|
| **Stat Bonus** | `clickManaMultiplier` +0.3 (base) |
|
||||||
|
| **Scaling Factor** | 90 |
|
||||||
|
| **Difficulty Factor** | 180 |
|
||||||
|
| **Drain Base** | 5 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Bonus |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `channel-1` | `once` | 250 | `elementCap_lightning` +15 |
|
||||||
|
|
||||||
|
#### Study Basic Weapon Enchantments (`study-basic-weapon-enchantments`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 10 |
|
||||||
|
| **Stat Bonus** | `enchantPower` +3 (base) |
|
||||||
|
| **Scaling Factor** | 80 |
|
||||||
|
| **Difficulty Factor** | 100 |
|
||||||
|
| **Drain Base** | 2 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `basic-weapon-fire` | `once` | 50 | `sword_fire` |
|
||||||
|
| `basic-weapon-frost` | `once` | 100 | `sword_frost` |
|
||||||
|
| `basic-weapon-lightning` | `once` | 150 | `sword_lightning` |
|
||||||
|
|
||||||
|
#### Study Advanced Weapon Enchantments (`study-advanced-weapon-enchantments`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 20 |
|
||||||
|
| **Requires** | `study-basic-weapon-enchantments` |
|
||||||
|
| **Stat Bonus** | `enchantPower` +5 (base) |
|
||||||
|
| **Scaling Factor** | 120 |
|
||||||
|
| **Difficulty Factor** | 200 |
|
||||||
|
| **Drain Base** | 4 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `advanced-weapon-void` | `once` | 100 | `sword_void` |
|
||||||
|
| `advanced-weapon-damage-5` | `once` | 150 | `damage_5` |
|
||||||
|
| `advanced-weapon-crit` | `once` | 200 | `crit_5` |
|
||||||
|
| `advanced-weapon-attack-speed` | `once` | 250 | `attack_speed_10` |
|
||||||
|
|
||||||
|
### 6.2 Utility Disciplines (`enchanter-utility.ts`) — 2 disciplines
|
||||||
|
|
||||||
|
#### Study Utility Enchantments (`study-utility-enchantments`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 8 |
|
||||||
|
| **Stat Bonus** | `studySpeed` +0.05 (base) |
|
||||||
|
| **Scaling Factor** | 60 |
|
||||||
|
| **Difficulty Factor** | 80 |
|
||||||
|
| **Drain Base** | 2 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `utility-meditate` | `once` | 50 | `meditate_10` |
|
||||||
|
| `utility-study` | `once` | 100 | `study_10` |
|
||||||
|
| `utility-insight` | `once` | 150 | `insight_5` |
|
||||||
|
|
||||||
|
#### Study Mana Enchantments (`study-mana-enchantments`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 15 |
|
||||||
|
| **Stat Bonus** | `maxManaBonus` +10 (base) |
|
||||||
|
| **Scaling Factor** | 100 |
|
||||||
|
| **Difficulty Factor** | 150 |
|
||||||
|
| **Drain Base** | 3 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `mana-cap-50` | `once` | 75 | `mana_cap_50` |
|
||||||
|
| `mana-cap-100` | `once` | 150 | `mana_cap_100` |
|
||||||
|
| `mana-regen-1` | `once` | 100 | `mana_regen_1` |
|
||||||
|
| `mana-regen-2` | `once` | 200 | `mana_regen_2` |
|
||||||
|
| `click-mana-1` | `once` | 125 | `click_mana_1` |
|
||||||
|
| `click-mana-3` | `once` | 225 | `click_mana_3` |
|
||||||
|
|
||||||
|
### 6.3 Spell Disciplines (`enchanter-spells.ts`) — 3 disciplines
|
||||||
|
|
||||||
|
#### Study Basic Spell Enchantments (`study-basic-spell-enchantments`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 18 |
|
||||||
|
| **Stat Bonus** | `enchantPower` +4 (base) |
|
||||||
|
| **Scaling Factor** | 100 |
|
||||||
|
| **Difficulty Factor** | 160 |
|
||||||
|
| **Drain Base** | 3 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell-mana-bolt` | `once` | 50 | `spell_manaBolt` |
|
||||||
|
| `spell-fireball` | `once` | 100 | `spell_fireball` |
|
||||||
|
| `spell-water-jet` | `once` | 100 | `spell_waterJet` |
|
||||||
|
| `spell-gust` | `once` | 100 | `spell_gust` |
|
||||||
|
| `spell-stone-bullet` | `once` | 100 | `spell_stoneBullet` |
|
||||||
|
| `spell-light-lance` | `once` | 150 | `spell_lightLance` |
|
||||||
|
| `spell-shadow-bolt` | `once` | 150 | `spell_shadowBolt` |
|
||||||
|
| `spell-drain` | `once` | 150 | `spell_drain` |
|
||||||
|
|
||||||
|
#### Study Intermediate Spell Enchantments (`study-intermediate-spell-enchantments`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 25 |
|
||||||
|
| **Requires** | `study-basic-spell-enchantments` |
|
||||||
|
| **Stat Bonus** | `enchantPower` +6 (base) |
|
||||||
|
| **Scaling Factor** | 150 |
|
||||||
|
| **Difficulty Factor** | 250 |
|
||||||
|
| **Drain Base** | 5 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell-inferno` | `once` | 100 | `spell_inferno` |
|
||||||
|
| `spell-tidal-wave` | `once` | 100 | `spell_tidalWave` |
|
||||||
|
| `spell-earthquake` | `once` | 120 | `spell_earthquake` |
|
||||||
|
| `spell-chain-lightning` | `once` | 100 | `spell_chainLightning` |
|
||||||
|
| `spell-metal-shard` | `once` | 80 | `spell_metalShard` |
|
||||||
|
| `spell-sand-blast` | `once` | 80 | `spell_sandBlast` |
|
||||||
|
|
||||||
|
#### Study Advanced Spell Enchantments (`study-advanced-spell-enchantments`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 35 |
|
||||||
|
| **Requires** | `study-intermediate-spell-enchantments` |
|
||||||
|
| **Stat Bonus** | `enchantPower` +10 (base) |
|
||||||
|
| **Scaling Factor** | 200 |
|
||||||
|
| **Difficulty Factor** | 350 |
|
||||||
|
| **Drain Base** | 7 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell-pyroclasm` | `once` | 100 | `spell_pyroclasm` |
|
||||||
|
| `spell-tsunami` | `once` | 100 | `spell_tsunami` |
|
||||||
|
| `spell-meteor-strike` | `once` | 120 | `spell_meteorStrike` |
|
||||||
|
| `spell-heaven-light` | `once` | 100 | `spell_heavenLight` |
|
||||||
|
| `spell-oblivion` | `once` | 100 | `spell_oblivion` |
|
||||||
|
| `spell-furnace-blast` | `once` | 100 | `spell_furnaceBlast` |
|
||||||
|
| `spell-dune-collapse` | `once` | 100 | `spell_duneCollapse` |
|
||||||
|
| `spell-stellar-nova` | `once` | 200 | `spell_stellarNova` |
|
||||||
|
| `spell-void-collapse` | `once` | 180 | `spell_voidCollapse` |
|
||||||
|
| `spell-crystal-shatter` | `once` | 160 | `spell_crystalShatter` |
|
||||||
|
|
||||||
|
### 6.4 Special Discipline (`enchanter-special.ts`) — 1 discipline
|
||||||
|
|
||||||
|
#### Study Special Enchantments (`study-special-enchantments`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `transference` |
|
||||||
|
| **Base Cost** | 22 |
|
||||||
|
| **Requires** | `study-advanced-weapon-enchantments` |
|
||||||
|
| **Stat Bonus** | `enchantPower` +5 (base) |
|
||||||
|
| **Scaling Factor** | 130 |
|
||||||
|
| **Difficulty Factor** | 220 |
|
||||||
|
| **Drain Base** | 4 |
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `special-spell-echo` | `once` | 100 | `spell_echo_10` |
|
||||||
|
| `special-guardian-dmg` | `once` | 80 | `guardian_dmg_10` |
|
||||||
|
| `special-overpower` | `once` | 150 | `overpower_80` |
|
||||||
|
| `special-first-strike` | `once` | 120 | `first_strike` |
|
||||||
|
| `special-combo-master` | `once` | 200 | `combo_master` |
|
||||||
|
| `special-adrenaline-rush` | `once` | 180 | `adrenaline_rush` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Systems Unlocked
|
||||||
|
|
||||||
|
The Enchanter attunement gates the **Enchanting System** (see `enchanting-spec.md`):
|
||||||
|
|
||||||
|
- **Design** stage: Create named enchantment designs
|
||||||
|
- **Prepare** stage: Clear existing enchantments, ready equipment
|
||||||
|
- **Apply** stage: Apply saved designs to prepared equipment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Puzzle Room Behavior
|
||||||
|
|
||||||
|
In the spire, every 7th floor has a puzzle room. When the room type is
|
||||||
|
`enchanter_trial`, progress scales at 2.5–3% per tick per Enchanter level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Attunement Level Interactions
|
||||||
|
|
||||||
|
Higher Enchanter level affects:
|
||||||
|
|
||||||
|
1. **Raw mana regen**: `0.5 × 1.5^(level-1)` per hour
|
||||||
|
2. **Transference conversion rate**: `0.2 × 1.5^(level-1)` per hour
|
||||||
|
3. **Enchanting XP → Attunement XP**: Enchanting awards Enchanter XP (1 per 10 capacity used), feeding back into leveling
|
||||||
|
|
||||||
|
Attunement level does **not** directly affect enchantment strength or discipline
|
||||||
|
power — those scale through discipline XP alone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Discipline Dependency Chain
|
||||||
|
|
||||||
|
```
|
||||||
|
enchant-crafting (root)
|
||||||
|
mana-channeling (root)
|
||||||
|
study-basic-weapon-enchantments (root)
|
||||||
|
└── study-advanced-weapon-enchantments
|
||||||
|
└── study-special-enchantments
|
||||||
|
study-utility-enchantments (root)
|
||||||
|
study-mana-enchantments (root)
|
||||||
|
study-basic-spell-enchantments (root)
|
||||||
|
└── study-intermediate-spell-enchantments
|
||||||
|
└── study-advanced-spell-enchantments
|
||||||
|
```
|
||||||
|
|
||||||
|
6 root disciplines. Maximum dependency depth: 3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Acceptance Criteria
|
||||||
|
|
||||||
|
| # | Criterion |
|
||||||
|
|---|---|
|
||||||
|
| AC-1 | Enchanter starts unlocked at level 1 with 0 XP. |
|
||||||
|
| AC-2 | All 10 Enchanter disciplines are available when Enchanter is active. |
|
||||||
|
| AC-3 | Discipline dependency chains are enforced — Advanced Weapon Enchantments requires Basic Weapon Enchantments. |
|
||||||
|
| AC-4 | All perk thresholds unlock the correct enchantment effects at the specified XP values. |
|
||||||
|
| AC-5 | Enchantment Power stat bonus from all active Enchanter disciplines stacks additively. |
|
||||||
|
| AC-6 | The `enchant-1` infinite perk grants +5 enchantPower every 150 XP beyond threshold. |
|
||||||
|
| AC-7 | The `enchant-2` capped perk grants +10 enchantPower per tier, max 3 tiers, interval 200 XP beyond threshold. |
|
||||||
|
| AC-8 | Enchanting system is accessible when Enchanter is active, locked when inactive. |
|
||||||
|
| AC-9 | Enchanter `enchanter_trial` puzzle rooms grant bonus progress per Enchanter level. |
|
||||||
|
| AC-10 | Enchanter level scales raw regen and conversion rate by `1.5^(level-1)`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Files Reference
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `src/lib/game/data/attunements.ts` | Enchanter definition |
|
||||||
|
| `src/lib/game/data/disciplines/enchanter.ts` | Core Enchanter disciplines (4) |
|
||||||
|
| `src/lib/game/data/disciplines/enchanter-utility.ts` | Utility enchantment disciplines (2) |
|
||||||
|
| `src/lib/game/data/disciplines/enchanter-spells.ts` | Spell enchantment disciplines (3) |
|
||||||
|
| `src/lib/game/data/disciplines/enchanter-special.ts` | Special enchantment discipline (1) |
|
||||||
|
| `docs/specs/attunements/enchanter/systems/enchanting-spec.md` | Enchanting system spec |
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
# Enchanting System — Design Spec
|
||||||
|
|
||||||
|
> Describes the three-stage enchanting pipeline: Design → Prepare → Apply.
|
||||||
|
> Covers stage timings, mana costs, auto-transitions, enchantment capacity system,
|
||||||
|
> full enchantment effect categories, disenchanting, and discipline perk interactions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objective
|
||||||
|
|
||||||
|
Enchanting is the Enchanter attunement's primary system for enhancing equipment. It
|
||||||
|
transforms raw mana and materials into permanent equipment bonuses through a
|
||||||
|
three-stage pipeline. The player creates reusable designs, prepares equipment by
|
||||||
|
stripping existing enchantments, then applies designs to prepared equipment.
|
||||||
|
|
||||||
|
**Design goals:**
|
||||||
|
- Three distinct stages encourage planning and resource management
|
||||||
|
- Capacity and stacking systems allow deep customization of individual items
|
||||||
|
- Discipline perks progressively unlock more powerful enchantment types
|
||||||
|
- Mana costs scale with design complexity, creating meaningful trade-offs
|
||||||
|
- Auto-transitions keep the pipeline flowing without manual state management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Controls / API
|
||||||
|
|
||||||
|
### 2.1 Player Actions
|
||||||
|
|
||||||
|
| Action | Stage | Trigger |
|
||||||
|
|---|---|---|
|
||||||
|
| **Create Design** | Design | Select effects, name design, click "Create Design" |
|
||||||
|
| **Start Prepare** | Prepare | Select equipped item, click "Prepare" |
|
||||||
|
| **Apply Enchantment** | Apply | Select saved design + prepared item, click "Apply" |
|
||||||
|
| **Disenchant** | Prepare | Initiate prepare on already-enchanted equipment (enchantments removed) |
|
||||||
|
| **Cancel** | Any | Click "Cancel" during any active stage |
|
||||||
|
|
||||||
|
### 2.2 Auto-Transitions
|
||||||
|
|
||||||
|
- Design complete → returns to idle (Meditate)
|
||||||
|
- Prepare complete → returns to idle (Meditate), item gains "Ready for Enchantment" tag
|
||||||
|
- Apply complete → returns to idle (Meditate), selection state resets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Stage 1: Design
|
||||||
|
|
||||||
|
### 3.1 Flow
|
||||||
|
|
||||||
|
1. Player selects an equipment type from the type selector
|
||||||
|
2. Player adds effects from the unlocked pool via the EffectSelector
|
||||||
|
3. Player sets stack count per effect (up to `maxStacks`)
|
||||||
|
4. Player names the design
|
||||||
|
5. Player clicks "Create Design" → design begins
|
||||||
|
6. `designProgress` accumulates at `HOURS_PER_TICK` per tick
|
||||||
|
7. When `designProgress >= requiredTime` → design saved to `completedDesigns`
|
||||||
|
|
||||||
|
### 3.2 Timing Formula
|
||||||
|
|
||||||
|
```
|
||||||
|
calculateDesignTime(effects):
|
||||||
|
time = 1 // base 1 hour
|
||||||
|
for each effect: time += 0.5 * stacks
|
||||||
|
return time
|
||||||
|
```
|
||||||
|
|
||||||
|
| Design Complexity | Time |
|
||||||
|
|---|---|
|
||||||
|
| 1 effect, 1 stack | 1.5 hours |
|
||||||
|
| 3 effects, 1 stack each | 2.5 hours |
|
||||||
|
| 2 effects, 3 stacks each | 4.0 hours |
|
||||||
|
|
||||||
|
Progress per tick: `HOURS_PER_TICK = 0.04` hours.
|
||||||
|
|
||||||
|
### 3.3 Hasty Enchanter (Special Effect)
|
||||||
|
|
||||||
|
If the player has the `HASTY_ENCHANTER` special effect and the design is a **repeat**
|
||||||
|
(re-creating a previously completed design):
|
||||||
|
|
||||||
|
```
|
||||||
|
time *= 0.75 // 25% faster
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Instant Designs (Special Effect)
|
||||||
|
|
||||||
|
Per tick, if the player has the `INSTANT_DESIGNS` special effect:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const INSTANT_DESIGN_CHANCE = 0.10; // 10%
|
||||||
|
if (Math.random() < INSTANT_DESIGN_CHANCE) {
|
||||||
|
designProgress = requiredTime; // instant completion
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Dual Design Slot
|
||||||
|
|
||||||
|
A second concurrent design slot is available when:
|
||||||
|
- The first design slot has an active design (`designProgress` exists)
|
||||||
|
- The second slot is empty (`designProgress2 === null`)
|
||||||
|
- The player has the `ENCHANT_MASTERY` special boolean
|
||||||
|
|
||||||
|
### 3.6 Design Mana Cost
|
||||||
|
|
||||||
|
**None.** The Design stage has no mana cost.
|
||||||
|
|
||||||
|
### 3.7 Design Validation
|
||||||
|
|
||||||
|
- `enchantingLevel >= 1` (enchanter attunement must be active)
|
||||||
|
- Each effect must exist in `ENCHANTMENT_EFFECTS`
|
||||||
|
- Each effect's `allowedEquipmentCategories` must include the equipment's category
|
||||||
|
- Stacks cannot exceed the effect's `maxStacks`
|
||||||
|
|
||||||
|
### 3.8 Enchanting XP Award
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
calculateEnchantingXP(capacityUsed: number): number {
|
||||||
|
return Math.max(1, Math.floor(capacityUsed / 10));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Awarded to Enchanter attunement XP on design completion. This is **Attunement XP**,
|
||||||
|
not discipline XP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Stage 2: Prepare
|
||||||
|
|
||||||
|
### 4.1 Flow
|
||||||
|
|
||||||
|
1. Player selects an equipped item to prepare
|
||||||
|
2. System checks: `'Ready for Enchantment'` tag required if item was previously prepared
|
||||||
|
3. If item has existing enchantments, a confirmation dialog warns they will be removed
|
||||||
|
4. Player confirms → preparation begins
|
||||||
|
5. Mana is deducted over the prep duration
|
||||||
|
6. On completion: all enchantments removed, `usedCapacity` reset to 0, rarity reset to `'common'`, `'Ready for Enchantment'` tag added
|
||||||
|
|
||||||
|
### 4.2 Timing Formula
|
||||||
|
|
||||||
|
```
|
||||||
|
calculatePrepTime(equipmentCapacity):
|
||||||
|
time = 2 + floor(equipmentCapacity / 50)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Capacity | Prep Time |
|
||||||
|
|---|---|
|
||||||
|
| 15 (shoes) | 2 hours |
|
||||||
|
| 30 (body) | 2 hours |
|
||||||
|
| 50 (caster) | 3 hours |
|
||||||
|
| 80 (robe) | 3 hours |
|
||||||
|
|
||||||
|
### 4.3 Mana Cost Formula
|
||||||
|
|
||||||
|
```
|
||||||
|
totalMana = equipmentCapacity × 10
|
||||||
|
manaPerHour = totalMana / prepTime
|
||||||
|
manaPerTick = manaPerHour × HOURS_PER_TICK
|
||||||
|
```
|
||||||
|
|
||||||
|
| Capacity | Total Mana Cost |
|
||||||
|
|---|---|
|
||||||
|
| 15 | 150 |
|
||||||
|
| 30 | 300 |
|
||||||
|
| 50 | 500 |
|
||||||
|
| 80 | 800 |
|
||||||
|
|
||||||
|
### 4.4 Disenchant Recovery
|
||||||
|
|
||||||
|
When preparing equipment that has existing enchantments, mana is partially recovered:
|
||||||
|
|
||||||
|
```
|
||||||
|
recoveryRate = 0.10 + disenchantLevel × 0.20
|
||||||
|
manaRecovered = Σ floor(enchantment.actualCost × recoveryRate)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Disenchant Level | Recovery Rate |
|
||||||
|
|---|---|
|
||||||
|
| 0 | 10% |
|
||||||
|
| 1 | 30% |
|
||||||
|
| 2 | 50% |
|
||||||
|
| 3 | 70% |
|
||||||
|
| 4 | 90% |
|
||||||
|
| 5 | 110% |
|
||||||
|
|
||||||
|
> **Note:** `disenchantLevel` is currently hardcoded to `0` in the codebase, so the
|
||||||
|
> effective recovery rate is always **10%**.
|
||||||
|
|
||||||
|
### 4.5 Cancellation Refund
|
||||||
|
|
||||||
|
```
|
||||||
|
remainingFraction = (required - progress) / required
|
||||||
|
refundRate = remainingFraction + (1 - remainingFraction) × 0.5
|
||||||
|
manaRefund = floor(manaSpent × refundRate)
|
||||||
|
```
|
||||||
|
|
||||||
|
Unspent progress gets 100% refund; spent progress gets 50% refund; blended proportionally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Stage 3: Apply
|
||||||
|
|
||||||
|
### 5.1 Flow
|
||||||
|
|
||||||
|
1. Player selects a saved design and a prepared equipment instance
|
||||||
|
2. System validates: `currentAction === 'meditate'`, item has `'Ready for Enchantment'` tag, capacity fits
|
||||||
|
3. Player clicks "Apply" → application begins
|
||||||
|
4. Mana is deducted per hour over the application duration
|
||||||
|
5. On completion: design's effects applied to equipment, `usedCapacity` updated, design consumed
|
||||||
|
|
||||||
|
### 5.2 Timing Formula
|
||||||
|
|
||||||
|
```
|
||||||
|
calculateApplicationTime(design):
|
||||||
|
time = 2 + Σ(stacks) for all effects in design
|
||||||
|
```
|
||||||
|
|
||||||
|
| Design | Apply Time |
|
||||||
|
|---|---|
|
||||||
|
| 1 effect, 1 stack | 3 hours |
|
||||||
|
| 3 effects, 1 stack each | 5 hours |
|
||||||
|
| 2 effects, 3 stacks each | 8 hours |
|
||||||
|
|
||||||
|
### 5.3 Mana Cost Formula
|
||||||
|
|
||||||
|
```
|
||||||
|
manaPerHour = 20 + Σ(stacks × 5) for all effects
|
||||||
|
manaPerTick = manaPerHour × HOURS_PER_TICK
|
||||||
|
```
|
||||||
|
|
||||||
|
| Design | Mana/Hour |
|
||||||
|
|---|---|
|
||||||
|
| 1 effect, 1 stack | 25 |
|
||||||
|
| 3 effects, 1 stack each | 35 |
|
||||||
|
| 2 effects, 3 stacks each | 50 |
|
||||||
|
|
||||||
|
### 5.4 Free Enchant Chances
|
||||||
|
|
||||||
|
Per tick, the system checks for free enchant chances. These are **additive**:
|
||||||
|
|
||||||
|
| Special Effect | Chance |
|
||||||
|
|---|---|
|
||||||
|
| `ENCHANT_PRESERVATION` | 25% |
|
||||||
|
| `THRIFTY_ENCHANTER` | 10% |
|
||||||
|
| `OPTIMIZED_ENCHANTING` | 25% |
|
||||||
|
| **Maximum combined** | **60%** |
|
||||||
|
|
||||||
|
On trigger: `applicationProgress = requiredTime` (instant completion for that tick),
|
||||||
|
**no mana consumed** for that tick.
|
||||||
|
|
||||||
|
### 5.5 Pure Essence (Special Effect)
|
||||||
|
|
||||||
|
If the player has the `PURE_ESSENCE` special effect:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const PURE_ESSENCE_STACK_BONUS = 1.25;
|
||||||
|
const PURE_ESSENCE_COST_CAP = 100;
|
||||||
|
|
||||||
|
if (effect.baseCapacityCost < PURE_ESSENCE_COST_CAP) {
|
||||||
|
actualStacks = Math.ceil(baseStacks × PURE_ESSENCE_STACK_BONUS);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Effects with `baseCapacityCost < 100` get **25% more stacks** (rounded up).
|
||||||
|
|
||||||
|
### 5.6 Cancellation Refund
|
||||||
|
|
||||||
|
Same formula as Prepare stage (§4.5).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Enchantment Capacity System
|
||||||
|
|
||||||
|
### 6.1 Base Capacity Per Equipment Type
|
||||||
|
|
||||||
|
| Category | Equipment | Base Capacity |
|
||||||
|
|---|---|---|
|
||||||
|
| **Caster** | basicStaff | 50 |
|
||||||
|
| | apprenticeWand | 35 |
|
||||||
|
| | oakStaff | 65 |
|
||||||
|
| | crystalWand | 45 |
|
||||||
|
| | arcanistStaff | 80 |
|
||||||
|
| | battlestaff | 70 |
|
||||||
|
| **Catalyst** | basicCatalyst | 40 |
|
||||||
|
| | fireCatalyst | 55 |
|
||||||
|
| | voidCatalyst | 75 |
|
||||||
|
| **Sword** | ironBlade | 30 |
|
||||||
|
| | steelBlade | 40 |
|
||||||
|
| | crystalBlade | 55 |
|
||||||
|
| | arcanistBlade | 65 |
|
||||||
|
| | voidBlade | 50 |
|
||||||
|
| **Head** | clothHood | 25 |
|
||||||
|
| | apprenticeCap | 30 |
|
||||||
|
| | wizardHat | 45 |
|
||||||
|
| | arcanistCirclet | 40 |
|
||||||
|
| | battleHelm | 50 |
|
||||||
|
| **Body** | civilianShirt | 30 |
|
||||||
|
| | apprenticeRobe | 45 |
|
||||||
|
| | scholarRobe | 55 |
|
||||||
|
| | battleRobe | 65 |
|
||||||
|
| | arcanistRobe | 80 |
|
||||||
|
| **Hands** | civilianGloves | 20 |
|
||||||
|
| | apprenticeGloves | 30 |
|
||||||
|
| | spellweaveGloves | 40 |
|
||||||
|
| | combatGauntlets | 35 |
|
||||||
|
| **Feet** | civilianShoes | 15 |
|
||||||
|
| | apprenticeBoots | 25 |
|
||||||
|
| | travelerBoots | 30 |
|
||||||
|
| | battleBoots | 35 |
|
||||||
|
| **Accessory** | copperRing | 15 |
|
||||||
|
| | silverRing | 25 |
|
||||||
|
| | goldRing | 35 |
|
||||||
|
| | signetRing | 30 |
|
||||||
|
| | copperAmulet | 20 |
|
||||||
|
| | silverAmulet | 30 |
|
||||||
|
| | crystalPendant | 45 |
|
||||||
|
| | manaBrooch | 40 |
|
||||||
|
| | arcanistPendant | 55 |
|
||||||
|
| | voidTouchedRing | 50 |
|
||||||
|
|
||||||
|
### 6.2 Stacking Cost Formula
|
||||||
|
|
||||||
|
```
|
||||||
|
calculateEffectCapacityCost(effectId, stacks, efficiencyBonus):
|
||||||
|
totalCost = 0
|
||||||
|
for i in 0..stacks-1:
|
||||||
|
stackMultiplier = 1 + (i × 0.2)
|
||||||
|
totalCost += baseCapacityCost × stackMultiplier
|
||||||
|
return floor(totalCost × (1 - efficiencyBonus))
|
||||||
|
```
|
||||||
|
|
||||||
|
| Stack Index | Multiplier |
|
||||||
|
|---|---|
|
||||||
|
| 0 (1st) | 1.0× |
|
||||||
|
| 1 (2nd) | 1.2× |
|
||||||
|
| 2 (3rd) | 1.4× |
|
||||||
|
| 3 (4th) | 1.6× |
|
||||||
|
| 4 (5th) | 1.8× |
|
||||||
|
|
||||||
|
Example: 3 stacks of a cost-20 effect:
|
||||||
|
`20×1.0 + 20×1.2 + 20×1.4 = 20 + 24 + 28 = 72` capacity used.
|
||||||
|
|
||||||
|
### 6.3 Efficiency Bonus
|
||||||
|
|
||||||
|
The `efficiencyBonus` reduces total capacity cost. Sources include discipline perks
|
||||||
|
(e.g., Crafting Efficiency discipline from Fabricator pool). Applied as:
|
||||||
|
`totalCost × (1 - efficiencyBonus)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Enchantment Effect Categories
|
||||||
|
|
||||||
|
### 7.1 Spell Effects (category: `'spell'`) — Casters only
|
||||||
|
|
||||||
|
**Basic Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_manaBolt` | Mana Bolt | 50 | 1 |
|
||||||
|
| `spell_manaStrike` | Mana Strike | 40 | 1 |
|
||||||
|
| `spell_fireball` | Fireball | 80 | 1 |
|
||||||
|
| `spell_emberShot` | Ember Shot | 60 | 1 |
|
||||||
|
| `spell_waterJet` | Water Jet | 70 | 1 |
|
||||||
|
| `spell_iceShard` | Ice Shard | 75 | 1 |
|
||||||
|
| `spell_gust` | Gust | 60 | 1 |
|
||||||
|
| `spell_stoneBullet` | Stone Bullet | 80 | 1 |
|
||||||
|
| `spell_lightLance` | Light Lance | 95 | 1 |
|
||||||
|
| `spell_shadowBolt` | Shadow Bolt | 95 | 1 |
|
||||||
|
| `spell_drain` | Drain | 85 | 1 |
|
||||||
|
| `spell_rotTouch` | Rot Touch | 80 | 1 |
|
||||||
|
| `spell_windSlash` | Wind Slash | 72 | 1 |
|
||||||
|
| `spell_rockSpike` | Rock Spike | 88 | 1 |
|
||||||
|
| `spell_radiance` | Radiance | 80 | 1 |
|
||||||
|
| `spell_darkPulse` | Dark Pulse | 68 | 1 |
|
||||||
|
|
||||||
|
**Tier 2 Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_inferno` | Inferno | 180 | 1 |
|
||||||
|
| `spell_tidalWave` | Tidal Wave | 175 | 1 |
|
||||||
|
| `spell_hurricane` | Hurricane | 170 | 1 |
|
||||||
|
| `spell_earthquake` | Earthquake | 200 | 1 |
|
||||||
|
| `spell_solarFlare` | Solar Flare | 190 | 1 |
|
||||||
|
| `spell_voidRift` | Void Rift | 175 | 1 |
|
||||||
|
| `spell_flameWave` | Flame Wave | 165 | 1 |
|
||||||
|
| `spell_iceStorm` | Ice Storm | 170 | 1 |
|
||||||
|
| `spell_windBlade` | Wind Blade | 155 | 1 |
|
||||||
|
| `spell_stoneBarrage` | Stone Barrage | 175 | 1 |
|
||||||
|
| `spell_divineSmite` | Divine Smite | 175 | 1 |
|
||||||
|
| `spell_shadowStorm` | Shadow Storm | 168 | 1 |
|
||||||
|
| `spell_soulRend` | Soul Rend | 170 | 1 |
|
||||||
|
|
||||||
|
**Tier 3 Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_pyroclasm` | Pyroclasm | 400 | 1 |
|
||||||
|
| `spell_tsunami` | Tsunami | 380 | 1 |
|
||||||
|
| `spell_meteorStrike` | Meteor Strike | 420 | 1 |
|
||||||
|
| `spell_cosmicStorm` | Cosmic Storm | 370 | 1 |
|
||||||
|
| `spell_heavenLight` | Heaven's Light | 390 | 1 |
|
||||||
|
| `spell_oblivion` | Oblivion | 385 | 1 |
|
||||||
|
| `spell_deathMark` | Death Mark | 370 | 1 |
|
||||||
|
|
||||||
|
**Legendary Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_stellarNova` | Stellar Nova | 600 | 1 |
|
||||||
|
| `spell_voidCollapse` | Void Collapse | 550 | 1 |
|
||||||
|
| `spell_crystalShatter` | Crystal Shatter | 500 | 1 |
|
||||||
|
|
||||||
|
**Lightning Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_spark` | Spark | 70 | 1 |
|
||||||
|
| `spell_lightningBolt` | Lightning Bolt | 90 | 1 |
|
||||||
|
| `spell_chainLightning` | Chain Lightning | 160 | 1 |
|
||||||
|
| `spell_stormCall` | Storm Call | 190 | 1 |
|
||||||
|
| `spell_thunderStrike` | Thunder Strike | 350 | 1 |
|
||||||
|
|
||||||
|
**Frost Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_frostBite` | Frost Bite | 78 | 1 |
|
||||||
|
| `spell_iceShard` | Ice Shard | 95 | 1 |
|
||||||
|
| `spell_frostNova` | Frost Nova | 165 | 1 |
|
||||||
|
| `spell_glacialSpike` | Glacial Spike | 200 | 1 |
|
||||||
|
| `spell_absoluteZero` | Absolute Zero | 380 | 1 |
|
||||||
|
|
||||||
|
**Metal Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_metalShard` | Metal Shard | 85 | 1 |
|
||||||
|
| `spell_ironFist` | Iron Fist | 120 | 1 |
|
||||||
|
| `spell_steelTempest` | Steel Tempest | 190 | 1 |
|
||||||
|
| `spell_furnaceBlast` | Furnace Blast | 400 | 1 |
|
||||||
|
|
||||||
|
**Sand Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_sandBlast` | Sand Blast | 72 | 1 |
|
||||||
|
| `spell_sandstorm` | Sandstorm | 100 | 1 |
|
||||||
|
| `spell_desertWind` | Desert Wind | 155 | 1 |
|
||||||
|
| `spell_duneCollapse` | Dune Collapse | 300 | 1 |
|
||||||
|
|
||||||
|
**BlackFlame Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_blackFire` | Black Fire | 82 | 1 |
|
||||||
|
| `spell_shadowEmber` | Shadow Ember | 105 | 1 |
|
||||||
|
| `spell_darkInferno` | Dark Inferno | 175 | 1 |
|
||||||
|
| `spell_umbralBlaze` | Umbral Blaze | 210 | 1 |
|
||||||
|
| `spell_hellfireCurse` | Hellfire Curse | 410 | 1 |
|
||||||
|
|
||||||
|
**Radiant Flames Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_radiantBurst` | Radiant Burst | 85 | 1 |
|
||||||
|
| `spell_holyFlame` | Holy Flame | 108 | 1 |
|
||||||
|
| `spell_blindingSun` | Blinding Sun | 180 | 1 |
|
||||||
|
| `spell_purifyingFire` | Purifying Fire | 215 | 1 |
|
||||||
|
| `spell_supernovaBlast` | Supernova Blast | 420 | 1 |
|
||||||
|
|
||||||
|
**Miasma Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_toxicCloud` | Toxic Cloud | 76 | 1 |
|
||||||
|
| `spell_plagueTouch` | Plague Touch | 100 | 1 |
|
||||||
|
| `spell_miasmaBurst` | Miasma Burst | 165 | 1 |
|
||||||
|
| `spell_pestilence` | Pestilence | 195 | 1 |
|
||||||
|
| `spell_deathMiasma` | Death Miasma | 390 | 1 |
|
||||||
|
|
||||||
|
**Shadow Glass Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_shadowSpike` | Shadow Spike | 88 | 1 |
|
||||||
|
| `spell_darkShard` | Dark Shard | 115 | 1 |
|
||||||
|
| `spell_obsidianStorm` | Obsidian Storm | 185 | 1 |
|
||||||
|
| `spell_voidBlade` | Void Blade | 225 | 1 |
|
||||||
|
| `spell_shadowGlassCataclysm` | Shadow Glass Cataclysm | 415 | 1 |
|
||||||
|
|
||||||
|
**Exotic Spells:**
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `spell_soulPierce` | Soul Pierce | 500 | 1 |
|
||||||
|
| `spell_spiritBlast` | Spirit Blast | 650 | 1 |
|
||||||
|
| `spell_temporalWarp` | Temporal Warp | 520 | 1 |
|
||||||
|
| `spell_chronoStasis` | Chrono Stasis | 680 | 1 |
|
||||||
|
| `spell_plasmaBolt` | Plasma Bolt | 510 | 1 |
|
||||||
|
| `spell_plasmaStorm` | Plasma Storm | 660 | 1 |
|
||||||
|
|
||||||
|
### 7.2 Mana Effects (category: `'mana'`)
|
||||||
|
|
||||||
|
**General Mana** — Allowed on: `['caster', 'catalyst', 'head', 'body', 'accessory']`
|
||||||
|
|
||||||
|
| Effect ID | Name | Description | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `mana_cap_50` | Mana Reserve | +50 max mana | 20 | 3 |
|
||||||
|
| `mana_cap_100` | Mana Reservoir | +100 max mana | 35 | 3 |
|
||||||
|
| `mana_regen_1` | Trickle | +1 mana/hour regen | 15 | 5 |
|
||||||
|
| `mana_regen_2` | Stream | +2 mana/hour regen | 28 | 4 |
|
||||||
|
| `mana_regen_5` | River | +5 mana/hour regen | 50 | 3 |
|
||||||
|
| `click_mana_1` | Mana Tap | +1 mana per click | 20 | 5 |
|
||||||
|
| `click_mana_3` | Mana Surge | +3 mana per click | 35 | 3 |
|
||||||
|
|
||||||
|
**Weapon Mana** — Allowed on: `['caster', 'catalyst', 'sword']`
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `weapon_mana_cap_20` | Mana Cell | 25 | 5 |
|
||||||
|
| `weapon_mana_cap_50` | Mana Vessel | 50 | 3 |
|
||||||
|
| `weapon_mana_cap_100` | Mana Core | 80 | 2 |
|
||||||
|
| `weapon_mana_regen_1` | Mana Wick | 20 | 5 |
|
||||||
|
| `weapon_mana_regen_2` | Mana Siphon | 35 | 3 |
|
||||||
|
| `weapon_mana_regen_5` | Mana Well | 60 | 2 |
|
||||||
|
|
||||||
|
**Per-Element Capacity** — Allowed on: `['caster', 'catalyst', 'head', 'body', 'accessory']`
|
||||||
|
|
||||||
|
Generated for each non-utility element (21 elements). Three tiers per element:
|
||||||
|
- `{element}_cap_10`: cost 30, max 5 stacks
|
||||||
|
- `{element}_cap_25`: cost 60, max 3 stacks
|
||||||
|
- `{element}_cap_50`: cost 100, max 2 stacks
|
||||||
|
|
||||||
|
### 7.3 Combat Effects (category: `'combat'`) — Casters, Hands
|
||||||
|
|
||||||
|
| Effect ID | Name | Description | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `damage_5` | Minor Power | +5 base damage | 15 | 5 |
|
||||||
|
| `damage_10` | Moderate Power | +10 base damage | 28 | 4 |
|
||||||
|
| `damage_pct_10` | Amplification | +10% damage | 30 | 3 |
|
||||||
|
| `crit_5` | Sharp Edge | +5% crit chance | 20 | 4 |
|
||||||
|
| `attack_speed_10` | Swift Casting | +10% attack speed | 22 | 4 |
|
||||||
|
|
||||||
|
### 7.4 Elemental Effects (category: `'elemental'`) — Casters, Swords
|
||||||
|
|
||||||
|
| Effect ID | Name | Description | Base Cost | Max Stacks |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `sword_fire` | Fire Enchant | Burns enemies | 40 | 1 |
|
||||||
|
| `sword_frost` | Frost Enchant | Prevents dodge | 40 | 1 |
|
||||||
|
| `sword_lightning` | Lightning Enchant | 30% armor pierce | 50 | 1 |
|
||||||
|
| `sword_void` | Void Enchant | +20% damage | 60 | 1 |
|
||||||
|
|
||||||
|
### 7.5 Utility Effects (category: `'utility'`)
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks | Allowed On |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `meditate_10` | Meditative Focus | 18 | 5 | head, body, accessory |
|
||||||
|
| `study_10` | Quick Study | 22 | 4 | caster, catalyst, head, body, hands, feet, accessory |
|
||||||
|
| `insight_5` | Insightful | 25 | 4 | head, accessory |
|
||||||
|
|
||||||
|
### 7.6 Special Effects (category: `'special'`)
|
||||||
|
|
||||||
|
| Effect ID | Name | Base Cost | Max Stacks | Allowed On |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `spell_echo_10` | Echo Chamber | 60 | 2 | caster |
|
||||||
|
| `guardian_dmg_10` | Bane | 35 | 3 | caster, catalyst, accessory |
|
||||||
|
| `overpower_80` | Overpower | 55 | 1 | caster, hands |
|
||||||
|
| `first_strike` | First Strike | 45 | 1 | caster, hands |
|
||||||
|
| `combo_master` | Combo Master | 65 | 1 | caster, hands |
|
||||||
|
| `adrenaline_rush` | Adrenaline Rush | 50 | 1 | caster, hands |
|
||||||
|
|
||||||
|
### 7.7 Defense Effects (category: `'defense'`)
|
||||||
|
|
||||||
|
**Empty** — No defense effects are currently defined.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Discipline Perks That Affect Enchanting
|
||||||
|
|
||||||
|
| Discipline | Perk | Threshold | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Enchantment Crafting | `enchant-1` (infinite) | 150 XP | +5 enchantPower per tier |
|
||||||
|
| Enchantment Crafting | `enchant-2` (capped) | 300 XP | +10 enchantPower/tier, max 3 |
|
||||||
|
| Study Basic Weapon Enchantments | `basic-weapon-fire` | 50 XP | Unlocks `sword_fire` |
|
||||||
|
| Study Basic Weapon Enchantments | `basic-weapon-frost` | 100 XP | Unlocks `sword_frost` |
|
||||||
|
| Study Basic Weapon Enchantments | `basic-weapon-lightning` | 150 XP | Unlocks `sword_lightning` |
|
||||||
|
| Study Advanced Weapon Enchantments | `advanced-weapon-void` | 100 XP | Unlocks `sword_void` |
|
||||||
|
| Study Advanced Weapon Enchantments | `advanced-weapon-damage-5` | 150 XP | Unlocks `damage_5` |
|
||||||
|
| Study Advanced Weapon Enchantments | `advanced-weapon-crit` | 200 XP | Unlocks `crit_5` |
|
||||||
|
| Study Advanced Weapon Enchantments | `advanced-weapon-attack-speed` | 250 XP | Unlocks `attack_speed_10` |
|
||||||
|
| Study Utility Enchantments | `utility-meditate` | 50 XP | Unlocks `meditate_10` |
|
||||||
|
| Study Utility Enchantments | `utility-study` | 100 XP | Unlocks `study_10` |
|
||||||
|
| Study Utility Enchantments | `utility-insight` | 150 XP | Unlocks `insight_5` |
|
||||||
|
| Study Mana Enchantments | `mana-cap-50` | 75 XP | Unlocks `mana_cap_50` |
|
||||||
|
| Study Mana Enchantments | `mana-cap-100` | 150 XP | Unlocks `mana_cap_100` |
|
||||||
|
| Study Mana Enchantments | `mana-regen-1` | 100 XP | Unlocks `mana_regen_1` |
|
||||||
|
| Study Mana Enchantments | `mana-regen-2` | 200 XP | Unlocks `mana_regen_2` |
|
||||||
|
| Study Mana Enchantments | `click-mana-1` | 125 XP | Unlocks `click_mana_1` |
|
||||||
|
| Study Mana Enchantments | `click-mana-3` | 225 XP | Unlocks `click_mana_3` |
|
||||||
|
| Study Basic Spell Enchantments | 8 perks | 50–150 XP | Unlock 8 basic spell enchants |
|
||||||
|
| Study Intermediate Spell Enchantments | 6 perks | 80–120 XP | Unlock 6 intermediate spell enchants |
|
||||||
|
| Study Advanced Spell Enchantments | 10 perks | 100–200 XP | Unlock 10 advanced spell enchants |
|
||||||
|
| Study Special Enchantments | 6 perks | 80–200 XP | Unlock 6 special enchants |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Attunement Level Interactions
|
||||||
|
|
||||||
|
Enchanter level does **not** directly affect enchanting mechanics (timings, costs,
|
||||||
|
capacity). It affects:
|
||||||
|
|
||||||
|
1. **Raw mana regen**: `0.5 × 1.5^(level-1)` per hour — more raw mana for enchanting
|
||||||
|
2. **Transference conversion**: `0.2 × 1.5^(level-1)` per hour — more transference mana for Enchanter disciplines
|
||||||
|
3. **Enchanting XP → Attunement XP**: 1 Enchanter XP per 10 capacity used
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Acceptance Criteria
|
||||||
|
|
||||||
|
| # | Criterion |
|
||||||
|
|---|---|
|
||||||
|
| AC-1 | Design stage takes `1 + 0.5 × totalStacks` hours; progress accumulates at 0.04 hours/tick. |
|
||||||
|
| AC-2 | Hasty Enchanter reduces design time by 25% on repeat designs only. |
|
||||||
|
| AC-3 | Instant Designs has a 10% chance per tick to complete the design immediately. |
|
||||||
|
| AC-4 | Dual design slot is available when Enchant Mastery is active and first slot is occupied. |
|
||||||
|
| AC-5 | Prepare stage takes `2 + floor(capacity/50)` hours and costs `capacity × 10` total mana. |
|
||||||
|
| AC-6 | Prepare removes all enchantments, resets usedCapacity to 0, resets rarity to 'common'. |
|
||||||
|
| AC-7 | Disenchant recovery rate is `0.10 + disenchantLevel × 0.20` of each enchantment's actual cost. |
|
||||||
|
| AC-8 | Apply stage takes `2 + totalStacks` hours and costs `20 + sum(stacks × 5)` mana/hour. |
|
||||||
|
| AC-9 | Free enchant chances are additive (max 60%) and skip mana cost for that tick. |
|
||||||
|
| AC-10 | Pure Essence grants 1.25× stacks (ceil) for effects with base cost < 100. |
|
||||||
|
| AC-11 | Stacking cost formula: `baseCost × (1 + i × 0.2)` for stack index i, reduced by efficiencyBonus. |
|
||||||
|
| AC-12 | Cancellation refunds unspent progress at 100% and spent progress at 50%, blended. |
|
||||||
|
| AC-13 | All enchantment effects are gated behind discipline perk thresholds and cannot be used until unlocked. |
|
||||||
|
| AC-14 | Equipment type capacity limits are enforced — designs exceeding capacity are rejected. |
|
||||||
|
| AC-15 | Spell effects can only be applied to caster equipment. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Files Reference
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `src/lib/game/crafting-design.ts` | Design stage logic, timing, validation |
|
||||||
|
| `src/lib/game/crafting-prep.ts` | Prepare stage logic, disenchant recovery |
|
||||||
|
| `src/lib/game/crafting-apply.ts` | Apply stage logic, free enchant, Pure Essence |
|
||||||
|
| `src/lib/game/crafting-utils.ts` | Shared utilities, capacity cost, cancellation refund |
|
||||||
|
| `src/lib/game/crafting-attunements.ts` | Attunement-crafting integration, enchanting XP |
|
||||||
|
| `src/lib/game/data/enchantments/` | All enchantment effect definitions (7 categories) |
|
||||||
|
| `src/lib/game/crafting-actions/design-actions.ts` | Design stage store actions |
|
||||||
|
| `src/lib/game/crafting-actions/preparation-actions.ts` | Prepare stage store actions |
|
||||||
|
| `src/lib/game/crafting-actions/application-actions.ts` | Apply stage store actions |
|
||||||
|
| `src/lib/game/crafting-actions/disenchant-actions.ts` | Disenchant action |
|
||||||
|
| `src/components/game/tabs/CraftingTab.tsx` | Crafting tab wrapper |
|
||||||
|
| `src/components/game/crafting/EnchantmentDesigner.tsx` | Design UI |
|
||||||
|
| `src/components/game/crafting/EnchantmentPreparer.tsx` | Prepare UI |
|
||||||
|
| `src/components/game/crafting/EnchantmentApplier.tsx` | Apply UI |
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
# Fabricator Attunement — Design Spec
|
||||||
|
|
||||||
|
> Describes the Fabricator attunement: identity, unlock flow, mana behavior, full
|
||||||
|
> discipline list with stats/perks, systems unlocked, and attunement level interactions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objective
|
||||||
|
|
||||||
|
The Fabricator is the crafting and golemancy attunement. It provides access to
|
||||||
|
Earth-based disciplines that unlock equipment fabrication recipes, golem summoning,
|
||||||
|
and crafting cost reduction. The Fabricator is the primary source of custom
|
||||||
|
equipment and the golem combat system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Identity
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **ID** | `fabricator` |
|
||||||
|
| **Slot** | `leftHand` |
|
||||||
|
| **Icon** | `⚒️` |
|
||||||
|
| **Color** | `#F4A261` (Earth) |
|
||||||
|
| **Primary Mana** | `earth` |
|
||||||
|
| **Raw Mana Regen** | +0.4/hour (base, scales with `1.5^(level-1)`) |
|
||||||
|
| **Conversion Rate** | 0.25 raw→earth/hour (base, scales with `1.5^(level-1)`) |
|
||||||
|
| **Unlock** | Prove crafting worth |
|
||||||
|
| **Capabilities** | `['golemCrafting', 'gearCrafting', 'earthShaping']` |
|
||||||
|
| **Skill Categories** | `['fabrication', 'golemancy']` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Unlock Condition and Flow
|
||||||
|
|
||||||
|
**Condition:** Prove your worth as a crafter.
|
||||||
|
|
||||||
|
**Unlock flow:**
|
||||||
|
1. Meet the crafting-related unlock condition
|
||||||
|
2. Fabricator becomes available for activation
|
||||||
|
3. Player activates Fabricator → initialized at `{ active: true, level: 1, experience: 0 }`
|
||||||
|
4. Fabricator disciplines become available (5 total)
|
||||||
|
|
||||||
|
The unlock condition is stored as a descriptive string:
|
||||||
|
`"Prove your worth as a crafter"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Raw Mana Regen Contribution
|
||||||
|
|
||||||
|
Base regen: **+0.4/hour** (at level 1). Scales exponentially:
|
||||||
|
|
||||||
|
```
|
||||||
|
effectiveRegen = 0.4 × 1.5^(level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Level | Raw Regen |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 0.400/hr |
|
||||||
|
| 5 | 2.025/hr |
|
||||||
|
| 10 | 15.377/hr |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Mana Conversion Behavior
|
||||||
|
|
||||||
|
The Fabricator converts raw mana to Earth:
|
||||||
|
|
||||||
|
```
|
||||||
|
effectiveConversionRate = 0.25 × 1.5^(level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
At level 10, the Fabricator converts **9.61 raw→earth/hour**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Disciplines
|
||||||
|
|
||||||
|
The Fabricator's discipline pool contains **5 disciplines**.
|
||||||
|
|
||||||
|
### 6.1 Golem Crafting (`golem-crafting`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `earth` |
|
||||||
|
| **Base Cost** | 10 |
|
||||||
|
| **Stat Bonus** | `golemCapacity` +2 (base) |
|
||||||
|
| **Scaling Factor** | 80 |
|
||||||
|
| **Difficulty Factor** | 150 |
|
||||||
|
| **Drain Base** | 4 |
|
||||||
|
|
||||||
|
**Perks:**
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Bonus |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `golem-1` | `once` | 200 | Unlock golem summoning |
|
||||||
|
| `golem-2` | `capped` | 500 | +1 Golem Capacity per tier, interval 500 XP, max 2 tiers |
|
||||||
|
|
||||||
|
### 6.2 Crafting Efficiency (`crafting-efficiency`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `earth` |
|
||||||
|
| **Base Cost** | 12 |
|
||||||
|
| **Stat Bonus** | `craftingCostReduction` +15 (base) |
|
||||||
|
| **Scaling Factor** | 90 |
|
||||||
|
| **Difficulty Factor** | 180 |
|
||||||
|
| **Drain Base** | 6 |
|
||||||
|
|
||||||
|
**Perks:**
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Bonus |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `efficiency-1` | `once` | 300 | +10% Crafting Cost Reduction |
|
||||||
|
|
||||||
|
### 6.3 Study Fabricator Recipes (`study-fabricator-recipes`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `earth` |
|
||||||
|
| **Base Cost** | 10 |
|
||||||
|
| **Stat Bonus** | `enchantPower` +3 (base) |
|
||||||
|
| **Scaling Factor** | 80 |
|
||||||
|
| **Difficulty Factor** | 100 |
|
||||||
|
| **Drain Base** | 2 |
|
||||||
|
|
||||||
|
**Perks:**
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `fabricator-earth` | `once` | 50 | `earthHelm`, `earthChest`, `earthBoots` |
|
||||||
|
| `fabricator-metal` | `once` | 100 | `metalBlade`, `metalShield`, `metalGloves` |
|
||||||
|
| `fabricator-sand` | `once` | 150 | `sandBoots`, `sandGloves`, `sandVest` |
|
||||||
|
| `fabricator-crystal` | `once` | 200 | `crystalWand`, `crystalRing`, `crystalAmulet` |
|
||||||
|
|
||||||
|
### 6.4 Study Wizard Equipment (`study-wizard-branch`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `earth` |
|
||||||
|
| **Base Cost** | 15 |
|
||||||
|
| **Requires** | `study-fabricator-recipes` |
|
||||||
|
| **Stat Bonus** | `enchantPower` +5 (base) |
|
||||||
|
| **Scaling Factor** | 100 |
|
||||||
|
| **Difficulty Factor** | 150 |
|
||||||
|
| **Drain Base** | 3 |
|
||||||
|
|
||||||
|
**Perks:**
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `wizard-oak` | `once` | 50 | `oakStaff` |
|
||||||
|
| `wizard-arcanist-staff` | `once` | 100 | `arcanistStaff` |
|
||||||
|
| `wizard-battlestaff` | `once` | 150 | `battlestaff` |
|
||||||
|
| `wizard-arcanist-gear` | `once` | 200 | `arcanistCirclet`, `arcanistRobe` |
|
||||||
|
| `wizard-void-catalyst` | `once` | 250 | `voidCatalyst` |
|
||||||
|
| `wizard-arcanist-pendant` | `once` | 300 | `arcanistPendant` |
|
||||||
|
|
||||||
|
### 6.5 Study Physical Equipment (`study-physical-branch`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `earth` |
|
||||||
|
| **Base Cost** | 15 |
|
||||||
|
| **Requires** | `study-fabricator-recipes` |
|
||||||
|
| **Stat Bonus** | `enchantPower` +5 (base) |
|
||||||
|
| **Scaling Factor** | 100 |
|
||||||
|
| **Difficulty Factor** | 150 |
|
||||||
|
| **Drain Base** | 3 |
|
||||||
|
|
||||||
|
**Perks:**
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Unlocks |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `physical-crystal-blade` | `once` | 50 | `crystalBlade` |
|
||||||
|
| `physical-arcanist-blade` | `once` | 100 | `arcanistBlade` |
|
||||||
|
| `physical-void-blade` | `once` | 150 | `voidBlade` |
|
||||||
|
| `physical-battle-gear` | `once` | 200 | `battleHelm`, `battleRobe` |
|
||||||
|
| `physical-battle-boots` | `once` | 250 | `battleBoots` |
|
||||||
|
| `physical-combat-gauntlets` | `once` | 300 | `combatGauntlets` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Systems Unlocked
|
||||||
|
|
||||||
|
The Fabricator attunement gates two systems:
|
||||||
|
|
||||||
|
1. **Golemancy** (see `golemancy-spec.md`): Summon and maintain golems for spire combat
|
||||||
|
2. **Item Fabrication** (see `item-fabrication-spec.md`): Craft equipment and materials from recipes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Puzzle Room Behavior
|
||||||
|
|
||||||
|
In the spire, every 7th floor has a puzzle room. When the room type is
|
||||||
|
`fabricator_trial`, progress scales at 2.5–3% per tick per Fabricator level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Attunement Level Interactions
|
||||||
|
|
||||||
|
Higher Fabricator level affects:
|
||||||
|
|
||||||
|
1. **Raw mana regen**: `0.4 × 1.5^(level-1)` per hour
|
||||||
|
2. **Earth conversion rate**: `0.25 × 1.5^(level-1)` per hour
|
||||||
|
3. **Golem slots**: `floor(fabricatorLevel / 2)` — Fabricator level directly determines golem capacity
|
||||||
|
|
||||||
|
| Fabricator Level | Golem Slots |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 0 |
|
||||||
|
| 2–3 | 1 |
|
||||||
|
| 4–5 | 2 |
|
||||||
|
| 6–7 | 3 |
|
||||||
|
| 8–9 | 4 |
|
||||||
|
| 10 | 5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Discipline Dependency Chain
|
||||||
|
|
||||||
|
```
|
||||||
|
golem-crafting (root)
|
||||||
|
crafting-efficiency (root)
|
||||||
|
study-fabricator-recipes (root)
|
||||||
|
└── study-wizard-branch
|
||||||
|
└── study-physical-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
3 root disciplines. Maximum dependency depth: 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Acceptance Criteria
|
||||||
|
|
||||||
|
| # | Criterion |
|
||||||
|
|---|---|
|
||||||
|
| AC-1 | Fabricator is locked until the unlock condition is met. |
|
||||||
|
| AC-2 | All 5 Fabricator disciplines are available when Fabricator is active. |
|
||||||
|
| AC-3 | `study-wizard-branch` and `study-physical-branch` require `study-fabricator-recipes`. |
|
||||||
|
| AC-4 | Golem summoning is unlocked at Golem Crafting discipline threshold 200 XP. |
|
||||||
|
| AC-5 | Golem capacity is 2 (base) + up to 2 (from capped perk) = max 4 from disciplines. |
|
||||||
|
| AC-6 | Golem slots from attunement level: `floor(fabricatorLevel / 2)`, max 5 at level 10. |
|
||||||
|
| AC-7 | All recipe unlock perks fire at the correct discipline XP thresholds. |
|
||||||
|
| AC-8 | Crafting Efficiency discipline reduces material costs by 15% (base) + 10% (perk). |
|
||||||
|
| AC-9 | Fabricator `fabricator_trial` puzzle rooms grant bonus progress per Fabricator level. |
|
||||||
|
| AC-10 | Fabricator level scales raw regen and earth conversion by `1.5^(level-1)`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Files Reference
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `src/lib/game/data/attunements.ts` | Fabricator definition |
|
||||||
|
| `src/lib/game/data/disciplines/fabricator.ts` | Fabricator disciplines (5) |
|
||||||
|
| `src/lib/game/data/golems/` | Golem definitions (10 golems) |
|
||||||
|
| `src/lib/game/crafting-fabricator.ts` | Fabrication crafting logic |
|
||||||
|
| `src/lib/game/data/fabricator-recipes.ts` | Core equipment recipes |
|
||||||
|
| `src/lib/game/data/fabricator-material-recipes.ts` | Material recipes |
|
||||||
|
| `src/lib/game/data/fabricator-physical-recipes.ts` | Physical branch recipes |
|
||||||
|
| `src/lib/game/data/fabricator-wizard-recipes.ts` | Wizard branch recipes |
|
||||||
|
| `src/components/game/tabs/GolemancyTab.tsx` | Golemancy UI |
|
||||||
|
| `docs/specs/attunements/fabricator/systems/golemancy-spec.md` | Golemancy system spec |
|
||||||
|
| `docs/specs/attunements/fabricator/systems/item-fabrication-spec.md` | Item fabrication spec |
|
||||||
@@ -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 |
|
||||||
|
| 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 |
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
# Item Fabrication System — Design Spec
|
||||||
|
|
||||||
|
> Describes the Fabricator attunement's crafting system: recipe categories, unlock
|
||||||
|
> gates, material costs, crafting flow, and how fabricated items differ from base loot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objective
|
||||||
|
|
||||||
|
Item Fabrication is the Fabricator attunement's non-combat crafting system. It allows
|
||||||
|
the player to craft materials and equipment using mana and component items. Recipes
|
||||||
|
are unlocked through Fabricator discipline perks, and the resulting equipment can
|
||||||
|
carry pre-applied enchantments, making fabrication a parallel path to the Enchanter's
|
||||||
|
enchanting system.
|
||||||
|
|
||||||
|
**Design goals:**
|
||||||
|
- Fabricated equipment provides an alternative to loot drops
|
||||||
|
- Material crafting creates a multi-tier resource pipeline
|
||||||
|
- Discipline-gated recipe unlocks reward Fabricator attunement investment
|
||||||
|
- Pre-applied enchantments on crafted gear offer unique combinations
|
||||||
|
- Crafting Efficiency discipline reduces material costs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Recipe Categories
|
||||||
|
|
||||||
|
### 2.1 Overview
|
||||||
|
|
||||||
|
| Category | File | Count | Unlock Gate |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Material Recipes | `fabricator-material-recipes.ts` | 15 | None (base recipes) |
|
||||||
|
| Core Equipment (Elemental) | `fabricator-recipes.ts` | 12 | Study Fabricator Recipes discipline |
|
||||||
|
| Wizard Branch | `fabricator-wizard-recipes.ts` | 14 | Study Wizard Equipment discipline |
|
||||||
|
| Physical Branch | `fabricator-physical-recipes.ts` | 7 | Study Physical Equipment discipline |
|
||||||
|
| **Total** | | **48** | |
|
||||||
|
|
||||||
|
### 2.2 Recipe Type Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface FabricatorRecipe {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
manaType: string; // Mana type required (must be unlocked)
|
||||||
|
equipmentTypeId: string; // Equipment type ID produced
|
||||||
|
slot: EquipmentSlot; // Slot the equipment occupies
|
||||||
|
materials: Record<string, number>; // materialId -> count required
|
||||||
|
manaCost: number; // Mana cost in the recipe's mana type
|
||||||
|
craftTime: number; // Craft time in hours
|
||||||
|
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
|
||||||
|
gearTrait: string; // Flavor text for gear properties
|
||||||
|
bonusEnchantments?: AppliedEnchantment[]; // Pre-applied enchantments
|
||||||
|
recipeType?: 'equipment' | 'material';
|
||||||
|
resultMaterial?: string; // For material recipes: material ID produced
|
||||||
|
resultAmount?: number; // For material recipes: how many are produced
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Material Recipes
|
||||||
|
|
||||||
|
### 3.1 Tier 1: Basic Materials
|
||||||
|
|
||||||
|
| ID | Name | Mana Type | Mana Cost | Input | Output | Time |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `manaCrystal` | Mana Crystal | raw | 500 | — | 1× manaCrystal | 1h |
|
||||||
|
| `manaCrystalDustCraft` | Mana Crystal Dust | raw | 10 | 1× manaCrystal | 2× manaCrystalDust | 1h |
|
||||||
|
|
||||||
|
### 3.2 Tier 2: Elemental Crystals
|
||||||
|
|
||||||
|
All cost 100 of the respective element mana, take 1 hour, produce 1 crystal.
|
||||||
|
|
||||||
|
| ID | Mana Type | Element |
|
||||||
|
|---|---|---|
|
||||||
|
| `fireCrystal` | fire | Fire |
|
||||||
|
| `waterCrystal` | water | Water |
|
||||||
|
| `airCrystal` | air | Air |
|
||||||
|
| `earthCrystal` | earth | Earth |
|
||||||
|
| `lightCrystal` | light | Light |
|
||||||
|
| `darkCrystal` | dark | Dark |
|
||||||
|
| `metalCrystal` | metal | Metal |
|
||||||
|
| `crystalCrystal` | crystal | Crystal |
|
||||||
|
|
||||||
|
### 3.3 Tier 3: Shards and Cores
|
||||||
|
|
||||||
|
| ID | Mana Type | Mana Cost | Input | Output | Time |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `earthShardCraft` | earth | 50 | 1× earthCrystal | 1× earthShard | 1h |
|
||||||
|
| `elementalCore` | raw | 100 | 10× manaCrystal | 1× elementalCore | 10h |
|
||||||
|
|
||||||
|
### 3.4 Tier 4: Advanced Materials
|
||||||
|
|
||||||
|
| ID | Mana Type | Mana Cost | Input | Output | Time |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `aetherWeave` | air | 500 | 3× airCrystal, 3× lightCrystal, 2× elementalCore | 1× aetherWeave | 12h |
|
||||||
|
| `voidCloth` | dark | 500 | 3× airCrystal, 3× darkCrystal, 2× voidEssence | 1× voidCloth | 12h |
|
||||||
|
| `liquidCrystalLattice` | crystal | 800 | 5× crystalCrystal, 3× elementalCore, 2× voidEssence, 1× celestialFragment | 1× liquidCrystalLattice | 20h |
|
||||||
|
|
||||||
|
### 3.5 Material Dependency Chain
|
||||||
|
|
||||||
|
```
|
||||||
|
Raw Mana (500) → Mana Crystal (1)
|
||||||
|
Mana Crystal (1) + Raw Mana (10) → Mana Crystal Dust (2)
|
||||||
|
Mana Crystal (1) + Element Mana (100) → Element Crystal (1) [per element]
|
||||||
|
Element Crystal (1) + Element Mana (50) → Element Shard (1) [earth only]
|
||||||
|
Mana Crystal (10) + Raw Mana (100) → Elemental Core (1) [10hr]
|
||||||
|
Air Crystal (3) + Light Crystal (3) + Elemental Core (2) → Aether Weave (1) [12hr]
|
||||||
|
Air Crystal (3) + Dark Crystal (3) + Void Essence (2) → Void Cloth (1) [12hr]
|
||||||
|
Crystal Crystal (5) + Elemental Core (3) + Void Essence (2) + Celestial Fragment (1) → Liquid Crystal Lattice (1) [20hr]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Equipment Recipes
|
||||||
|
|
||||||
|
### 4.1 Earth Gear (Unlock: Study Fabricator Recipes @ 50 XP)
|
||||||
|
|
||||||
|
| ID | Name | Slot | Mana Cost | Materials | Rarity | Time |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `earthHelm` | Earthen Helm | head | 200 earth | 4× manaCrystalDust, 2× earthShard | uncommon | 3h |
|
||||||
|
| `earthChest` | Stoneguard Armor | body | 500 earth | 8× manaCrystalDust, 4× earthShard, 1× elementalCore | rare | 6h |
|
||||||
|
| `earthBoots` | Stonegreaves | feet | 150 earth | 3× manaCrystalDust, 1× earthShard | uncommon | 2h |
|
||||||
|
|
||||||
|
### 4.2 Metal Gear (Unlock: Study Fabricator Recipes @ 100 XP)
|
||||||
|
|
||||||
|
| ID | Name | Slot | Mana Cost | Materials | Rarity | Time |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `metalBlade` | Metal Blade | mainHand | 400 metal | 6× manaCrystalDust, 3× metalShard, 2× elementalCore | rare | 5h |
|
||||||
|
| `metalShield` | Metal Spell Focus | offHand | 450 metal | 7× manaCrystalDust, 4× metalShard, 1× elementalCore | rare | 5h |
|
||||||
|
| `metalGloves` | Metalweave Gauntlets | hands | 250 metal | 4× manaCrystalDust, 2× metalShard | uncommon | 3h |
|
||||||
|
|
||||||
|
### 4.3 Sand Gear (Unlock: Study Fabricator Recipes @ 150 XP)
|
||||||
|
|
||||||
|
| ID | Name | Slot | Mana Cost | Materials | Rarity | Time |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `sandBoots` | Sandstrider Boots | feet | 120 sand | 3× manaCrystalDust, 1× sandShard | uncommon | 2h |
|
||||||
|
| `sandGloves` | Sandweave Gloves | hands | 140 sand | 3× manaCrystalDust, 2× sandShard | uncommon | 2h |
|
||||||
|
| `sandVest` | Sandcloth Vest | body | 300 sand | 5× manaCrystalDust, 2× sandShard, 1× elementalCore | rare | 4h |
|
||||||
|
|
||||||
|
### 4.4 Crystal Gear (Unlock: Study Fabricator Recipes @ 200 XP)
|
||||||
|
|
||||||
|
| ID | Name | Slot | Mana Cost | Materials | Rarity | Time |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `crystalWand` | Crystal Focus Wand | mainHand | 600 crystal | 10× manaCrystalDust, 5× crystalShard, 3× elementalCore | epic | 6h |
|
||||||
|
| `crystalRing` | Crystal Ring | accessory1 | 350 crystal | 5× manaCrystalDust, 3× crystalShard, 1× elementalCore | rare | 3h |
|
||||||
|
| `crystalAmulet` | Crystal Pendant | accessory2 | 400 crystal | 6× manaCrystalDust, 3× crystalShard, 2× elementalCore | rare | 4h |
|
||||||
|
|
||||||
|
### 4.5 Wizard Branch (Unlock: Study Wizard Equipment discipline)
|
||||||
|
|
||||||
|
| ID | Name | Slot | Unlock (XP) | Mana Cost | Materials | Rarity | Time |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| `oakStaff` | Oak Staff | mainHand | 50 | 200 earth | 5× manaCrystalDust, 2× earthShard | uncommon | 3h |
|
||||||
|
| `arcanistStaff` | Arcanist Staff | mainHand | 100 | 700 crystal | 12× manaCrystalDust, 6× crystalShard, 3× elementalCore | epic | 8h |
|
||||||
|
| `battlestaff` | Battlestaff | mainHand | 150 | 500 metal | 8× manaCrystalDust, 4× metalShard, 2× elementalCore | rare | 6h |
|
||||||
|
| `arcanistCirclet` | Arcanist Circlet | head | 150 | 300 crystal | 6× manaCrystalDust, 2× crystalShard, 1× lightCrystal | rare | 4h |
|
||||||
|
| `arcanistRobe` | Arcanist Robe | body | 150 | 800 crystal | 14× manaCrystalDust, 7× crystalShard, 3× elementalCore | epic | 8h |
|
||||||
|
| `voidCatalyst` | Void Catalyst | mainHand | 200 | 600 crystal | 10× manaCrystalDust, 3× darkCrystal, 2× voidEssence, 2× elementalCore | epic | 7h |
|
||||||
|
| `arcanistPendant` | Arcanist Pendant | accessory1 | 250 | 500 crystal | 8× manaCrystalDust, 4× crystalShard, 2× elementalCore | epic | 5h |
|
||||||
|
|
||||||
|
**Advanced Wizard Gear:**
|
||||||
|
|
||||||
|
| ID | Name | Slot | Mana Cost | Materials | Rarity | Time |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `aetherRobe` | Aetherweave Robe | body | 1200 crystal | 3× aetherWeave, 15× manaCrystalDust, 8× crystalShard, 4× elementalCore | legendary | 15h |
|
||||||
|
| `aetherCirclet` | Aetherweave Circlet | head | 900 crystal | 2× aetherWeave, 10× manaCrystalDust, 3× lightCrystal, 3× elementalCore | epic | 10h |
|
||||||
|
| `voidRobe` | Voidweave Robe | body | 1200 sand | 3× voidCloth, 15× manaCrystalDust, 8× crystalShard, 3× voidEssence | legendary | 15h |
|
||||||
|
| `voidCowl` | Voidweave Cowl | head | 900 sand | 2× voidCloth, 10× manaCrystalDust, 3× darkCrystal, 2× voidEssence | epic | 10h |
|
||||||
|
| `latticeStaff` | Crystal Lattice Staff | mainHand | 2000 crystal | 2× liquidCrystalLattice, 2× aetherWeave, 2× voidCloth, 5× elementalCore | legendary | 25h |
|
||||||
|
| `latticeAmulet` | Crystal Lattice Amulet | accessory1 | 1500 crystal | 1× liquidCrystalLattice, 5× crystalCrystal, 4× elementalCore, 2× voidEssence | legendary | 18h |
|
||||||
|
|
||||||
|
### 4.6 Physical Branch (Unlock: Study Physical Equipment discipline)
|
||||||
|
|
||||||
|
| ID | Name | Slot | Unlock (XP) | Mana Cost | Materials | Rarity | Time |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| `crystalBlade` | Crystal Blade | mainHand | 50 | 500 crystal | 8× manaCrystalDust, 4× crystalShard, 2× elementalCore | rare | 5h |
|
||||||
|
| `arcanistBlade` | Arcanist Blade | mainHand | 100 | 600 metal | 10× manaCrystalDust, 5× metalShard, 3× elementalCore | epic | 7h |
|
||||||
|
| `voidBlade` | Void-Touched Blade | mainHand | 150 | 550 crystal | 9× manaCrystalDust, 3× darkCrystal, 2× voidEssence, 2× elementalCore | epic | 6h |
|
||||||
|
| `battleHelm` | Battle Helm | head | 200 | 350 metal | 6× manaCrystalDust, 3× metalShard, 1× elementalCore | rare | 4h |
|
||||||
|
| `battleRobe` | Battle Robe | body | 200 | 400 sand | 8× manaCrystalDust, 3× sandShard, 2× elementalCore | rare | 5h |
|
||||||
|
| `battleBoots` | Battle Boots | feet | 250 | 180 sand | 4× manaCrystalDust, 2× sandShard | uncommon | 3h |
|
||||||
|
| `combatGauntlets` | Combat Gauntlets | hands | 300 | 300 metal | 5× manaCrystalDust, 2× metalShard, 1× elementalCore | uncommon | 3h |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Recipe Unlock Gates
|
||||||
|
|
||||||
|
### 5.1 Study Fabricator Recipes Discipline
|
||||||
|
|
||||||
|
| XP Threshold | Recipes Unlocked |
|
||||||
|
|---|---|
|
||||||
|
| 50 | Earth gear (helm, chest, boots) |
|
||||||
|
| 100 | Metal gear (blade, shield, gloves) |
|
||||||
|
| 150 | Sand gear (boots, gloves, vest) |
|
||||||
|
| 200 | Crystal gear (wand, ring, amulet) |
|
||||||
|
|
||||||
|
### 5.2 Study Wizard Equipment Discipline
|
||||||
|
|
||||||
|
| XP Threshold | Recipes Unlocked |
|
||||||
|
|---|---|
|
||||||
|
| 50 | Oak Staff |
|
||||||
|
| 100 | Arcanist Staff |
|
||||||
|
| 150 | Battlestaff, Arcanist Circlet, Arcanist Robe |
|
||||||
|
| 200 | Void Catalyst |
|
||||||
|
| 250 | Arcanist Pendant |
|
||||||
|
| 300 | (advanced recipes via material availability) |
|
||||||
|
|
||||||
|
### 5.3 Study Physical Equipment Discipline
|
||||||
|
|
||||||
|
| XP Threshold | Recipes Unlocked |
|
||||||
|
|---|---|
|
||||||
|
| 50 | Crystal Blade |
|
||||||
|
| 100 | Arcanist Blade |
|
||||||
|
| 150 | Void Blade |
|
||||||
|
| 200 | Battle Helm, Battle Robe |
|
||||||
|
| 250 | Battle Boots |
|
||||||
|
| 300 | Combat Gauntlets |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Crafting Flow
|
||||||
|
|
||||||
|
### 6.1 Pre-Craft Checks
|
||||||
|
|
||||||
|
```
|
||||||
|
checkFabricatorCosts(recipe, materials, rawMana, elements):
|
||||||
|
- Verify all material counts are sufficient
|
||||||
|
- Verify mana (raw or elemental) is sufficient
|
||||||
|
- Return { canCraft, missingMana, missingMaterials }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Crafting Execution
|
||||||
|
|
||||||
|
```
|
||||||
|
executeMaterialCraft(recipe, materials):
|
||||||
|
1. Deduct mana cost from raw or elemental pool
|
||||||
|
2. Deduct input materials from inventory
|
||||||
|
3. Add resultAmount of resultMaterial to inventory
|
||||||
|
|
||||||
|
makeFabricatorProgress(recipeId, equipmentTypeId, craftTime, manaCost):
|
||||||
|
1. Create EquipmentCraftingProgress object
|
||||||
|
2. blueprintId = "fabricator-{recipeId}"
|
||||||
|
3. Progress accumulates at HOURS_PER_TICK per tick
|
||||||
|
4. On completion: create equipment instance with bonusEnchantments
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 Cancellation Refund
|
||||||
|
|
||||||
|
```
|
||||||
|
remainingFraction = (required - progress) / required
|
||||||
|
refundRate = remainingFraction + (1 - remainingFraction) × 0.5
|
||||||
|
manaRefund = floor(manaSpent × refundRate)
|
||||||
|
materialRefund = floor(materialsSpent × 0.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Crafting Efficiency Discipline Interaction
|
||||||
|
|
||||||
|
The **Crafting Efficiency** discipline provides:
|
||||||
|
|
||||||
|
| Source | Effect |
|
||||||
|
|---|---|
|
||||||
|
| Base stat bonus | `craftingCostReduction` +15 |
|
||||||
|
| Perk `efficiency-1` (once @ 300 XP) | +10% Crafting Cost Reduction |
|
||||||
|
|
||||||
|
The `craftingCostReduction` stat reduces material costs for all fabrication recipes.
|
||||||
|
Applied as: `actualCost = baseCost × (1 - craftingCostReduction / 100)`.
|
||||||
|
|
||||||
|
At maximum: 15 (base) + 10 (perk) = **25% cost reduction**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. How Fabricated Items Differ from Base Loot
|
||||||
|
|
||||||
|
| Property | Loot Drops | Fabricated Items |
|
||||||
|
|---|---|---|
|
||||||
|
| **Source** | Enemy drops, treasure rooms | Crafting recipes |
|
||||||
|
| **Enchantments** | None (must be enchanted) | Pre-applied `bonusEnchantments` |
|
||||||
|
| **Rarity** | Random (common–legendary) | Fixed per recipe |
|
||||||
|
| **Quality** | Random (0–100) | Fixed per recipe |
|
||||||
|
| **Stats** | Base for type | Base for type + enchantment bonuses |
|
||||||
|
| **Control** | None (random) | Full (player chooses recipe) |
|
||||||
|
|
||||||
|
Fabricated items are created with `bonusEnchantments` — pre-applied enchantment
|
||||||
|
objects with `effectId`, `stacks`, and `actualCost`. These enchantments are
|
||||||
|
permanent and cannot be removed without the Enchanter's disenchant process.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Equipment Types Producible via Fabrication
|
||||||
|
|
||||||
|
| Slot | Equipment Types |
|
||||||
|
|---|---|
|
||||||
|
| mainHand | Metal Blade, Crystal Focus Wand, Oak Staff, Arcanist Staff, Battlestaff, Void Catalyst, Crystal Lattice Staff |
|
||||||
|
| offHand | Metal Spell Focus |
|
||||||
|
| head | Earthen Helm, Arcanist Circlet, Aetherweave Circlet, Voidweave Cowl, Battle Helm |
|
||||||
|
| body | Stoneguard Armor, Sandcloth Vest, Arcanist Robe, Aetherweave Robe, Voidweave Robe, Battle Robe |
|
||||||
|
| hands | Metalweave Gauntlets, Sandweave Gloves, Combat Gauntlets |
|
||||||
|
| feet | Stonegreaves, Sandstrider Boots, Battle Boots |
|
||||||
|
| accessory1 | Crystal Ring, Arcanist Pendant, Crystal Lattice Amulet |
|
||||||
|
| accessory2 | Crystal Pendant |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Rarity Distribution
|
||||||
|
|
||||||
|
| Rarity | Count | Examples |
|
||||||
|
|---|---|---|
|
||||||
|
| common | 2 | Mana Crystal Dust, Earth Shard |
|
||||||
|
| uncommon | 14 | Earth gear, Sand gear, Oak Staff, Battle Boots, Combat Gauntlets, Mana Crystal |
|
||||||
|
| rare | 14 | Earth Chest, Metal gear, Crystal Ring/Amulet, Sand Vest, Crystal Blade, Battle Helm/Robe |
|
||||||
|
| epic | 10 | Crystal Wand, Arcanist Staff/Robe, Void Blade/Catalyst, Arcanist Pendant, Aether Circlet, Void Cowl |
|
||||||
|
| legendary | 5 | Aether Robe, Void Robe, Lattice Staff, Lattice Amulet, Liquid Crystal Lattice |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Acceptance Criteria
|
||||||
|
|
||||||
|
| # | Criterion |
|
||||||
|
|---|---|
|
||||||
|
| AC-1 | All 48 recipes are accessible when the Fabricator attunement is active. |
|
||||||
|
| AC-2 | Recipe unlock gates fire at the correct discipline XP thresholds. |
|
||||||
|
| AC-3 | Material crafting correctly consumes mana and input materials, producing the correct output. |
|
||||||
|
| AC-4 | Equipment crafting produces items with the correct pre-applied enchantments. |
|
||||||
|
| AC-5 | Crafting Efficiency discipline reduces material costs by the correct percentage. |
|
||||||
|
| AC-6 | Cancellation refunds mana at the blended rate (100% unspent, 50% spent) and materials at 50%. |
|
||||||
|
| AC-7 | Fabricated items cannot be crafted without the required mana type unlocked. |
|
||||||
|
| AC-8 | Material dependency chain is correct: Mana Crystal → Element Crystal → Elemental Core → Advanced Materials. |
|
||||||
|
| AC-9 | Craft time ranges from 1h (basic materials) to 25h (Crystal Lattice Staff). |
|
||||||
|
| AC-10 | Mana cost ranges from 10 (Mana Crystal Dust) to 2000 (Crystal Lattice Staff). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Files Reference
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `src/lib/game/data/fabricator-material-recipes.ts` | Material recipes (15) |
|
||||||
|
| `src/lib/game/data/fabricator-recipes.ts` | Core equipment recipes (12) |
|
||||||
|
| `src/lib/game/data/fabricator-wizard-recipes.ts` | Wizard branch recipes (14) |
|
||||||
|
| `src/lib/game/data/fabricator-physical-recipes.ts` | Physical branch recipes (7) |
|
||||||
|
| `src/lib/game/data/fabricator-recipe-types.ts` | Recipe type definitions |
|
||||||
|
| `src/lib/game/crafting-fabricator.ts` | Fabrication crafting logic |
|
||||||
|
| `src/lib/game/data/disciplines/fabricator.ts` | Fabricator disciplines (5) |
|
||||||
|
| `src/components/game/tabs/CraftingTab.tsx` | Crafting tab wrapper |
|
||||||
|
| `src/components/game/tabs/CraftingTab/FabricatorSubTab.tsx` | Fabricator crafting UI |
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# Invoker Attunement — Design Spec
|
||||||
|
|
||||||
|
> Describes the Invoker attunement: identity, unlock flow, mana behavior, full
|
||||||
|
> discipline list with stats/perks, systems unlocked, pact interactions, and
|
||||||
|
> attunement level interactions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objective
|
||||||
|
|
||||||
|
The Invoker is the pact-focused attunement that transforms Guardian defeats into
|
||||||
|
permanent power. Unlike the other attunements, the Invoker has no primary mana type
|
||||||
|
and no automatic mana conversion — it gains elemental mana exclusively by signing
|
||||||
|
pacts with Guardians. Its disciplines amplify pact power, boon effectiveness, and
|
||||||
|
guardian-related multipliers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Identity
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **ID** | `invoker` |
|
||||||
|
| **Slot** | `chest` |
|
||||||
|
| **Icon** | `💜` |
|
||||||
|
| **Color** | `#9B59B6` (Purple) |
|
||||||
|
| **Primary Mana** | None (gains elemental mana from pacts) |
|
||||||
|
| **Raw Mana Regen** | +0.3/hour (base, scales with `1.5^(level-1)`) |
|
||||||
|
| **Conversion Rate** | None (0 at all levels) |
|
||||||
|
| **Unlock** | Defeat first Guardian |
|
||||||
|
| **Capabilities** | `['pacts', 'guardianPowers', 'elementalMastery']` |
|
||||||
|
| **Skill Categories** | `['invocation', 'pact']` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Unlock Condition and Flow
|
||||||
|
|
||||||
|
**Condition:** Defeat the first Guardian (floor 10).
|
||||||
|
|
||||||
|
**Unlock flow:**
|
||||||
|
1. Defeat the floor 10 Guardian (Ignis Prime)
|
||||||
|
2. Invoker becomes available for activation
|
||||||
|
3. Player activates Invoker → initialized at `{ active: true, level: 1, experience: 0 }`
|
||||||
|
4. Invoker disciplines become available: `pact-attunement`, `guardians-boon`
|
||||||
|
|
||||||
|
The unlock condition is stored as a descriptive string:
|
||||||
|
`"Defeat your first guardian and choose the path of the Invoker"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Raw Mana Regen Contribution
|
||||||
|
|
||||||
|
Base regen: **+0.3/hour** (at level 1). Scales exponentially:
|
||||||
|
|
||||||
|
```
|
||||||
|
effectiveRegen = 0.3 × 1.5^(level - 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Level | Raw Regen |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 0.300/hr |
|
||||||
|
| 5 | 1.519/hr |
|
||||||
|
| 10 | 11.533/hr |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Mana Gain from Pacts (No Conversion)
|
||||||
|
|
||||||
|
The Invoker has **no automatic mana conversion**. Instead, it gains elemental mana
|
||||||
|
types exclusively through Guardian pacts:
|
||||||
|
|
||||||
|
When a pact is signed (`completePactRitual`):
|
||||||
|
```typescript
|
||||||
|
for (const manaType of guardian.unlocksMana || []) {
|
||||||
|
manaStore.unlockElement(manaType, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each guardian's `unlocksMana` is resolved via `resolveMultiUnlockChain(element)`,
|
||||||
|
which walks the element recipe tree to unlock the guardian's element and all base
|
||||||
|
components:
|
||||||
|
|
||||||
|
| Guardian | Element | Unlocks Mana Types |
|
||||||
|
|---|---|---|
|
||||||
|
| Floor 10 (Ignis Prime) | fire | `fire` |
|
||||||
|
| Floor 20 (Aqua Regia) | water | `water` |
|
||||||
|
| Floor 40 (Terra Firma) | earth | `earth` |
|
||||||
|
| Floor 90 (Metal) | metal | `fire`, `earth`, `metal` |
|
||||||
|
| Floor 130 (BlackFlame) | blackflame | `fire`, `earth`, `metal` |
|
||||||
|
| Floor 150 (Lightning) | lightning | `fire`, `air`, `lightning` |
|
||||||
|
|
||||||
|
Signing pacts is the **only** way for the Invoker to access elemental mana for
|
||||||
|
casting elemental spells and running elemental disciplines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Disciplines
|
||||||
|
|
||||||
|
The Invoker's discipline pool contains **2 disciplines**.
|
||||||
|
|
||||||
|
### 6.1 Pact Attunement (`pact-attunement`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `raw` |
|
||||||
|
| **Base Cost** | 12 |
|
||||||
|
| **Requires** | `['signed_pact']` |
|
||||||
|
| **Stat Bonus** | `pactAffinityBonus` +0.05 (base) |
|
||||||
|
| **Scaling Factor** | 80 |
|
||||||
|
| **Difficulty Factor** | 150 |
|
||||||
|
| **Drain Base** | 4 |
|
||||||
|
|
||||||
|
**Perks:**
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Bonus |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `pact-affinity-scaling` | `once` | 100 | Unlock pact affinity scaling |
|
||||||
|
| `pact-affinity-infinite` | `infinite` | 200 | Every 100 XP: `pactAffinityBonus` +0.05 |
|
||||||
|
| `pact-power-boost` | `capped` | 500 | Every 200 XP: `guardianBoonMultiplier` +0.03, max 5 tiers |
|
||||||
|
|
||||||
|
### 6.2 Guardian's Boon (`guardians-boon`)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Mana Type** | `raw` |
|
||||||
|
| **Base Cost** | 18 |
|
||||||
|
| **Requires** | `['signed_pact']` |
|
||||||
|
| **Stat Bonus** | `guardianBoonMultiplier` +0.10 (base) |
|
||||||
|
| **Scaling Factor** | 100 |
|
||||||
|
| **Difficulty Factor** | 200 |
|
||||||
|
| **Drain Base** | 6 |
|
||||||
|
|
||||||
|
**Perks:**
|
||||||
|
|
||||||
|
| Perk ID | Type | Threshold | Bonus |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `boon-1` | `once` | 100 | `guardianBoonMultiplier` +0.10 |
|
||||||
|
| `boon-2` | `capped` | 200 | Every 350 XP: `guardianBoonMultiplier` +0.05, max 5 tiers |
|
||||||
|
|
||||||
|
### 6.3 Guardian Boon Multiplier Scaling
|
||||||
|
|
||||||
|
Maximum theoretical `guardianBoonMultiplier` from disciplines:
|
||||||
|
|
||||||
|
| Source | Value |
|
||||||
|
|---|---|
|
||||||
|
| Base (Guardian's Boon discipline) | +0.10 |
|
||||||
|
| `boon-1` perk (once @ 100 XP) | +0.10 |
|
||||||
|
| `boon-2` perk (capped, 5 tiers × 0.05) | +0.25 |
|
||||||
|
| `pact-power-boost` perk (capped, 5 tiers × 0.03) | +0.15 |
|
||||||
|
| **Maximum total** | **+0.60** |
|
||||||
|
|
||||||
|
With the base multiplier of 1.0, the maximum guardian boon multiplier is **1.60**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Systems Unlocked
|
||||||
|
|
||||||
|
The Invoker attunement gates the **Pact System** (see `pact-system-spec.md`):
|
||||||
|
|
||||||
|
- Sign pacts with defeated Guardians
|
||||||
|
- Gain permanent boons and elemental mana unlocks
|
||||||
|
- Pact slots limit simultaneous signed pacts
|
||||||
|
- Pact affinity reduces ritual time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Puzzle Room Behavior
|
||||||
|
|
||||||
|
In the spire, every 7th floor has a puzzle room. When the room type is
|
||||||
|
`invoker_trial`, progress scales at 2.5–3% per tick per Invoker level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Attunement Level Interactions
|
||||||
|
|
||||||
|
Higher Invoker level affects:
|
||||||
|
|
||||||
|
1. **Raw mana regen**: `0.3 × 1.5^(level-1)` per hour
|
||||||
|
2. **No conversion**: Invoker never has automatic mana conversion
|
||||||
|
3. **Pact affinity**: Higher raw regen supports the raw mana cost of pact rituals
|
||||||
|
|
||||||
|
Attunement level does **not** directly affect pact multipliers or boon power —
|
||||||
|
those scale through discipline XP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Known Code Issues
|
||||||
|
|
||||||
|
The following inconsistencies exist in the codebase:
|
||||||
|
|
||||||
|
| Issue | Description |
|
||||||
|
|---|---|
|
||||||
|
| `pactBinding` upgrade | Referenced in `prestigeStore.doPrestige` but **not defined** in `PRESTIGE_DEF` constants |
|
||||||
|
| UI vs store mismatch | UI displays `prestigeUpgrades.pactCapacity` but store logic checks `pactBinding` |
|
||||||
|
| Pact persistence | `signedPacts` is persisted but also reset to `[]` on `startNewLoop` — pacts don't survive loops in current implementation |
|
||||||
|
| `pactInterferenceMitigation` | Used in `pact-utils.ts` but no prestige upgrade defines it |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Acceptance Criteria
|
||||||
|
|
||||||
|
| # | Criterion |
|
||||||
|
|---|---|
|
||||||
|
| AC-1 | Invoker is locked until the first Guardian is defeated. |
|
||||||
|
| AC-2 | Invoker has no primary mana type and no automatic conversion at any level. |
|
||||||
|
| AC-3 | Signing a pact unlocks the guardian's element and all component elements. |
|
||||||
|
| AC-4 | Both Invoker disciplines require at least one signed pact to activate. |
|
||||||
|
| AC-5 | `pact-affinity-infinite` perk grants +0.05 pactAffinityBonus every 100 XP beyond threshold 200. |
|
||||||
|
| AC-6 | `boon-2` capped perk grants +0.05 guardianBoonMultiplier per tier, max 5 tiers, interval 350 XP. |
|
||||||
|
| AC-7 | `pact-power-boost` capped perk grants +0.03 guardianBoonMultiplier per tier, max 5 tiers, interval 200 XP. |
|
||||||
|
| AC-8 | Maximum theoretical guardianBoonMultiplier from disciplines is 1.60 (base 1.0 + 0.60). |
|
||||||
|
| AC-9 | Invoker `invoker_trial` puzzle rooms grant bonus progress per Invoker level. |
|
||||||
|
| AC-10 | Invoker level scales raw regen by `1.5^(level-1)`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Files Reference
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `src/lib/game/data/attunements.ts` | Invoker definition |
|
||||||
|
| `src/lib/game/data/disciplines/invoker.ts` | Invoker disciplines (2) |
|
||||||
|
| `src/lib/game/stores/prestigeStore.ts` | Pact ritual state, slot management |
|
||||||
|
| `src/lib/game/stores/pipelines/pact-ritual.ts` | Pact ritual tick processing |
|
||||||
|
| `src/lib/game/utils/pact-utils.ts` | Pact multiplier calculations |
|
||||||
|
| `src/lib/game/data/guardian-data.ts` | Static guardian definitions |
|
||||||
|
| `src/lib/game/data/guardian-encounters.ts` | Procedural guardian lookup |
|
||||||
|
| `src/components/game/tabs/GuardianPactsTab.tsx` | Pact signing UI |
|
||||||
|
| `docs/specs/attunements/invoker/systems/pact-system-spec.md` | Pact system spec |
|
||||||
@@ -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 |
|
||||||
@@ -75,6 +75,7 @@ function runCombatTick(rawMana: number, elements: Record<string, { current: numb
|
|||||||
[], // signedPacts
|
[], // signedPacts
|
||||||
{ activeGolems: [] }, // golemancyState
|
{ activeGolems: [] }, // golemancyState
|
||||||
golemApplyDamageToRoom,
|
golemApplyDamageToRoom,
|
||||||
|
(dmg: number) => dmg, // applyEnemyDefenses (passthrough for tests)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ function processCombatTickDirect(
|
|||||||
onFloorCleared, onDamageDealt, signedPacts,
|
onFloorCleared, onDamageDealt, signedPacts,
|
||||||
{ activeGolems: [] },
|
{ activeGolems: [] },
|
||||||
golemApplyDamageToRoom,
|
golemApplyDamageToRoom,
|
||||||
|
(dmg: number) => dmg, // applyEnemyDefenses (passthrough for tests)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function resetAllStores() {
|
|||||||
roomResetState: {},
|
roomResetState: {},
|
||||||
clearedRooms: {},
|
clearedRooms: {},
|
||||||
isDescentComplete: false,
|
isDescentComplete: false,
|
||||||
golemancy: { enabledGolems: [], summonedGolems: [], lastSummonFloor: 0 },
|
golemancy: { enabledGolems: [], summonedGolems: [], activeGolems: [], lastSummonFloor: 0 },
|
||||||
equipmentSpellStates: [],
|
equipmentSpellStates: [],
|
||||||
comboHitCount: 0,
|
comboHitCount: 0,
|
||||||
floorHitCount: 0,
|
floorHitCount: 0,
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ function makeEnemy(overrides: Partial<EnemyState> = {}): EnemyState {
|
|||||||
armor: 0,
|
armor: 0,
|
||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
element: 'fire',
|
element: 'fire',
|
||||||
|
activeEffects: [],
|
||||||
|
effectiveArmor: 0,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -197,18 +199,17 @@ describe('Enemy Defenses (spec §5.2)', () => {
|
|||||||
|
|
||||||
describe('effectiveArmor (post-corrode)', () => {
|
describe('effectiveArmor (post-corrode)', () => {
|
||||||
it('should use effectiveArmor over base armor when set', () => {
|
it('should use effectiveArmor over base armor when set', () => {
|
||||||
const enemy = makeEnemy({ armor: 0.4 }) as EnemyState & { effectiveArmor?: number };
|
const enemy = makeEnemy({ armor: 0.4, effectiveArmor: 0.2 }); // Armor reduced by corrode
|
||||||
enemy.effectiveArmor = 0.2; // Armor reduced by corrode
|
|
||||||
|
|
||||||
// The defense pipeline uses effectiveArmor ?? armor
|
// The defense pipeline uses effectiveArmor (after corrode)
|
||||||
const armorValue = enemy.effectiveArmor ?? enemy.armor;
|
const armorValue = enemy.effectiveArmor;
|
||||||
expect(armorValue).toBe(0.2);
|
expect(armorValue).toBe(0.2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fall back to base armor when effectiveArmor is not set', () => {
|
it('should have effectiveArmor equal to base armor when no corrode applied', () => {
|
||||||
const enemy = makeEnemy({ armor: 0.4 });
|
const enemy = makeEnemy({ armor: 0.4, effectiveArmor: 0.4 });
|
||||||
|
|
||||||
const armorValue = (enemy as EnemyState & { effectiveArmor?: number }).effectiveArmor ?? enemy.armor;
|
const armorValue = enemy.effectiveArmor;
|
||||||
expect(armorValue).toBe(0.4);
|
expect(armorValue).toBe(0.4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -219,6 +220,7 @@ describe('Enemy Defenses (spec §5.2)', () => {
|
|||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
barrier: 0.5,
|
barrier: 0.5,
|
||||||
armor: 0.3,
|
armor: 0.3,
|
||||||
|
effectiveArmor: 0.3,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate: 100 damage → barrier (50%) → 50 → armor (30%) → 35
|
// Simulate: 100 damage → barrier (50%) → 50 → armor (30%) → 35
|
||||||
@@ -229,10 +231,11 @@ describe('Enemy Defenses (spec §5.2)', () => {
|
|||||||
}
|
}
|
||||||
expect(dmg).toBe(50);
|
expect(dmg).toBe(50);
|
||||||
|
|
||||||
const armorValue = (enemy as EnemyState & { effectiveArmor?: number }).effectiveArmor ?? enemy.armor;
|
const armorValue = enemy.effectiveArmor;
|
||||||
if (armorValue && armorValue > 0) {
|
if (armorValue && armorValue > 0) {
|
||||||
dmg *= (1 - armorValue);
|
dmg *= (1 - armorValue);
|
||||||
}
|
}
|
||||||
|
// 100 → barrier (50%) → 50 → armor (30%) → 35
|
||||||
expect(dmg).toBe(35);
|
expect(dmg).toBe(35);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export const BASIC_ELEMENTAL_SPELLS: Record<string, SpellDef> = {
|
|||||||
castSpeed: 2,
|
castSpeed: 2,
|
||||||
unlock: 100,
|
unlock: 100,
|
||||||
studyTime: 2,
|
studyTime: 2,
|
||||||
desc: "Hurl a ball of fire at your enemy."
|
desc: "Hurl a ball of fire at your enemy.",
|
||||||
|
onHitEffect: { type: 'burn', duration: 4, magnitude: 3 },
|
||||||
},
|
},
|
||||||
emberShot: {
|
emberShot: {
|
||||||
name: "Ember Shot",
|
name: "Ember Shot",
|
||||||
@@ -24,7 +25,8 @@ export const BASIC_ELEMENTAL_SPELLS: Record<string, SpellDef> = {
|
|||||||
castSpeed: 3,
|
castSpeed: 3,
|
||||||
unlock: 75,
|
unlock: 75,
|
||||||
studyTime: 1,
|
studyTime: 1,
|
||||||
desc: "A quick shot of embers. Efficient fire damage."
|
desc: "A quick shot of embers. Efficient fire damage.",
|
||||||
|
onHitEffect: { type: 'burn', duration: 3, magnitude: 2 },
|
||||||
},
|
},
|
||||||
waterJet: {
|
waterJet: {
|
||||||
name: "Water Jet",
|
name: "Water Jet",
|
||||||
@@ -145,7 +147,8 @@ export const BASIC_ELEMENTAL_SPELLS: Record<string, SpellDef> = {
|
|||||||
castSpeed: 2,
|
castSpeed: 2,
|
||||||
unlock: 150,
|
unlock: 150,
|
||||||
studyTime: 3,
|
studyTime: 3,
|
||||||
desc: "Siphon vital energy from your enemy."
|
desc: "Siphon vital energy from your enemy.",
|
||||||
|
onHitEffect: { type: 'curse', duration: 4, magnitude: 0.20 },
|
||||||
},
|
},
|
||||||
rotTouch: {
|
rotTouch: {
|
||||||
name: "Rot Touch",
|
name: "Rot Touch",
|
||||||
@@ -156,6 +159,7 @@ export const BASIC_ELEMENTAL_SPELLS: Record<string, SpellDef> = {
|
|||||||
castSpeed: 2,
|
castSpeed: 2,
|
||||||
unlock: 170,
|
unlock: 170,
|
||||||
studyTime: 3,
|
studyTime: 3,
|
||||||
desc: "Touch of decay and rot."
|
desc: "Touch of decay and rot.",
|
||||||
|
onHitEffect: { type: 'curse', duration: 4, magnitude: 0.20 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const FROST_SPELLS: Record<string, SpellDef> = {
|
|||||||
unlock: 180,
|
unlock: 180,
|
||||||
studyTime: 3,
|
studyTime: 3,
|
||||||
desc: "A chilling bite of frost. Fast casting with freeze chance.",
|
desc: "A chilling bite of frost. Fast casting with freeze chance.",
|
||||||
effects: [{ type: 'freeze', value: 0.15 }]
|
effects: [{ type: 'freeze', value: 0.15 }],
|
||||||
|
onHitEffect: { type: 'freeze', duration: 3, magnitude: 0.15 },
|
||||||
},
|
},
|
||||||
iceShard: {
|
iceShard: {
|
||||||
name: "Ice Shard",
|
name: "Ice Shard",
|
||||||
@@ -27,7 +28,8 @@ export const FROST_SPELLS: Record<string, SpellDef> = {
|
|||||||
unlock: 250,
|
unlock: 250,
|
||||||
studyTime: 4,
|
studyTime: 4,
|
||||||
desc: "A sharp shard of ice. Moderate speed, freeze effect.",
|
desc: "A sharp shard of ice. Moderate speed, freeze effect.",
|
||||||
effects: [{ type: 'freeze', value: 0.2 }]
|
effects: [{ type: 'freeze', value: 0.2 }],
|
||||||
|
onHitEffect: { type: 'freeze', duration: 3, magnitude: 0.2 },
|
||||||
},
|
},
|
||||||
|
|
||||||
// Tier 2 - Advanced Frost
|
// Tier 2 - Advanced Frost
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const LIGHTNING_SPELLS: Record<string, SpellDef> = {
|
|||||||
unlock: 120,
|
unlock: 120,
|
||||||
studyTime: 2,
|
studyTime: 2,
|
||||||
desc: "A quick spark of lightning. Very fast and hard to dodge.",
|
desc: "A quick spark of lightning. Very fast and hard to dodge.",
|
||||||
effects: [{ type: 'armor_pierce', value: 0.2 }]
|
effects: [{ type: 'armor_pierce', value: 0.2 }],
|
||||||
|
onHitEffect: { type: 'armor_corrode', duration: 3, magnitude: 0.15 },
|
||||||
},
|
},
|
||||||
lightningBolt: {
|
lightningBolt: {
|
||||||
name: "Lightning Bolt",
|
name: "Lightning Bolt",
|
||||||
@@ -27,7 +28,8 @@ export const LIGHTNING_SPELLS: Record<string, SpellDef> = {
|
|||||||
unlock: 150,
|
unlock: 150,
|
||||||
studyTime: 3,
|
studyTime: 3,
|
||||||
desc: "A bolt of lightning that pierces armor.",
|
desc: "A bolt of lightning that pierces armor.",
|
||||||
effects: [{ type: 'armor_pierce', value: 0.3 }]
|
effects: [{ type: 'armor_pierce', value: 0.3 }],
|
||||||
|
onHitEffect: { type: 'armor_corrode', duration: 3, magnitude: 0.15 },
|
||||||
},
|
},
|
||||||
|
|
||||||
// Tier 2 - Advanced Lightning
|
// Tier 2 - Advanced Lightning
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const SOUL_SPELLS: Record<string, SpellDef> = {
|
|||||||
unlock: 30000,
|
unlock: 30000,
|
||||||
studyTime: 36,
|
studyTime: 36,
|
||||||
desc: "Strike at the soul. Bypasses all armor and resistances.",
|
desc: "Strike at the soul. Bypasses all armor and resistances.",
|
||||||
effects: [{ type: 'defense_bypass', value: 1.0 }]
|
effects: [{ type: 'defense_bypass', value: 1.0 }],
|
||||||
|
onHitEffect: { type: 'burn', duration: 5, magnitude: 20, bypassArmor: true },
|
||||||
},
|
},
|
||||||
spiritBlast: {
|
spiritBlast: {
|
||||||
name: "Spirit Blast",
|
name: "Spirit Blast",
|
||||||
@@ -27,6 +28,7 @@ export const SOUL_SPELLS: Record<string, SpellDef> = {
|
|||||||
unlock: 60000,
|
unlock: 60000,
|
||||||
studyTime: 50,
|
studyTime: 50,
|
||||||
desc: "A blast of pure soul energy. Ignores all defenses entirely.",
|
desc: "A blast of pure soul energy. Ignores all defenses entirely.",
|
||||||
effects: [{ type: 'defense_bypass', value: 1.0 }, { type: 'resist_ignore', value: 0.5 }]
|
effects: [{ type: 'defense_bypass', value: 1.0 }, { type: 'resist_ignore', value: 0.5 }],
|
||||||
|
onHitEffect: { type: 'burn', duration: 5, magnitude: 35, bypassArmor: true },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
import { SPELLS_DEF, HOURS_PER_TICK } from '../constants';
|
import { SPELLS_DEF, HOURS_PER_TICK } from '../constants';
|
||||||
import { getGuardianForFloor } from '../data/guardian-encounters';
|
import { getGuardianForFloor } from '../data/guardian-encounters';
|
||||||
import type { CombatStore, CombatState } from './combat-state.types';
|
import type { CombatStore, CombatState } from './combat-state.types';
|
||||||
import type { SpellState } from '../types';
|
import type { SpellState, EnemyState } from '../types';
|
||||||
|
import { applyOnHitEffect, processDoTPhase } from './dot-runtime';
|
||||||
import type { ActiveGolem } from '../types';
|
import type { ActiveGolem } from '../types';
|
||||||
|
import type { SpellOnHitEffect } from '../types/spells';
|
||||||
import { getFloorMaxHP, getFloorElement, getMultiElementBonus, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
|
import { getFloorMaxHP, getFloorElement, getMultiElementBonus, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
|
||||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||||
import { processGolemMaintenance, processGolemAttacks } from './golem-combat-actions';
|
import { processGolemMaintenance, processGolemAttacks } from './golem-combat-actions';
|
||||||
@@ -65,6 +67,14 @@ export function processCombatTick(
|
|||||||
signedPacts: number[],
|
signedPacts: number[],
|
||||||
golemancyState: { activeGolems: ActiveGolem[] },
|
golemancyState: { activeGolems: ActiveGolem[] },
|
||||||
golemApplyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean },
|
golemApplyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean },
|
||||||
|
applyEnemyDefenses: (
|
||||||
|
dmg: number,
|
||||||
|
enemy: EnemyState | null,
|
||||||
|
roomType: string,
|
||||||
|
addLog: (msg: string) => void,
|
||||||
|
bypassArmor?: boolean,
|
||||||
|
bypassBarrier?: boolean,
|
||||||
|
) => number,
|
||||||
): CombatTickResult {
|
): CombatTickResult {
|
||||||
const state = get();
|
const state = get();
|
||||||
const logMessages: string[] = [];
|
const logMessages: string[] = [];
|
||||||
@@ -177,6 +187,9 @@ export function processCombatTick(
|
|||||||
castProgress -= 1;
|
castProgress -= 1;
|
||||||
safetyCounter++;
|
safetyCounter++;
|
||||||
|
|
||||||
|
// Apply on-hit effect (DoT/debuff) to enemy (spec §6.2)
|
||||||
|
applyOnHitEffect(get, set, spellId, logMessages);
|
||||||
|
|
||||||
// Check if room/floor is cleared
|
// Check if room/floor is cleared
|
||||||
if (floorHP <= 0) {
|
if (floorHP <= 0) {
|
||||||
const guardian = getGuardianForFloor(currentFloor);
|
const guardian = getGuardianForFloor(currentFloor);
|
||||||
@@ -302,6 +315,24 @@ export function processCombatTick(
|
|||||||
currentFloor = postGolemState.currentFloor;
|
currentFloor = postGolemState.currentFloor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── DoT/Debuff tick processing (spec §6.3) ──────────────────────────
|
||||||
|
// Process after all weapon/golem attacks
|
||||||
|
if (floorHP > 0) {
|
||||||
|
const doTDamage = processDoTPhase(get, set, applyEnemyDefenses, logMessages);
|
||||||
|
floorHP = Math.max(0, floorHP - doTDamage);
|
||||||
|
|
||||||
|
// Check if DoT cleared the room
|
||||||
|
if (floorHP <= 0) {
|
||||||
|
const guardian = getGuardianForFloor(currentFloor);
|
||||||
|
onFloorCleared(currentFloor, !!guardian);
|
||||||
|
get().advanceRoomOrFloor();
|
||||||
|
const newState = get();
|
||||||
|
currentFloor = newState.currentFloor;
|
||||||
|
floorMaxHP = newState.floorMaxHP;
|
||||||
|
floorHP = newState.floorHP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newMaxFloorReached = Math.max(state.maxFloorReached, currentFloor);
|
const newMaxFloorReached = Math.max(state.maxFloorReached, currentFloor);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ─── Combat State Types ────────────────────────────────────────────────────────
|
// ─── Combat State Types ────────────────────────────────────────────────────────
|
||||||
// Shared types for combat store and combat actions to avoid circular dependency
|
// Shared types for combat store and combat actions to avoid circular dependency
|
||||||
|
|
||||||
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType, ActiveGolem } from '../types';
|
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType, ActiveGolem, EnemyState } from '../types';
|
||||||
|
|
||||||
// ─── Combat State (data only) ─────────────────────────────────────────────────
|
// ─── Combat State (data only) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -145,6 +145,14 @@ export interface CombatActions {
|
|||||||
signedPacts: number[],
|
signedPacts: number[],
|
||||||
golemancyState: { activeGolems: ActiveGolem[] },
|
golemancyState: { activeGolems: ActiveGolem[] },
|
||||||
golemApplyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean },
|
golemApplyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean },
|
||||||
|
applyEnemyDefenses: (
|
||||||
|
dmg: number,
|
||||||
|
enemy: EnemyState | null,
|
||||||
|
roomType: string,
|
||||||
|
addLog: (msg: string) => void,
|
||||||
|
bypassArmor?: boolean,
|
||||||
|
bypassBarrier?: boolean,
|
||||||
|
) => number,
|
||||||
) => {
|
) => {
|
||||||
rawMana: number;
|
rawMana: number;
|
||||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import { createSafeStorage } from '../utils/safe-persist';
|
import { createSafeStorage } from '../utils/safe-persist';
|
||||||
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType, ActiveGolem } from '../types';
|
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType, ActiveGolem, EnemyState } from '../types';
|
||||||
import { getFloorMaxHP } from '../utils';
|
import { getFloorMaxHP } from '../utils';
|
||||||
import { generateFloorState } from '../utils/room-utils';
|
import { generateFloorState } from '../utils/room-utils';
|
||||||
import { addActivityLogEntry } from '../utils/activity-log';
|
import { addActivityLogEntry } from '../utils/activity-log';
|
||||||
@@ -302,6 +302,14 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
signedPacts: number[],
|
signedPacts: number[],
|
||||||
golemancyState: { activeGolems: ActiveGolem[] },
|
golemancyState: { activeGolems: ActiveGolem[] },
|
||||||
golemApplyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean },
|
golemApplyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean },
|
||||||
|
applyEnemyDefenses: (
|
||||||
|
dmg: number,
|
||||||
|
enemy: EnemyState | null,
|
||||||
|
roomType: string,
|
||||||
|
addLog: (msg: string) => void,
|
||||||
|
bypassArmor?: boolean,
|
||||||
|
bypassBarrier?: boolean,
|
||||||
|
) => number,
|
||||||
) => {
|
) => {
|
||||||
return processCombatTick(
|
return processCombatTick(
|
||||||
get,
|
get,
|
||||||
@@ -315,6 +323,7 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
signedPacts,
|
signedPacts,
|
||||||
golemancyState,
|
golemancyState,
|
||||||
golemApplyDamageToRoom,
|
golemApplyDamageToRoom,
|
||||||
|
applyEnemyDefenses,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// ─── DoT/Debuff Runtime (spec §6) ─────────────────────────────────────────────
|
||||||
|
// Handles on-hit effect application and DoT tick processing for combat.
|
||||||
|
|
||||||
|
import { SPELLS_DEF } from '../constants';
|
||||||
|
import type { CombatStore, CombatState } from './combat-state.types';
|
||||||
|
import type { ActiveEffect, EnemyState } from '../types';
|
||||||
|
import type { SpellOnHitEffect } from '../types/spells';
|
||||||
|
|
||||||
|
const DOT_EFFECT_TYPES = new Set(['burn', 'poison', 'bleed']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply an on-hit effect from a spell to the current room's primary enemy.
|
||||||
|
* Called after a successful spell hit in processCombatTick.
|
||||||
|
* spec §6.2
|
||||||
|
*/
|
||||||
|
export function applyOnHitEffect(
|
||||||
|
get: () => CombatStore,
|
||||||
|
set: (state: Partial<CombatState>) => void,
|
||||||
|
spellId: string,
|
||||||
|
logMessages: string[],
|
||||||
|
): void {
|
||||||
|
const spellDef = SPELLS_DEF[spellId];
|
||||||
|
if (!spellDef?.onHitEffect) return;
|
||||||
|
|
||||||
|
const onHit = spellDef.onHitEffect as SpellOnHitEffect;
|
||||||
|
if (onHit.applyChance !== undefined && Math.random() > onHit.applyChance) return;
|
||||||
|
|
||||||
|
const state = get();
|
||||||
|
const room = state.currentRoom;
|
||||||
|
if (!room || room.enemies.length === 0) return;
|
||||||
|
|
||||||
|
// Apply to the first living enemy
|
||||||
|
const targetIdx = room.enemies.findIndex(e => e.hp > 0);
|
||||||
|
if (targetIdx === -1) return;
|
||||||
|
|
||||||
|
const target = room.enemies[targetIdx];
|
||||||
|
const effect: ActiveEffect = {
|
||||||
|
type: onHit.type,
|
||||||
|
remainingDuration: onHit.duration,
|
||||||
|
magnitude: onHit.magnitude,
|
||||||
|
source: 'spell',
|
||||||
|
bypassArmor: onHit.bypassArmor,
|
||||||
|
bypassBarrier: onHit.bypassBarrier,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedEnemies = [...room.enemies];
|
||||||
|
updatedEnemies[targetIdx] = {
|
||||||
|
...target,
|
||||||
|
activeEffects: [...target.activeEffects, effect],
|
||||||
|
};
|
||||||
|
|
||||||
|
set({ currentRoom: { ...room, enemies: updatedEnemies } });
|
||||||
|
logMessages.push(`${target.name} afflicted with ${onHit.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process one tick of all active effects on all enemies in the current room.
|
||||||
|
* Called after all weapon/golem attacks in processCombatTick.
|
||||||
|
* spec §6.3
|
||||||
|
* Returns total DoT damage dealt (to subtract from floorHP).
|
||||||
|
*/
|
||||||
|
export function processDoTPhase(
|
||||||
|
get: () => CombatStore,
|
||||||
|
set: (state: Partial<CombatState>) => void,
|
||||||
|
applyEnemyDefenses: (
|
||||||
|
dmg: number,
|
||||||
|
enemy: EnemyState | null,
|
||||||
|
roomType: string,
|
||||||
|
addLog: (msg: string) => void,
|
||||||
|
bypassArmor?: boolean,
|
||||||
|
bypassBarrier?: boolean,
|
||||||
|
) => number,
|
||||||
|
logMessages: string[],
|
||||||
|
): number {
|
||||||
|
const state = get();
|
||||||
|
const room = state.currentRoom;
|
||||||
|
if (!room || room.enemies.length === 0) return 0;
|
||||||
|
|
||||||
|
let totalDoTDamage = 0;
|
||||||
|
const updatedEnemies: EnemyState[] = [];
|
||||||
|
|
||||||
|
for (const enemy of room.enemies) {
|
||||||
|
if (enemy.hp <= 0) {
|
||||||
|
updatedEnemies.push(enemy);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let enemyHp = enemy.hp;
|
||||||
|
let enemyArmor = enemy.effectiveArmor;
|
||||||
|
const remainingEffects: ActiveEffect[] = [];
|
||||||
|
|
||||||
|
for (const effect of enemy.activeEffects) {
|
||||||
|
if (DOT_EFFECT_TYPES.has(effect.type)) {
|
||||||
|
// DoT: burn, poison, bleed (spec §6.3)
|
||||||
|
const dmg = effect.magnitude;
|
||||||
|
let dotFinalDmg: number;
|
||||||
|
if (effect.bypassArmor) {
|
||||||
|
// AC-13: bypass armor — apply directly to HP, skip all defenses
|
||||||
|
dotFinalDmg = dmg;
|
||||||
|
} else if (effect.bypassBarrier) {
|
||||||
|
// Bypass barrier: skip barrier but still apply armor
|
||||||
|
dotFinalDmg = applyEnemyDefenses(dmg, enemy, room.roomType, () => {}, false, true);
|
||||||
|
} else {
|
||||||
|
// Normal: apply full defense pipeline (dodge → barrier → armor)
|
||||||
|
dotFinalDmg = applyEnemyDefenses(dmg, enemy, room.roomType, () => {});
|
||||||
|
}
|
||||||
|
enemyHp = Math.max(0, enemyHp - dotFinalDmg);
|
||||||
|
totalDoTDamage += dotFinalDmg;
|
||||||
|
} else if (effect.type === 'curse') {
|
||||||
|
// Curse: amplifies incoming damage (tracked on enemy, applied next tick)
|
||||||
|
// No immediate HP effect — curse multiplier is stored on the enemy
|
||||||
|
} else if (effect.type === 'armor_corrode') {
|
||||||
|
// Armor corrode: reduce effective armor
|
||||||
|
enemyArmor = Math.max(0, enemyArmor - effect.magnitude);
|
||||||
|
}
|
||||||
|
// freeze, slow, blind: soft CC — no HP effect, just tracked
|
||||||
|
|
||||||
|
// Decrement duration
|
||||||
|
const newDuration = effect.remainingDuration - 1;
|
||||||
|
if (newDuration > 0) {
|
||||||
|
remainingEffects.push({ ...effect, remainingDuration: newDuration });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedEnemies.push({
|
||||||
|
...enemy,
|
||||||
|
hp: enemyHp,
|
||||||
|
effectiveArmor: enemyArmor,
|
||||||
|
activeEffects: remainingEffects,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ currentRoom: { ...room, enemies: updatedEnemies } });
|
||||||
|
return totalDoTDamage;
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { buildGolemCombatPipeline } from './pipelines/golem-combat';
|
|||||||
import type { TickContext, TickWrites } from './tick-pipeline';
|
import type { TickContext, TickWrites } from './tick-pipeline';
|
||||||
import type { GameCoordinatorState } from './gameStore.types';
|
import type { GameCoordinatorState } from './gameStore.types';
|
||||||
import type { EnemyState } from '../types';
|
import type { EnemyState } from '../types';
|
||||||
|
import { applyEnemyDefenses as applyEnemyDefensesFromPipeline } from './pipelines/combat-tick';
|
||||||
|
|
||||||
export interface GameCoordinatorStore extends GameCoordinatorState {
|
export interface GameCoordinatorStore extends GameCoordinatorState {
|
||||||
tick: () => void;
|
tick: () => void;
|
||||||
@@ -309,6 +310,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
ctx.prestige.signedPacts,
|
ctx.prestige.signedPacts,
|
||||||
{ activeGolems: golemPipeline.activeGolems },
|
{ activeGolems: golemPipeline.activeGolems },
|
||||||
golemPipeline.golemApplyDamageToRoom,
|
golemPipeline.golemApplyDamageToRoom,
|
||||||
|
(dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier) => applyEnemyDefensesFromPipeline(
|
||||||
|
dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
rawMana = combatResult.rawMana;
|
rawMana = combatResult.rawMana;
|
||||||
elements = combatResult.elements;
|
elements = combatResult.elements;
|
||||||
|
|||||||
@@ -37,13 +37,62 @@ interface BuildCombatCallbacksParams {
|
|||||||
effects: ComputedEffects;
|
effects: ComputedEffects;
|
||||||
maxMana: number;
|
maxMana: number;
|
||||||
addLog: (msg: string) => void;
|
addLog: (msg: string) => void;
|
||||||
useCombatStore: { setState: (s: Record<string, unknown>) => void };
|
useCombatStore: { setState: (s: Record<string, unknown>) => void; getState: () => Record<string, unknown> };
|
||||||
usePrestigeStore: { getState: () => { addDefeatedGuardian: (floor: number) => void } };
|
usePrestigeStore: { getState: () => { addDefeatedGuardian: (floor: number) => void } };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Speed-room bonus added to agile dodge chance (spec §4.5) */
|
/** Speed-room bonus added to agile dodge chance (spec §4.5) */
|
||||||
const SPEED_ROOM_DODGE_BONUS = 0.20;
|
const SPEED_ROOM_DODGE_BONUS = 0.20;
|
||||||
|
|
||||||
|
// ─── Standalone Enemy Defenses (for DoT/debuff pipeline) ─────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply regular enemy defenses: dodge → barrier → armor (spec §5.2).
|
||||||
|
* Returns modified damage, or 0 on dodge.
|
||||||
|
* Exported for use by the DoT/debuff tick processing system (spec §6.3).
|
||||||
|
*
|
||||||
|
* @param bypassArmor — if true, skip armor reduction entirely (spec §6.4, AC-13)
|
||||||
|
* @param bypassBarrier — if true, skip barrier absorption (spec §6.4)
|
||||||
|
*/
|
||||||
|
export function applyEnemyDefenses(
|
||||||
|
dmg: number,
|
||||||
|
enemy: EnemyState | null,
|
||||||
|
roomType: string,
|
||||||
|
addLog: (msg: string) => void,
|
||||||
|
bypassArmor?: boolean,
|
||||||
|
bypassBarrier?: boolean,
|
||||||
|
): number {
|
||||||
|
if (!enemy) return dmg;
|
||||||
|
|
||||||
|
// 1. Dodge check (spec §5.2, §4.5)
|
||||||
|
let effectiveDodge = enemy.dodgeChance;
|
||||||
|
if (roomType === 'speed') {
|
||||||
|
const hasAgile = enemy.name.toLowerCase().includes('agile');
|
||||||
|
if (hasAgile) {
|
||||||
|
effectiveDodge = Math.min(0.75, enemy.dodgeChance + SPEED_ROOM_DODGE_BONUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (effectiveDodge > 0 && Math.random() < effectiveDodge) {
|
||||||
|
addLog('Attack dodged!');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Barrier absorption (percentage, spec §5.2) — skipped if bypassBarrier
|
||||||
|
if (!bypassBarrier && enemy.barrier && enemy.barrier > 0) {
|
||||||
|
dmg *= (1 - enemy.barrier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Armor reduction — skipped if bypassArmor (spec §6.4, AC-13)
|
||||||
|
if (!bypassArmor) {
|
||||||
|
const armorValue = (enemy as EnemyState & { effectiveArmor?: number }).effectiveArmor ?? enemy.armor;
|
||||||
|
if (armorValue && armorValue > 0) {
|
||||||
|
dmg *= (1 - armorValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dmg;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
||||||
const { ctx, effects, maxMana, useCombatStore, usePrestigeStore } = params;
|
const { ctx, effects, maxMana, useCombatStore, usePrestigeStore } = params;
|
||||||
|
|
||||||
@@ -85,46 +134,8 @@ export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
|||||||
return { ...enemy, barrier: recharged };
|
return { ...enemy, barrier: recharged };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Local reference to the module-level applyEnemyDefenses
|
||||||
* Apply regular enemy defenses: dodge → barrier → armor (spec §5.2).
|
const defApply = applyEnemyDefenses;
|
||||||
* Returns modified damage, or 0 on dodge.
|
|
||||||
* This is the single defense pipeline used for ALL enemy hits (not just guardians).
|
|
||||||
*/
|
|
||||||
const applyEnemyDefenses = (
|
|
||||||
dmg: number,
|
|
||||||
enemy: EnemyState | null,
|
|
||||||
roomType: string,
|
|
||||||
addLog: (msg: string) => void,
|
|
||||||
): number => {
|
|
||||||
if (!enemy) return dmg;
|
|
||||||
|
|
||||||
// 1. Dodge check (spec §5.2, §4.5)
|
|
||||||
let effectiveDodge = enemy.dodgeChance;
|
|
||||||
if (roomType === 'speed') {
|
|
||||||
// Agile + speed room: additive dodge bonus, capped at 0.75
|
|
||||||
const hasAgile = enemy.name.toLowerCase().includes('agile');
|
|
||||||
if (hasAgile) {
|
|
||||||
effectiveDodge = Math.min(0.75, enemy.dodgeChance + SPEED_ROOM_DODGE_BONUS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (effectiveDodge > 0 && Math.random() < effectiveDodge) {
|
|
||||||
addLog('Attack dodged!');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Barrier absorption (percentage, spec §5.2)
|
|
||||||
if (enemy.barrier && enemy.barrier > 0) {
|
|
||||||
dmg *= (1 - enemy.barrier);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Armor reduction — use effectiveArmor (after corrode) if available, else base armor (spec §5.2)
|
|
||||||
const armorValue = (enemy as EnemyState & { effectiveArmor?: number }).effectiveArmor ?? enemy.armor;
|
|
||||||
if (armorValue && armorValue > 0) {
|
|
||||||
dmg *= (1 - armorValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dmg;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the onDamageDealt callback for this tick.
|
* Create the onDamageDealt callback for this tick.
|
||||||
@@ -150,7 +161,7 @@ export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply regular enemy defenses for ALL enemies (spec §5.2)
|
// Apply regular enemy defenses for ALL enemies (spec §5.2)
|
||||||
dmg = applyEnemyDefenses(dmg, defCtx.enemy, defCtx.roomType, addLog);
|
dmg = defApply(dmg, defCtx.enemy, defCtx.roomType, addLog);
|
||||||
|
|
||||||
// Guardian-specific defensive pipeline (shield → barrier → health regen, spec §5.3)
|
// Guardian-specific defensive pipeline (shield → barrier → health regen, spec §5.3)
|
||||||
const guardian = getGuardianForFloor(ctx.combat.currentFloor);
|
const guardian = getGuardianForFloor(ctx.combat.currentFloor);
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export type ActivityEventType =
|
|||||||
| 'armor_proc'
|
| 'armor_proc'
|
||||||
| 'spell_cast'
|
| 'spell_cast'
|
||||||
| 'golem_attack'
|
| 'golem_attack'
|
||||||
| 'puzzle_solved';
|
| 'puzzle_solved'
|
||||||
|
| 'debuff';
|
||||||
|
|
||||||
export interface ActivityLogEntry {
|
export interface ActivityLogEntry {
|
||||||
id: string; // Unique ID for React key
|
id: string; // Unique ID for React key
|
||||||
@@ -37,6 +38,25 @@ export interface ActivityLogEntry {
|
|||||||
|
|
||||||
export type RoomType = 'combat' | 'puzzle' | 'swarm' | 'speed' | 'guardian' | 'recovery' | 'library' | 'treasure';
|
export type RoomType = 'combat' | 'puzzle' | 'swarm' | 'speed' | 'guardian' | 'recovery' | 'library' | 'treasure';
|
||||||
|
|
||||||
|
export type EffectType =
|
||||||
|
| 'burn'
|
||||||
|
| 'poison'
|
||||||
|
| 'bleed'
|
||||||
|
| 'freeze'
|
||||||
|
| 'slow'
|
||||||
|
| 'curse'
|
||||||
|
| 'armor_corrode'
|
||||||
|
| 'blind';
|
||||||
|
|
||||||
|
export interface ActiveEffect {
|
||||||
|
type: EffectType;
|
||||||
|
remainingDuration: number; // in ticks
|
||||||
|
magnitude: number;
|
||||||
|
source: 'spell' | 'golem';
|
||||||
|
bypassArmor?: boolean;
|
||||||
|
bypassBarrier?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EnemyState {
|
export interface EnemyState {
|
||||||
id: string;
|
id: string;
|
||||||
name: string; // Display name for the enemy
|
name: string; // Display name for the enemy
|
||||||
@@ -46,6 +66,8 @@ export interface EnemyState {
|
|||||||
dodgeChance: number; // For speed rooms (0-1)
|
dodgeChance: number; // For speed rooms (0-1)
|
||||||
barrier?: number; // Shield that absorbs damage before HP (0-1 as percentage of max HP)
|
barrier?: number; // Shield that absorbs damage before HP (0-1 as percentage of max HP)
|
||||||
element: string;
|
element: string;
|
||||||
|
activeEffects: ActiveEffect[]; // DoT/debuff effects currently on this enemy
|
||||||
|
effectiveArmor: number; // Armor after corrode effects (0-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FloorState {
|
export interface FloorState {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type { ElementCategory, ElementDef, ElementState, ManaType } from './elem
|
|||||||
export type { AttunementSlot, AttunementDef, AttunementState, GuardianBoon, GuardianDef } from './attunements';
|
export type { AttunementSlot, AttunementDef, AttunementState, GuardianBoon, GuardianDef } from './attunements';
|
||||||
|
|
||||||
// Spell types
|
// Spell types
|
||||||
export type { SpellCost, SpellDef, SpellEffect, SpellState } from './spells';
|
export type { SpellCost, SpellDef, SpellEffect, SpellState, SpellOnHitEffect } from './spells';
|
||||||
|
|
||||||
// Equipment types
|
// Equipment types
|
||||||
export type {
|
export type {
|
||||||
@@ -48,4 +48,6 @@ export type {
|
|||||||
ActivityLogEntry,
|
ActivityLogEntry,
|
||||||
PrestigeDef,
|
PrestigeDef,
|
||||||
LootDrop,
|
LootDrop,
|
||||||
|
ActiveEffect,
|
||||||
|
EffectType,
|
||||||
} from './game';
|
} from './game';
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ export interface SpellCost {
|
|||||||
amount: number; // Amount of mana required
|
amount: number; // Amount of mana required
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SpellOnHitEffect {
|
||||||
|
type: 'burn' | 'poison' | 'bleed' | 'freeze' | 'slow' | 'curse' | 'armor_corrode' | 'blind';
|
||||||
|
duration: number; // ticks
|
||||||
|
magnitude: number;
|
||||||
|
bypassArmor?: boolean;
|
||||||
|
bypassBarrier?: boolean;
|
||||||
|
applyChance?: number; // 0-1, defaults to 1.0
|
||||||
|
}
|
||||||
|
|
||||||
export interface SpellDef {
|
export interface SpellDef {
|
||||||
name: string;
|
name: string;
|
||||||
elem: string; // Element type for damage calculations
|
elem: string; // Element type for damage calculations
|
||||||
@@ -23,6 +32,7 @@ export interface SpellDef {
|
|||||||
aoeTargets?: number; // Number of enemies hit by AOE
|
aoeTargets?: number; // Number of enemies hit by AOE
|
||||||
isWeaponEnchant?: boolean; // Can be used as weapon enchantment (magic swords)
|
isWeaponEnchant?: boolean; // Can be used as weapon enchantment (magic swords)
|
||||||
grimoire?: boolean; // Whether this spell appears in the grimoire
|
grimoire?: boolean; // Whether this spell appears in the grimoire
|
||||||
|
onHitEffect?: SpellOnHitEffect; // DoT/debuff applied on successful hit
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpellEffect {
|
export interface SpellEffect {
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export function generateSwarmEnemies(floor: number): EnemyState[] {
|
|||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
barrier: getEnemyBarrier(floor, element),
|
barrier: getEnemyBarrier(floor, element),
|
||||||
element,
|
element,
|
||||||
|
activeEffects: [],
|
||||||
|
effectiveArmor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return enemies;
|
return enemies;
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ export function generateFloorState(floor: number): FloorState {
|
|||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
barrier: 0,
|
barrier: 0,
|
||||||
element: guardian.element.join('+'),
|
element: guardian.element.join('+'),
|
||||||
|
activeEffects: [],
|
||||||
|
effectiveArmor: guardian.armor || 0,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,6 +134,8 @@ export function generateFloorState(floor: number): FloorState {
|
|||||||
dodgeChance: getDodgeChance(floor),
|
dodgeChance: getDodgeChance(floor),
|
||||||
barrier: getEnemyBarrier(floor, element),
|
barrier: getEnemyBarrier(floor, element),
|
||||||
element,
|
element,
|
||||||
|
activeEffects: [],
|
||||||
|
effectiveArmor: getFloorArmor(floor),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -164,6 +168,8 @@ export function generateFloorState(floor: number): FloorState {
|
|||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
barrier: getEnemyBarrier(floor, element),
|
barrier: getEnemyBarrier(floor, element),
|
||||||
element,
|
element,
|
||||||
|
activeEffects: [],
|
||||||
|
effectiveArmor: getFloorArmor(floor),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ export function generateSpireFloorState(floor: number, roomIndex: number, totalR
|
|||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
barrier: 0,
|
barrier: 0,
|
||||||
element: guardian.element.join('+'),
|
element: guardian.element.join('+'),
|
||||||
|
activeEffects: [],
|
||||||
|
effectiveArmor: guardian.armor || 0,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -184,6 +186,8 @@ function generateCombatRoom(floor: number, element: string, baseHP: number): Flo
|
|||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
barrier,
|
barrier,
|
||||||
element,
|
element,
|
||||||
|
activeEffects: [],
|
||||||
|
effectiveArmor: armor,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -203,6 +207,8 @@ function generateSwarmRoom(floor: number, element: string, baseHP: number): Floo
|
|||||||
dodgeChance: 0,
|
dodgeChance: 0,
|
||||||
barrier: 0,
|
barrier: 0,
|
||||||
element,
|
element,
|
||||||
|
activeEffects: [],
|
||||||
|
effectiveArmor: Math.floor(floor / 15) * 0.02,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +230,8 @@ function generateSpeedRoom(floor: number, element: string, baseHP: number): Floo
|
|||||||
dodgeChance,
|
dodgeChance,
|
||||||
barrier: getSpireEnemyBarrier(floor, element),
|
barrier: getSpireEnemyBarrier(floor, element),
|
||||||
element,
|
element,
|
||||||
|
activeEffects: [],
|
||||||
|
effectiveArmor: armor,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user