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
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
This commit is contained in:
+11
-31
@@ -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 (3–8 depending on core tier). Stats are derived from components via `computeGolemStats()`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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. ✅
|
||||||
|
|||||||
@@ -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 |
|
||||||
@@ -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; // 1–4 (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 |
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user