chore: golemancy redesign cleanup — remove orphaned legacy code and update docs
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s

This commit is contained in:
2026-06-07 12:54:12 +02:00
parent 59fe6cd111
commit 1a0886f702
13 changed files with 128 additions and 153 deletions
+11 -31
View File
@@ -768,43 +768,23 @@ Level 8: 4 slots
Level 10: 5 slots Level 10: 5 slots
``` ```
### Golem Types (10 Total — undergoing redesign, see issue #268) ### Component-Based Construction
#### Base Golems (1) Golems are designed by assembling **three mandatory components** plus optional enchantments:
| Golem | Element | Damage | Speed | HP | Pierce | Unlock | | Component | Role | Count | Examples |
|-------|---------|--------|-------|----|--------|--------| |-----------|------|-------|----------|
| Earth Golem | Earth | 8 | 1.5/s | 50 | 15% | Fabricator 2 | | **Core** | Power source: mana types, capacity, regen, upkeep, duration | 4 (Basic, Intermediate, Advanced, Guardian) | Basic Core (Earth only), Guardian Core (all guardian mana types) |
| **Frame** | Combat stats: damage, speed, armor pierce, magic affinity, special | 7 (Earth, Sand, Frost, Crystal, Steel, Shadowglass, Crystal-Steel Hybrid) | Earth (balanced), Shadowglass (fast + AoE), Crystal-Steel Hybrid (guardian constructs) |
#### Elemental Variant Golems (3) | **Mind Circuit** | Behavior: basic attacks, spell casting, spell cycling | 4 (Simple, Intermediate, Advanced, Guardian) | Simple (basic only), Guardian (cycle all spells) |
| **Enchantments** | Sword effects on basic attacks (optional) | 8 | Burn, Slow, Shock, Weaken, Armor Pierce, Crit Chance |
| Golem | Element | Damage | Speed | HP | Pierce | Unlock |
|-------|---------|--------|-------|----|--------|--------|
| Steel Golem | Metal | 12 | 1.2/s | 60 | 35% | Metal mana unlocked |
| Crystal Golem | Crystal | 18 | 1.0/s | 40 | 25% | Crystal mana unlocked |
| Sand Golem | Sand | 10 | 2.0/hr | 45 | 15% | Sand mana unlocked |
#### Hybrid Golems (6) — Require Enchanter 5 + Fabricator 5
| Golem | Elements | Damage | Speed | HP | Pierce | Special |
|-------|----------|--------|-------|----|--------|---------|
| Lava Golem | Earth + Fire | 15 | 1.0/s | 70 | 20% | AOE 2 |
| Galvanic Golem | Metal + Lightning | 10 | 3.5/s | 45 | 45% | Fast |
| Obsidian Golem | Earth + Dark | 25 | 0.8/s | 55 | 50% | High damage |
| Prism Golem | Crystal + Light | 28 | 2.0/hr | 60 | 45% | AOE 3 |
| Quicksilver Golem | Metal + Water | 14 | 4.0/hr | 55 | 35% | Very fast |
| Voidstone Golem | Earth + Void | 40 | 0.6/s | 100 | 60% | AOE 3, ultimate |
### Golem Combat
> ⚠ The golemancy system is undergoing a full redesign (see issue #268). The current data definitions exist but are disconnected from the combat pipeline.
**Player upkeep formula:**
``` ```
progressPerTick = HOURS_PER_TICK × attackSpeed × efficiencyBonus Upkeep per hour = Core.manaRegen × 2
damage = baseDamage × (1 + golemMasteryBonus)
``` ```
Golems last `1 + golemLongevity` floors. Maintenance cost multiplier: `1 - (golemSiphon × 0.1)`. Golems last `Core.maxRoomDuration` rooms (38 depending on core tier). Stats are derived from components via `computeGolemStats()`.
--- ---
+1 -1
View File
@@ -1,4 +1,4 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-06-06T16:37:23.532Z Generated: 2026-06-06T17:19:20.910Z
No circular dependencies found. ✅ No circular dependencies found. ✅
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-06-06T16:37:21.673Z", "generated": "2026-06-06T17:19:19.033Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
}, },
@@ -557,6 +557,8 @@
], ],
"stores/combat-descent-actions.ts": [ "stores/combat-descent-actions.ts": [
"data/guardian-encounters.ts", "data/guardian-encounters.ts",
"effects/discipline-effects.ts",
"stores/attunementStore.ts",
"stores/combat-state.types.ts", "stores/combat-state.types.ts",
"stores/golem-combat-actions.ts", "stores/golem-combat-actions.ts",
"stores/manaStore.ts", "stores/manaStore.ts",
@@ -253,7 +253,7 @@ study-fabricator-recipes (root)
|---|---| |---|---|
| `src/lib/game/data/attunements.ts` | Fabricator definition | | `src/lib/game/data/attunements.ts` | Fabricator definition |
| `src/lib/game/data/disciplines/fabricator.ts` | Fabricator disciplines (5) | | `src/lib/game/data/disciplines/fabricator.ts` | Fabricator disciplines (5) |
| `src/lib/game/data/golems/` | Golem definitions (10 golems) | | `src/lib/game/data/golems/` | Golem component definitions (4 cores, 7 frames, 4 mind circuits, 8 enchantments) |
| `src/lib/game/crafting-fabricator.ts` | Fabrication crafting logic | | `src/lib/game/crafting-fabricator.ts` | Fabrication crafting logic |
| `src/lib/game/data/fabricator-recipes.ts` | Core equipment recipes | | `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-material-recipes.ts` | Material recipes |
@@ -550,4 +550,4 @@ Directly determines base golem slots: `floor(fabricatorLevel / 2)`.
| `src/lib/game/stores/golem-combat-actions.ts` | Golem combat actions (rewrite) | | `src/lib/game/stores/golem-combat-actions.ts` | Golem combat actions (rewrite) |
| `src/lib/game/stores/pipelines/golem-combat.ts` | Golem combat pipeline (rewrite) | | `src/lib/game/stores/pipelines/golem-combat.ts` | Golem combat pipeline (rewrite) |
| `src/components/game/tabs/GolemancyTab.tsx` | Golemancy UI (major rewrite — design builder) | | `src/components/game/tabs/GolemancyTab.tsx` | Golemancy UI (major rewrite — design builder) |
| `docs/specs/spire-combat-spec.md §9` | Authoritative runtime spec (needs update) | | `docs/specs/spire-combat-spec.md §9` | Authoritative runtime spec |
+76 -49
View File
@@ -414,68 +414,81 @@ At peak incursion (day 30), regen falls to 5% of base. Practical effects:
### 9.1 Overview ### 9.1 Overview
Golemancy is the **Fabricator attunement's** combat contribution. Golems are Golemancy is the **Fabricator attunement's** combat contribution. Players design
summoned automatically at room entry, fight alongside the player, and disappear custom golems from components (Core + Frame + Mind Circuit + Enchantments), then
after a fixed number of rooms or if their maintenance cost cannot be met. configure a loadout. Golems are summoned automatically at room entry, fight alongside
the player, and disappear after a fixed number of rooms or if their maintenance cost
cannot be met.
### 9.2 Golem Loadout (Outside Spire) ### 9.2 Golem Loadout (Outside Spire)
The player configures a **golem loadout** from the Golemancy tab before entering 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. the spire. The loadout defines which golem designs to attempt to summon and in what
This configuration persists across rooms but not across spire runs. order. This configuration persists across rooms but not across spire runs.
### 9.3 Summoning on Room Entry ### 9.3 Summoning on Room Entry
When the player enters a new combat room: When the player enters a new combat room, `summonGolemsOnRoomEntry()` iterates the
loadout in priority order:
``` ```
onRoomEntry(): summonGolemsOnRoomEntry(loadout, rawMana, elements, currentFloor, existingActiveGolems, disciplineSlotsBonus, fabricatorLevel):
for each golem in golemLoadout: for each entry in loadout:
if player has enough mana of golem.summonCostType >= golem.summonCost: if !entry.enabled → skip
deductMana(golem.summonCost, golem.summonCostType) if activeGolems.length >= totalSlots → break // max 7
if already active → skip
resolve components (Core, Frame, Mind Circuit) from design
stats = computeGolemStats(componentDesign)
if player can afford stats.totalSummonCost:
deduct summon cost from player mana
activeGolems.push({ activeGolems.push({
...golemDef, designId: entry.designId,
roomsRemaining: golemDef.maxRoomDuration, summonedFloor: currentFloor,
attackProgress: 0, attackProgress: 0,
roomsRemaining: stats.maxRoomDuration,
currentMana: stats.manaCapacity, // starts full
spellCastIndex: 0,
}) })
activityLog("${golem.name} summoned")
else: else:
activityLog("Not enough mana to summon ${golem.name} — skipped") log "Not enough mana — skipped"
``` ```
Total slots = `min(7, floor(fabricatorLevel / 2) + disciplineBonus)`.
Golems that could not be summoned (insufficient mana) are **not re-attempted** Golems that could not be summoned (insufficient mana) are **not re-attempted**
within the same room. They will be attempted again on the next room entry. within the same room. They will be attempted again on the next room entry.
### 9.4 Golem Combat ### 9.4 Golem Combat
Each active golem attacks on its own `attackProgress` timer, identical to swords: Each active golem attacks on its own `attackProgress` timer:
``` ```
golemProgress += HOURS_PER_TICK × golem.attackSpeed attackProgress += HOURS_PER_TICK × frame.attackSpeed
while golemProgress >= 1: while attackProgress >= 1:
dmg = golem.baseDamage if mindCircuit has spells && golem.currentMana >= spellCost:
// Apply golem's own elemental type if it has one cast spell: damage = baseSpellDamage × frame.magicAffinity
if golem.element: golem.currentMana -= spellCost
dmg ×= getElementalBonus(golem.element, enemy.element) spellCastIndex = (spellCastIndex + 1) % selectedSpells.length
// Apply golem special effects (DoT, armor pierce, AoE, etc.) else:
applyGolemEffects(golem, dmg, enemy) dmg = frame.baseDamage × (1 + frame.armorPierce)
apply enchantment effects (burn, slow, etc.)
applyDamageToRoom(dmg) applyDamageToRoom(dmg)
golemProgress -= 1 attackProgress -= 1
``` ```
Golems ignore Executioner and Berserker discipline specials. Golems ignore Executioner and Berserker discipline specials.
### 9.5 Maintenance Cost ### 9.5 Maintenance Cost
Each tick, each active golem checks its maintenance cost: Each tick, `processGolemMaintenance()` checks upkeep for each active golem:
``` ```
tickGolemMaintenance(golem): upkeepPerTick = core.manaRegen × 2 × HOURS_PER_TICK
if player mana[golem.maintenanceCostType] >= golem.maintenanceCost × HOURS_PER_TICK: if player has enough of core.primaryManaType:
deductMana(golem.maintenanceCost × HOURS_PER_TICK, golem.maintenanceCostType) deduct upkeepPerTick from player element mana
else: else:
dismiss(golem) dismiss(golem)
activityLog("${golem.name} dismissed — insufficient ${golem.maintenanceCostType} mana") log "${name} dismissed — insufficient mana for upkeep"
``` ```
A dismissed golem is **not re-summoned mid-room**. It will be re-attempted on the A dismissed golem is **not re-summoned mid-room**. It will be re-attempted on the
@@ -483,13 +496,14 @@ next room entry if mana has recovered.
### 9.6 Room Duration Limit ### 9.6 Room Duration Limit
`countdownGolemRoomDuration()` runs on room clear:
``` ```
onRoomCleared():
for each activeGolem: for each activeGolem:
activeGolem.roomsRemaining -= 1 golem.roomsRemaining -= 1
if activeGolem.roomsRemaining <= 0: if golem.roomsRemaining <= 0:
dismiss(golem) dismiss(golem)
activityLog("${golem.name} has faded after ${maxRoomDuration} rooms") log "${name} has faded after ${maxRoomDuration} rooms"
``` ```
Room duration ticks down on room clear, not on room entry — golems persist through Room duration ticks down on room clear, not on room entry — golems persist through
@@ -497,25 +511,38 @@ the full room they were summoned in.
### 9.7 Golem Data Shape ### 9.7 Golem Data Shape
The runtime active golem type (`RuntimeActiveGolem` in `types/game.ts`):
```typescript ```typescript
interface GolemDefinition { interface RuntimeActiveGolem {
id: string; designId: string; // Reference to the player's GolemDesign
name: string; summonedFloor: number; // Floor when golem was summoned
tier: number; // 14 (determines general power) attackProgress: number; // Progress toward next attack (accumulated)
baseDamage: number; roomsRemaining: number; // Rooms before golem fades
attackSpeed: number; // attacks per in-game hour currentMana: number; // Current mana in golem's own pool
element?: ElementType; // optional elemental type for matchup spellCastIndex: number; // For alternating/cycling spell circuits
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;
} }
``` ```
The serialized design type (`SerializedGolemDesign` in `types/game.ts`):
```typescript
interface SerializedGolemDesign {
id: string;
name: string;
coreId: string;
frameId: string;
mindCircuitId: string;
enchantmentIds: string[];
selectedManaTypes: string[];
selectedSpells: string[];
}
```
Golem stats are computed from components via `computeGolemStats()` in
`data/golems/utils.ts`, which sums summon costs from all components and derives
upkeep from `core.manaRegen × 2`.
--- ---
## 10. In-Game Time Display ## 10. In-Game Time Display
@@ -545,7 +572,7 @@ They are **in scope for the implementation this spec describes**:
| Mage barrier recharge | `MODIFIER_CONFIG.mage.barrierRechargeRate` | Data-only | Tick in `onDamageDealt` §5.2 | | Mage barrier recharge | `MODIFIER_CONFIG.mage.barrierRechargeRate` | Data-only | Tick in `onDamageDealt` §5.2 |
| Guardian armor | `GuardianDef.armor` | Data-only | Add check to guardian pipeline §5.3 | | Guardian armor | `GuardianDef.armor` | Data-only | Add check to guardian pipeline §5.3 |
| DoT / debuff system | Spell/enchantment type defs | **Implemented**`dot-runtime.ts` complete and wired into combat tick; curse amplification added (issue #286) | Verified working | | DoT / debuff system | Spell/enchantment type defs | **Implemented**`dot-runtime.ts` complete and wired into combat tick; curse amplification added (issue #286) | Verified working |
| Golemancy combat | Full golem data exists | Disconnected | Implement per §9 | | Golemancy combat | Full golem data + runtime | **Implemented** — component-based system complete | Verified working |
| Sword melee attacks | Weapon type exists | **Implemented** — meleeProgress with enemy defense application (issue #285) | Add `meleeProgress` per §3.1 | | Sword melee attacks | Weapon type exists | **Implemented** — meleeProgress with enemy defense application (issue #285) | Add `meleeProgress` per §3.1 |
| AoE target distribution | `SpellDefinition.aoe` flag | Partial | Implement per §3.2 | | AoE target distribution | `SpellDefinition.aoe` flag | Partial | Implement per §3.2 |
| `elemMasteryBonus` | Stub in `calcDamage` | Hardcoded `1` | Future — leave as `1` for now | | `elemMasteryBonus` | Stub in `calcDamage` | Hardcoded `1` | Future — leave as `1` for now |
+2
View File
@@ -21,6 +21,7 @@ export function LeftPanel() {
const rawMana = useManaStore((s) => s.rawMana); const rawMana = useManaStore((s) => s.rawMana);
const elements = useManaStore((s) => s.elements); const elements = useManaStore((s) => s.elements);
const meditateTicks = useManaStore((s) => s.meditateTicks); const meditateTicks = useManaStore((s) => s.meditateTicks);
const elementRegen = useManaStore((s) => s.elementRegen);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
@@ -77,6 +78,7 @@ export function LeftPanel() {
onGatherStart={handleGatherStart} onGatherStart={handleGatherStart}
onGatherEnd={handleGatherEnd} onGatherEnd={handleGatherEnd}
elements={elements} elements={elements}
elementRegen={elementRegen}
/> />
</DebugName> </DebugName>
@@ -14,6 +14,7 @@ import {
createActiveGolem, createActiveGolem,
} from '@/lib/game/data/golems/utils'; } from '@/lib/game/data/golems/utils';
import type { GolemDesign } from '@/lib/game/data/golems/types'; import type { GolemDesign } from '@/lib/game/data/golems/types';
import type { RuntimeActiveGolem } from '@/lib/game/types';
// ─── Helper ─────────────────────────────────────────────────────────────────── // ─── Helper ───────────────────────────────────────────────────────────────────
@@ -274,9 +275,9 @@ describe('canAffordGolemDesign', () => {
// ─── Active Golem Creation ──────────────────────────────────────────────────── // ─── Active Golem Creation ────────────────────────────────────────────────────
describe('createActiveGolem', () => { describe('createActiveGolem', () => {
it('creates active golem with correct initial state', () => { it('creates RuntimeActiveGolem with correct initial state', () => {
const design = makeDesign('basic', 'earth', 'simple'); const design = makeDesign('basic', 'earth', 'simple');
const active = createActiveGolem(design, 5); const active = createActiveGolem(design, 5) as RuntimeActiveGolem;
expect(active.designId).toBe(design.id); expect(active.designId).toBe(design.id);
expect(active.summonedFloor).toBe(5); expect(active.summonedFloor).toBe(5);
@@ -288,7 +289,7 @@ describe('createActiveGolem', () => {
it('guardian golem starts with full mana', () => { it('guardian golem starts with full mana', () => {
const design = makeDesign('guardian', 'crystalSteelHybrid', 'guardian'); const design = makeDesign('guardian', 'crystalSteelHybrid', 'guardian');
const active = createActiveGolem(design, 10); const active = createActiveGolem(design, 10) as RuntimeActiveGolem;
expect(active.currentMana).toBe(500); expect(active.currentMana).toBe(500);
expect(active.roomsRemaining).toBe(8); expect(active.roomsRemaining).toBe(8);
+1 -1
View File
@@ -16,7 +16,7 @@ export type {
ComputedGolemStats, ComputedGolemStats,
GolemManaCost, GolemManaCost,
GolemUnlockRequirement, GolemUnlockRequirement,
ActiveGolemV2,
} from './types'; } from './types';
export { elemCost, rawCost } from './types'; export { elemCost, rawCost } from './types';
-15
View File
@@ -135,18 +135,3 @@ export interface ComputedGolemStats {
specialEffect: FrameSpecial; specialEffect: FrameSpecial;
} }
// ─── Runtime Active Golem (in combat) ───────────────────────────────────
export interface ActiveGolemV2 {
/** Reference to the GolemDesign used */
designId: string;
design: GolemDesign;
summonedFloor: number;
attackProgress: number;
roomsRemaining: number;
currentMana: number;
/** Index for alternating/cycling spells */
spellCastIndex: number;
}
+5 -42
View File
@@ -1,12 +1,12 @@
// ─── Golem Helper Functions ────────────────────────────────────────────── // ─── Golem Helper Functions ──────────────────────────────────────────────
// Component-based construction system utilities. // Component-based construction system utilities.
import type { RuntimeActiveGolem } from '../../types';
import type { import type {
ComputedGolemStats, ComputedGolemStats,
GolemDesign, GolemDesign,
GolemManaCost, GolemManaCost,
GolemUnlockRequirement, GolemUnlockRequirement,
ActiveGolemV2,
} from './types'; } from './types';
import { CORES } from './cores'; import { CORES } from './cores';
import { FRAMES } from './frames'; import { FRAMES } from './frames';
@@ -144,22 +144,21 @@ export function canAffordGolemDesign(
return { canAfford: true, missing: '' }; return { canAfford: true, missing: '' };
} }
// ─── Active Golem V2 Helpers ────────────────────────────────────────────── // ─── Active Golem Helpers ─────────────────────────────────────────────────
/** /**
* Create a new ActiveGolemV2 from a GolemDesign for combat. * Create a new RuntimeActiveGolem from a GolemDesign for combat.
*/ */
export function createActiveGolem( export function createActiveGolem(
design: GolemDesign, design: GolemDesign,
currentFloor: number, currentFloor: number,
): ActiveGolemV2 { ): RuntimeActiveGolem {
return { return {
designId: design.id, designId: design.id,
design,
summonedFloor: currentFloor, summonedFloor: currentFloor,
attackProgress: 0, attackProgress: 0,
roomsRemaining: design.core.maxRoomDuration, roomsRemaining: design.core.maxRoomDuration,
currentMana: design.core.manaCapacity, // Starts full currentMana: design.core.manaCapacity,
spellCastIndex: 0, spellCastIndex: 0,
}; };
} }
@@ -181,39 +180,3 @@ export function getMindCircuit(id: string) {
return MIND_CIRCUITS[id] || null; return MIND_CIRCUITS[id] || null;
} }
// ─── Legacy Compatibility ────────────────────────────────────────────────
/**
* @deprecated Use getGolemSlots instead
*/
export function getGolemFloorDuration(_skills: Record<string, number>): number {
return 3; // Default room duration for legacy calls
}
/**
* @deprecated Use computeGolemStats instead
*/
export function getGolemDamage(
golemId: string,
_skills: Record<string, number>,
): number {
// Legacy lookup — returns 0 for component-based golems
return 0;
}
/**
* @deprecated Use computeGolemStats instead
*/
export function getGolemAttackSpeed(
golemId: string,
_skills: Record<string, number>,
): number {
return 0;
}
/**
* @deprecated Component-based system doesn't use skill-based maintenance multiplier
*/
export function getGolemMaintenanceMultiplier(_skills: Record<string, number>): number {
return 1;
}
+8 -1
View File
@@ -190,6 +190,13 @@ export const useGameStore = create<GameCoordinatorStore>()(
if (!elements[elem].unlocked) elements[elem] = { ...elements[elem], unlocked: true }; if (!elements[elem].unlocked) elements[elem] = { ...elements[elem], unlocked: true };
elements[elem] = { ...elements[elem], current: Math.min(elements[elem].max, elements[elem].current + entry.finalRate * HOURS_PER_TICK) }; elements[elem] = { ...elements[elem], current: Math.min(elements[elem].max, elements[elem].current + entry.finalRate * HOURS_PER_TICK) };
} }
// Compute per-element net regen: produced rate - drain from being used as component
const elementRegen: Record<string, number> = {};
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
const produced = entry.finalRate;
const drained = conversionResult.elementDrain[elem] || 0;
elementRegen[elem] = produced - drained;
}
// Net raw regen = gross regen - conversion drains - incursion // Net raw regen = gross regen - conversion drains - incursion
const netRawRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - conversionResult.totalRawDrain); const netRawRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - conversionResult.totalRawDrain);
const actualRegen = Math.floor(Math.min(netRawRegen * HOURS_PER_TICK, maxMana - rawMana) * 1000) / 1000; const actualRegen = Math.floor(Math.min(netRawRegen * HOURS_PER_TICK, maxMana - rawMana) * 1000) / 1000;
@@ -305,7 +312,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
// Phase 3: Write // Phase 3: Write
writes.game = { day, hour, incursionStrength }; writes.game = { day, hour, incursionStrength };
writes.mana = { rawMana, meditateTicks, totalManaGathered, elements }; writes.mana = { rawMana, meditateTicks, totalManaGathered, elements, elementRegen };
applyTickWrites(writes, storeSetters); applyTickWrites(writes, storeSetters);
} catch (error: unknown) { } catch (error: unknown) {
+10 -2
View File
@@ -19,6 +19,8 @@ export interface ManaState {
meditateTicks: number; meditateTicks: number;
totalManaGathered: number; totalManaGathered: number;
elements: Record<string, ElementState>; elements: Record<string, ElementState>;
/** Per-element net regen rates (from unified conversion system) */
elementRegen: Record<string, number>;
} }
// ─── Mana Actions ──────────────────────────────────────────────────────────── // ─── Mana Actions ────────────────────────────────────────────────────────────
@@ -40,6 +42,7 @@ export interface ManaActions {
spendElementMana: (element: string, amount: number) => Result<void>; spendElementMana: (element: string, amount: number) => Result<void>;
setElementMax: (max: number) => void; setElementMax: (max: number) => void;
computeElementMaxWithBonuses: (perElementBonuses: Record<string, number>) => void; computeElementMaxWithBonuses: (perElementBonuses: Record<string, number>) => void;
setElementRegen: (regen: Record<string, number>) => void;
// Reset // Reset
resetMana: (prestigeUpgrades: Record<string, number>) => void; resetMana: (prestigeUpgrades: Record<string, number>) => void;
@@ -66,6 +69,7 @@ export const useManaStore = create<ManaStore>()(
} }
]) ])
) as Record<string, ElementState>, ) as Record<string, ElementState>,
elementRegen: {},
setRawMana: (amount: number) => { setRawMana: (amount: number) => {
set({ rawMana: Math.max(0, amount) }); set({ rawMana: Math.max(0, amount) });
@@ -148,17 +152,21 @@ export const useManaStore = create<ManaStore>()(
}); });
}, },
setElementRegen: (regen: Record<string, number>) => {
set({ elementRegen: regen });
},
resetMana: (prestigeUpgrades: Record<string, number>) => { resetMana: (prestigeUpgrades: Record<string, number>) => {
const elementMax = 10 + (prestigeUpgrades.elementalAttune || 0) * 25; const elementMax = 10 + (prestigeUpgrades.elementalAttune || 0) * 25;
const startingMana = 10 + (prestigeUpgrades.manaStart || 0) * 10; const startingMana = 10 + (prestigeUpgrades.manaStart || 0) * 10;
set({ rawMana: startingMana, meditateTicks: 0, totalManaGathered: 0, elements: makeInitialElements(elementMax, prestigeUpgrades) }); set({ rawMana: startingMana, meditateTicks: 0, totalManaGathered: 0, elements: makeInitialElements(elementMax, prestigeUpgrades), elementRegen: {} });
}, },
}), }),
{ {
storage: createSafeStorage(), storage: createSafeStorage(),
name: 'mana-loop-mana', name: 'mana-loop-mana',
version: 2, version: 2,
partialize: (state) => ({ rawMana: state.rawMana, meditateTicks: state.meditateTicks, totalManaGathered: state.totalManaGathered, elements: state.elements }), partialize: (state) => ({ rawMana: state.rawMana, meditateTicks: state.meditateTicks, totalManaGathered: state.totalManaGathered, elements: state.elements, elementRegen: state.elementRegen }),
migrate: (persistedState: any, _version) => { migrate: (persistedState: any, _version) => {
if (persistedState && persistedState.elements) { if (persistedState && persistedState.elements) {
for (const k of Object.keys(persistedState.elements)) { for (const k of Object.keys(persistedState.elements)) {