diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index ec2049f..ce9c977 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,4 +1,4 @@ # Circular Dependencies -Generated: 2026-06-02T14:39:46.904Z +Generated: 2026-06-03T13:40:52.900Z No circular dependencies found. ✅ diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 9d6e141..dc07048 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_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.", "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", "effects/discipline-effects.ts", "stores/combat-state.types.ts", + "stores/golem-combat-actions.ts", "types.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": [ "types.ts" ], "stores/combatStore.ts": [ "data/guardian-encounters.ts", "stores/combat-actions.ts", + "stores/combat-descent-actions.ts", "stores/combat-state.types.ts", "types.ts", "utils/activity-log.ts", "utils/index.ts", "utils/room-utils.ts", - "utils/safe-persist.ts", - "utils/spire-utils.ts" + "utils/safe-persist.ts" ], "stores/crafting-equipment-tick.ts": [ "constants.ts", @@ -661,15 +672,23 @@ "stores/manaStore.ts", "stores/pipelines/combat-tick.ts", "stores/pipelines/enchanting-tick.ts", + "stores/pipelines/golem-combat.ts", "stores/pipelines/pact-ritual.ts", "stores/prestigeStore.ts", "stores/tick-pipeline.ts", "stores/uiStore.ts", + "types.ts", "utils/element-cap-bonus.ts", "utils/index.ts", "utils/safe-persist.ts" ], "stores/gameStore.types.ts": [], + "stores/golem-combat-actions.ts": [ + "constants.ts", + "data/golems/index.ts", + "types.ts", + "utils/index.ts" + ], "stores/index.ts": [ "constants.ts", "stores/attunementStore.ts", @@ -696,7 +715,9 @@ "constants.ts", "data/guardian-encounters.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": [ "constants.ts", @@ -715,6 +736,12 @@ "stores/manaStore.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": [ "constants.ts", "data/guardian-encounters.ts", diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 44e11af..f6f2b41 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -12,6 +12,21 @@ Mana-Loop/ │ └── pre-commit ├── docs/ │ ├── 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-combat-spec.md │ ├── GAME_BRIEFING.md @@ -347,6 +362,7 @@ Mana-Loop/ │ │ │ │ ├── craftingStore.types.ts │ │ │ │ ├── debugBridge.ts │ │ │ │ ├── discipline-slice.ts +│ │ │ │ ├── dot-runtime.ts │ │ │ │ ├── gameActions.ts │ │ │ │ ├── gameHooks.ts │ │ │ │ ├── gameLoopActions.ts diff --git a/docs/specs/attunements/attunement-system-spec.md b/docs/specs/attunements/attunement-system-spec.md new file mode 100644 index 0000000..3d95ca6 --- /dev/null +++ b/docs/specs/attunements/attunement-system-spec.md @@ -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 | diff --git a/docs/specs/attunements/enchanter/enchanter-spec.md b/docs/specs/attunements/enchanter/enchanter-spec.md new file mode 100644 index 0000000..c1dd710 --- /dev/null +++ b/docs/specs/attunements/enchanter/enchanter-spec.md @@ -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 | diff --git a/docs/specs/attunements/enchanter/systems/enchanting-spec.md b/docs/specs/attunements/enchanter/systems/enchanting-spec.md new file mode 100644 index 0000000..8c28f6c --- /dev/null +++ b/docs/specs/attunements/enchanter/systems/enchanting-spec.md @@ -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 | diff --git a/docs/specs/attunements/fabricator/fabricator-spec.md b/docs/specs/attunements/fabricator/fabricator-spec.md new file mode 100644 index 0000000..b4e4f7e --- /dev/null +++ b/docs/specs/attunements/fabricator/fabricator-spec.md @@ -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 | diff --git a/docs/specs/attunements/fabricator/systems/golemancy-spec.md b/docs/specs/attunements/fabricator/systems/golemancy-spec.md new file mode 100644 index 0000000..61be614 --- /dev/null +++ b/docs/specs/attunements/fabricator/systems/golemancy-spec.md @@ -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 | diff --git a/docs/specs/attunements/fabricator/systems/item-fabrication-spec.md b/docs/specs/attunements/fabricator/systems/item-fabrication-spec.md new file mode 100644 index 0000000..a14bbe0 --- /dev/null +++ b/docs/specs/attunements/fabricator/systems/item-fabrication-spec.md @@ -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; // 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 | diff --git a/docs/specs/attunements/invoker/invoker-spec.md b/docs/specs/attunements/invoker/invoker-spec.md new file mode 100644 index 0000000..4d12bdd --- /dev/null +++ b/docs/specs/attunements/invoker/invoker-spec.md @@ -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 | diff --git a/docs/specs/attunements/invoker/systems/pact-system-spec.md b/docs/specs/attunements/invoker/systems/pact-system-spec.md new file mode 100644 index 0000000..a6225e9 --- /dev/null +++ b/docs/specs/attunements/invoker/systems/pact-system-spec.md @@ -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 | diff --git a/src/lib/game/__tests__/combat-actions.test.ts b/src/lib/game/__tests__/combat-actions.test.ts index 05267e5..11d4e59 100644 --- a/src/lib/game/__tests__/combat-actions.test.ts +++ b/src/lib/game/__tests__/combat-actions.test.ts @@ -75,6 +75,7 @@ function runCombatTick(rawMana: number, elements: Record dmg, // applyEnemyDefenses (passthrough for tests) ); } @@ -94,6 +95,7 @@ function processCombatTickDirect( onFloorCleared, onDamageDealt, signedPacts, { activeGolems: [] }, golemApplyDamageToRoom, + (dmg: number) => dmg, // applyEnemyDefenses (passthrough for tests) ); } diff --git a/src/lib/game/__tests__/cross-module-helpers.ts b/src/lib/game/__tests__/cross-module-helpers.ts index 61a5d03..e3f9ea0 100644 --- a/src/lib/game/__tests__/cross-module-helpers.ts +++ b/src/lib/game/__tests__/cross-module-helpers.ts @@ -52,7 +52,7 @@ export function resetAllStores() { roomResetState: {}, clearedRooms: {}, isDescentComplete: false, - golemancy: { enabledGolems: [], summonedGolems: [], lastSummonFloor: 0 }, + golemancy: { enabledGolems: [], summonedGolems: [], activeGolems: [], lastSummonFloor: 0 }, equipmentSpellStates: [], comboHitCount: 0, floorHitCount: 0, diff --git a/src/lib/game/__tests__/enemy-defenses.test.ts b/src/lib/game/__tests__/enemy-defenses.test.ts index 5d73a93..3a1b9bd 100644 --- a/src/lib/game/__tests__/enemy-defenses.test.ts +++ b/src/lib/game/__tests__/enemy-defenses.test.ts @@ -76,6 +76,8 @@ function makeEnemy(overrides: Partial = {}): EnemyState { armor: 0, dodgeChance: 0, element: 'fire', + activeEffects: [], + effectiveArmor: 0, ...overrides, }; } @@ -197,18 +199,17 @@ describe('Enemy Defenses (spec §5.2)', () => { describe('effectiveArmor (post-corrode)', () => { it('should use effectiveArmor over base armor when set', () => { - const enemy = makeEnemy({ armor: 0.4 }) as EnemyState & { effectiveArmor?: number }; - enemy.effectiveArmor = 0.2; // Armor reduced by corrode + const enemy = makeEnemy({ armor: 0.4, effectiveArmor: 0.2 }); // Armor reduced by corrode - // The defense pipeline uses effectiveArmor ?? armor - const armorValue = enemy.effectiveArmor ?? enemy.armor; + // The defense pipeline uses effectiveArmor (after corrode) + const armorValue = enemy.effectiveArmor; expect(armorValue).toBe(0.2); }); - it('should fall back to base armor when effectiveArmor is not set', () => { - const enemy = makeEnemy({ armor: 0.4 }); + it('should have effectiveArmor equal to base armor when no corrode applied', () => { + 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); }); }); @@ -219,6 +220,7 @@ describe('Enemy Defenses (spec §5.2)', () => { dodgeChance: 0, barrier: 0.5, armor: 0.3, + effectiveArmor: 0.3, }); // Simulate: 100 damage → barrier (50%) → 50 → armor (30%) → 35 @@ -229,10 +231,11 @@ describe('Enemy Defenses (spec §5.2)', () => { } expect(dmg).toBe(50); - const armorValue = (enemy as EnemyState & { effectiveArmor?: number }).effectiveArmor ?? enemy.armor; + const armorValue = enemy.effectiveArmor; if (armorValue && armorValue > 0) { dmg *= (1 - armorValue); } + // 100 → barrier (50%) → 50 → armor (30%) → 35 expect(dmg).toBe(35); }); }); diff --git a/src/lib/game/constants/spells-modules/basic-elemental-spells.ts b/src/lib/game/constants/spells-modules/basic-elemental-spells.ts index 189bdb0..28342ef 100644 --- a/src/lib/game/constants/spells-modules/basic-elemental-spells.ts +++ b/src/lib/game/constants/spells-modules/basic-elemental-spells.ts @@ -13,7 +13,8 @@ export const BASIC_ELEMENTAL_SPELLS: Record = { castSpeed: 2, unlock: 100, 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: { name: "Ember Shot", @@ -24,7 +25,8 @@ export const BASIC_ELEMENTAL_SPELLS: Record = { castSpeed: 3, unlock: 75, 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: { name: "Water Jet", @@ -145,7 +147,8 @@ export const BASIC_ELEMENTAL_SPELLS: Record = { castSpeed: 2, unlock: 150, 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: { name: "Rot Touch", @@ -156,6 +159,7 @@ export const BASIC_ELEMENTAL_SPELLS: Record = { castSpeed: 2, unlock: 170, studyTime: 3, - desc: "Touch of decay and rot." + desc: "Touch of decay and rot.", + onHitEffect: { type: 'curse', duration: 4, magnitude: 0.20 }, }, }; diff --git a/src/lib/game/constants/spells-modules/frost-spells.ts b/src/lib/game/constants/spells-modules/frost-spells.ts index e912f41..bff0aa7 100644 --- a/src/lib/game/constants/spells-modules/frost-spells.ts +++ b/src/lib/game/constants/spells-modules/frost-spells.ts @@ -15,7 +15,8 @@ export const FROST_SPELLS: Record = { unlock: 180, studyTime: 3, 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: { name: "Ice Shard", @@ -27,7 +28,8 @@ export const FROST_SPELLS: Record = { unlock: 250, studyTime: 4, 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 diff --git a/src/lib/game/constants/spells-modules/lightning-spells.ts b/src/lib/game/constants/spells-modules/lightning-spells.ts index d2b1474..f2bc2e1 100644 --- a/src/lib/game/constants/spells-modules/lightning-spells.ts +++ b/src/lib/game/constants/spells-modules/lightning-spells.ts @@ -15,7 +15,8 @@ export const LIGHTNING_SPELLS: Record = { unlock: 120, studyTime: 2, 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: { name: "Lightning Bolt", @@ -27,7 +28,8 @@ export const LIGHTNING_SPELLS: Record = { unlock: 150, studyTime: 3, 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 diff --git a/src/lib/game/constants/spells-modules/soul-spells.ts b/src/lib/game/constants/spells-modules/soul-spells.ts index 0d578df..f8d4dd3 100644 --- a/src/lib/game/constants/spells-modules/soul-spells.ts +++ b/src/lib/game/constants/spells-modules/soul-spells.ts @@ -15,7 +15,8 @@ export const SOUL_SPELLS: Record = { unlock: 30000, studyTime: 36, 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: { name: "Spirit Blast", @@ -27,6 +28,7 @@ export const SOUL_SPELLS: Record = { unlock: 60000, studyTime: 50, 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 }, }, }; diff --git a/src/lib/game/stores/combat-actions.ts b/src/lib/game/stores/combat-actions.ts index bded565..ffea402 100644 --- a/src/lib/game/stores/combat-actions.ts +++ b/src/lib/game/stores/combat-actions.ts @@ -5,8 +5,10 @@ import { SPELLS_DEF, HOURS_PER_TICK } from '../constants'; import { getGuardianForFloor } from '../data/guardian-encounters'; 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 { SpellOnHitEffect } from '../types/spells'; import { getFloorMaxHP, getFloorElement, getMultiElementBonus, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils'; import { computeDisciplineEffects } from '../effects/discipline-effects'; import { processGolemMaintenance, processGolemAttacks } from './golem-combat-actions'; @@ -65,6 +67,14 @@ export function processCombatTick( signedPacts: number[], golemancyState: { activeGolems: ActiveGolem[] }, 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 { const state = get(); const logMessages: string[] = []; @@ -177,6 +187,9 @@ export function processCombatTick( castProgress -= 1; safetyCounter++; + // Apply on-hit effect (DoT/debuff) to enemy (spec §6.2) + applyOnHitEffect(get, set, spellId, logMessages); + // Check if room/floor is cleared if (floorHP <= 0) { const guardian = getGuardianForFloor(currentFloor); @@ -302,6 +315,24 @@ export function processCombatTick( 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); return { diff --git a/src/lib/game/stores/combat-state.types.ts b/src/lib/game/stores/combat-state.types.ts index cabc282..b8395b8 100644 --- a/src/lib/game/stores/combat-state.types.ts +++ b/src/lib/game/stores/combat-state.types.ts @@ -1,7 +1,7 @@ // ─── Combat State Types ──────────────────────────────────────────────────────── // 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) ───────────────────────────────────────────────── @@ -145,6 +145,14 @@ export interface CombatActions { signedPacts: number[], golemancyState: { activeGolems: ActiveGolem[] }, 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; elements: Record; diff --git a/src/lib/game/stores/combatStore.ts b/src/lib/game/stores/combatStore.ts index d15d57d..d740969 100644 --- a/src/lib/game/stores/combatStore.ts +++ b/src/lib/game/stores/combatStore.ts @@ -4,7 +4,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; 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 { generateFloorState } from '../utils/room-utils'; import { addActivityLogEntry } from '../utils/activity-log'; @@ -302,6 +302,14 @@ export const useCombatStore = create()( signedPacts: number[], golemancyState: { activeGolems: ActiveGolem[] }, 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( get, @@ -315,6 +323,7 @@ export const useCombatStore = create()( signedPacts, golemancyState, golemApplyDamageToRoom, + applyEnemyDefenses, ); }, diff --git a/src/lib/game/stores/dot-runtime.ts b/src/lib/game/stores/dot-runtime.ts new file mode 100644 index 0000000..414a2ce --- /dev/null +++ b/src/lib/game/stores/dot-runtime.ts @@ -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) => 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) => 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; +} diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index beb8c99..a82e371 100644 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -28,6 +28,7 @@ import { buildGolemCombatPipeline } from './pipelines/golem-combat'; import type { TickContext, TickWrites } from './tick-pipeline'; import type { GameCoordinatorState } from './gameStore.types'; import type { EnemyState } from '../types'; +import { applyEnemyDefenses as applyEnemyDefensesFromPipeline } from './pipelines/combat-tick'; export interface GameCoordinatorStore extends GameCoordinatorState { tick: () => void; @@ -309,6 +310,9 @@ export const useGameStore = create()( ctx.prestige.signedPacts, { activeGolems: golemPipeline.activeGolems }, golemPipeline.golemApplyDamageToRoom, + (dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier) => applyEnemyDefensesFromPipeline( + dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier, + ), ); rawMana = combatResult.rawMana; elements = combatResult.elements; diff --git a/src/lib/game/stores/pipelines/combat-tick.ts b/src/lib/game/stores/pipelines/combat-tick.ts index c674e15..7ded4d6 100644 --- a/src/lib/game/stores/pipelines/combat-tick.ts +++ b/src/lib/game/stores/pipelines/combat-tick.ts @@ -37,13 +37,62 @@ interface BuildCombatCallbacksParams { effects: ComputedEffects; maxMana: number; addLog: (msg: string) => void; - useCombatStore: { setState: (s: Record) => void }; + useCombatStore: { setState: (s: Record) => void; getState: () => Record }; usePrestigeStore: { getState: () => { addDefeatedGuardian: (floor: number) => void } }; } /** Speed-room bonus added to agile dodge chance (spec §4.5) */ 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) { const { ctx, effects, maxMana, useCombatStore, usePrestigeStore } = params; @@ -85,46 +134,8 @@ export function buildCombatCallbacks(params: BuildCombatCallbacksParams) { return { ...enemy, barrier: recharged }; }; - /** - * Apply regular enemy defenses: dodge → barrier → armor (spec §5.2). - * 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; - }; + // Local reference to the module-level applyEnemyDefenses + const defApply = applyEnemyDefenses; /** * 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) - 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) const guardian = getGuardianForFloor(ctx.combat.currentFloor); diff --git a/src/lib/game/types/game.ts b/src/lib/game/types/game.ts index 34cf692..809c069 100644 --- a/src/lib/game/types/game.ts +++ b/src/lib/game/types/game.ts @@ -17,7 +17,8 @@ export type ActivityEventType = | 'armor_proc' | 'spell_cast' | 'golem_attack' - | 'puzzle_solved'; + | 'puzzle_solved' + | 'debuff'; export interface ActivityLogEntry { 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 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 { id: string; name: string; // Display name for the enemy @@ -46,6 +66,8 @@ export interface EnemyState { dodgeChance: number; // For speed rooms (0-1) barrier?: number; // Shield that absorbs damage before HP (0-1 as percentage of max HP) element: string; + activeEffects: ActiveEffect[]; // DoT/debuff effects currently on this enemy + effectiveArmor: number; // Armor after corrode effects (0-1) } export interface FloorState { diff --git a/src/lib/game/types/index.ts b/src/lib/game/types/index.ts index 80d8261..9f587a6 100644 --- a/src/lib/game/types/index.ts +++ b/src/lib/game/types/index.ts @@ -9,7 +9,7 @@ export type { ElementCategory, ElementDef, ElementState, ManaType } from './elem export type { AttunementSlot, AttunementDef, AttunementState, GuardianBoon, GuardianDef } from './attunements'; // Spell types -export type { SpellCost, SpellDef, SpellEffect, SpellState } from './spells'; +export type { SpellCost, SpellDef, SpellEffect, SpellState, SpellOnHitEffect } from './spells'; // Equipment types export type { @@ -48,4 +48,6 @@ export type { ActivityLogEntry, PrestigeDef, LootDrop, + ActiveEffect, + EffectType, } from './game'; diff --git a/src/lib/game/types/spells.ts b/src/lib/game/types/spells.ts index bc7de7d..952e196 100644 --- a/src/lib/game/types/spells.ts +++ b/src/lib/game/types/spells.ts @@ -7,6 +7,15 @@ export interface SpellCost { 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 { name: string; elem: string; // Element type for damage calculations @@ -23,6 +32,7 @@ export interface SpellDef { aoeTargets?: number; // Number of enemies hit by AOE isWeaponEnchant?: boolean; // Can be used as weapon enchantment (magic swords) grimoire?: boolean; // Whether this spell appears in the grimoire + onHitEffect?: SpellOnHitEffect; // DoT/debuff applied on successful hit } export interface SpellEffect { diff --git a/src/lib/game/utils/enemy-utils.ts b/src/lib/game/utils/enemy-utils.ts index ea5e459..33251df 100644 --- a/src/lib/game/utils/enemy-utils.ts +++ b/src/lib/game/utils/enemy-utils.ts @@ -65,6 +65,8 @@ export function generateSwarmEnemies(floor: number): EnemyState[] { dodgeChance: 0, barrier: getEnemyBarrier(floor, element), element, + activeEffects: [], + effectiveArmor: SWARM_CONFIG.armorBase + Math.floor(floor / 10) * SWARM_CONFIG.armorPerFloor, }); } return enemies; diff --git a/src/lib/game/utils/room-utils.ts b/src/lib/game/utils/room-utils.ts index 6042675..91aca14 100644 --- a/src/lib/game/utils/room-utils.ts +++ b/src/lib/game/utils/room-utils.ts @@ -110,6 +110,8 @@ export function generateFloorState(floor: number): FloorState { dodgeChance: 0, barrier: 0, element: guardian.element.join('+'), + activeEffects: [], + effectiveArmor: guardian.armor || 0, }], }; @@ -132,6 +134,8 @@ export function generateFloorState(floor: number): FloorState { dodgeChance: getDodgeChance(floor), barrier: getEnemyBarrier(floor, element), element, + activeEffects: [], + effectiveArmor: getFloorArmor(floor), }], }; } @@ -164,6 +168,8 @@ export function generateFloorState(floor: number): FloorState { dodgeChance: 0, barrier: getEnemyBarrier(floor, element), element, + activeEffects: [], + effectiveArmor: getFloorArmor(floor), }], }; } diff --git a/src/lib/game/utils/spire-utils.ts b/src/lib/game/utils/spire-utils.ts index 7057e46..4a5318d 100644 --- a/src/lib/game/utils/spire-utils.ts +++ b/src/lib/game/utils/spire-utils.ts @@ -111,6 +111,8 @@ export function generateSpireFloorState(floor: number, roomIndex: number, totalR dodgeChance: 0, barrier: 0, element: guardian.element.join('+'), + activeEffects: [], + effectiveArmor: guardian.armor || 0, }], }; } @@ -184,6 +186,8 @@ function generateCombatRoom(floor: number, element: string, baseHP: number): Flo dodgeChance: 0, barrier, element, + activeEffects: [], + effectiveArmor: armor, }], }; } @@ -203,6 +207,8 @@ function generateSwarmRoom(floor: number, element: string, baseHP: number): Floo dodgeChance: 0, barrier: 0, element, + activeEffects: [], + effectiveArmor: Math.floor(floor / 15) * 0.02, }); } @@ -224,6 +230,8 @@ function generateSpeedRoom(floor: number, element: string, baseHP: number): Floo dodgeChance, barrier: getSpireEnemyBarrier(floor, element), element, + activeEffects: [], + effectiveArmor: armor, }], }; }