feat(golemancy): Phase 1 - Component-based construction system data definitions
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s

- Add new golem component types (Core, Frame, MindCircuit, Enchantment)
- Create 4 Core tiers, 7 Frames, 4 Mind Circuits, 8 Enchantments
- Rewrite golem utils for component-based stat computation
- Update GolemancyState with new fields (golemDesigns, golemLoadout, activeGolems)
- Update combat store, actions, and pipelines for new golem system
- Rewrite GolemancyTab with component selection UI
- Update fabricator discipline perks for new system
- Add comprehensive tests for component registries and utilities
- All files under 400 lines, all 743 tests passing
This commit is contained in:
2026-06-06 16:50:26 +02:00
parent c40e4ee940
commit 4b7aa82953
43 changed files with 2763 additions and 944 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ export const fabricatorDisciplines: DisciplineDefinition[] = [
type: 'once',
threshold: 200,
value: 0,
description: 'Unlock golem summoning',
description: 'Unlock golem design ability',
},
{
id: 'golem-2',
+100
View File
@@ -0,0 +1,100 @@
// ─── Core Definitions ────────────────────────────────────────────────────
// Power sources for golems. Determine mana types, capacity, regen, upkeep, duration.
import type { CoreDefinition } from './types';
import { elemCost } from './types';
// ───_BASIC CORE ────────────────────────────────────────────────────────────
// Fabricator 2 — single Earth mana, modest stats
const BASIC_CORE: CoreDefinition = {
id: 'basic',
tier: 1,
name: 'Basic Core',
description: 'A simple earth-infused core. Provides modest mana capacity and single-element support.',
manaTypes: ['earth'],
manaCapacity: 50,
manaRegen: 0.5,
maxRoomDuration: 3,
summonCost: [elemCost('earth', 10)],
primaryManaType: 'earth',
tierMultiplier: 1.0,
unlockRequirement: {
type: 'attunement_level',
attunement: 'fabricator',
level: 2,
},
};
// ─── INTERMEDIATE CORE ────────────────────────────────────────────────────
// Fabricator 4 + Enchanter 2 — choose 2 mana types
const INTERMEDIATE_CORE: CoreDefinition = {
id: 'intermediate',
tier: 2,
name: 'Intermediate Core',
description: 'A refined crystal core supporting two mana types. Player selects from unlocked elements.',
manaTypes: ['earth'], // Default; player overrides with chosen types
manaCapacity: 100,
manaRegen: 1.5,
maxRoomDuration: 4,
summonCost: [elemCost('crystal', 20)],
primaryManaType: 'crystal',
tierMultiplier: 1.5,
unlockRequirement: {
type: 'dual_attunement',
attunements: ['fabricator', 'enchanter'],
levels: [4, 2],
},
};
// ─── ADVANCED CORE ───────────────────────────────────────────────────────
// Fabricator 6 + Enchanter 3 — choose 3 mana types
const ADVANCED_CORE: CoreDefinition = {
id: 'advanced',
tier: 3,
name: 'Advanced Core',
description: 'A powerful crystal core supporting three mana types. Player selects from unlocked elements.',
manaTypes: ['earth'], // Default; player overrides with chosen types
manaCapacity: 200,
manaRegen: 3.0,
maxRoomDuration: 5,
summonCost: [elemCost('crystal', 30)],
primaryManaType: 'crystal',
tierMultiplier: 2.0,
unlockRequirement: {
type: 'dual_attunement',
attunements: ['fabricator', 'enchanter'],
levels: [6, 3],
},
};
// ─── GUARDIAN CORE ───────────────────────────────────────────────────────
// Invoker 5 + Fabricator 5 + Guardian Pact — guardian-specific mana types
const GUARDIAN_CORE: CoreDefinition = {
id: 'guardian',
tier: 4,
name: 'Guardian Core',
description: 'A legendary core imbued with guardian energy. Provides all mana types granted by the chosen Guardian. Required for Guardian Constructs.',
manaTypes: ['earth'], // Overridden by guardian-specific types when assigned
manaCapacity: 500,
manaRegen: 10.0,
maxRoomDuration: 8,
summonCost: [elemCost('earth', 50)], // Guardian-specific in practice
primaryManaType: 'earth', // Overridden by guardian pact
tierMultiplier: 3.0,
unlockRequirement: {
type: 'guardian_pact',
attunements: ['invoker', 'fabricator'],
levels: [5, 5],
},
};
// ─── CORE REGISTRY ───────────────────────────────────────────────────────
export const CORES: Record<string, CoreDefinition> = {
[BASIC_CORE.id]: BASIC_CORE,
[INTERMEDIATE_CORE.id]: INTERMEDIATE_CORE,
[ADVANCED_CORE.id]: ADVANCED_CORE,
[GUARDIAN_CORE.id]: GUARDIAN_CORE,
};
export const ALL_CORES = [BASIC_CORE, INTERMEDIATE_CORE, ADVANCED_CORE, GUARDIAN_CORE];
+155
View File
@@ -0,0 +1,155 @@
// ─── Frame Definitions ───────────────────────────────────────────────────
// Physical combat characteristics for golems: damage, speed, armor pierce, magic affinity, special.
import type { FrameDefinition } from './types';
import { elemCost, rawCost } from './types';
const EARTH_FRAME: FrameDefinition = {
id: 'earth',
name: 'Earth Frame',
description: 'A sturdy construct of stone and soil. Balanced but unremarkable.',
baseDamage: 6,
attackSpeed: 1.2,
armorPierce: 0.05,
magicAffinity: 0.3,
aoeTargets: 1,
element: 'earth',
specialEffect: 'none',
summonCost: [rawCost(5)],
unlockRequirement: {
type: 'attunement_level',
attunement: 'fabricator',
level: 2,
},
};
const SAND_FRAME: FrameDefinition = {
id: 'sand',
name: 'Sand Frame',
description: 'A shifting construct of sand particles. Hits multiple enemies with high armor pierce.',
baseDamage: 8,
attackSpeed: 1.0,
armorPierce: 0.6,
magicAffinity: 0.5,
aoeTargets: 2,
element: 'sand',
specialEffect: 'aoe',
summonCost: [elemCost('sand', 8)],
unlockRequirement: {
type: 'mana_unlocked',
manaType: 'sand',
},
};
const FROST_FRAME: FrameDefinition = {
id: 'frost',
name: 'Frost Frame',
description: 'An icy construct that slows enemies on hit. High magic affinity.',
baseDamage: 10,
attackSpeed: 1.2,
armorPierce: 0.25,
magicAffinity: 0.8,
aoeTargets: 1,
element: 'frost',
specialEffect: 'slow',
summonCost: [elemCost('frost', 10)],
unlockRequirement: {
type: 'mana_unlocked',
manaType: 'frost',
},
};
const CRYSTAL_FRAME: FrameDefinition = {
id: 'crystal',
name: 'Crystal Frame',
description: 'A prismatic construct dealing high damage with precision. Very high magic affinity.',
baseDamage: 14,
attackSpeed: 1.8,
armorPierce: 0.15,
magicAffinity: 0.9,
aoeTargets: 1,
element: 'crystal',
specialEffect: 'none',
summonCost: [elemCost('crystal', 12)],
unlockRequirement: {
type: 'mana_unlocked',
manaType: 'crystal',
},
};
const STEEL_FRAME: FrameDefinition = {
id: 'steel',
name: 'Steel Frame',
description: 'Forged from metal, this frame delivers devastating attacks with high armor pierce.',
baseDamage: 18,
attackSpeed: 1.6,
armorPierce: 0.5,
magicAffinity: 0.5,
aoeTargets: 1,
element: 'metal',
specialEffect: 'none',
summonCost: [elemCost('metal', 14)],
unlockRequirement: {
type: 'mana_unlocked',
manaType: 'metal',
},
};
const SHADOWGLASS_FRAME: FrameDefinition = {
id: 'shadowglass',
name: 'Shadowglass Frame',
description: 'Volcanic glass animated by shadow. Extremely fast AoE attacks with devastating magic affinity.',
baseDamage: 20,
attackSpeed: 2.5,
armorPierce: 0.65,
magicAffinity: 0.95,
aoeTargets: 2,
element: 'shadowglass',
specialEffect: 'aoe',
summonCost: [elemCost('shadowglass', 18), rawCost(10)],
unlockRequirement: {
type: 'mana_unlocked',
manaType: 'shadowglass',
},
};
const CRYSTAL_STEEL_HYBRID_FRAME: FrameDefinition = {
id: 'crystalSteelHybrid',
name: 'Crystal-Steel Hybrid Frame',
description: 'An advanced hybrid frame capable of housing Guardian Cores. Highest combined stats. Required for Guardian Constructs.',
baseDamage: 22,
attackSpeed: 2.8,
armorPierce: 0.7,
magicAffinity: 1.0,
aoeTargets: 1,
element: 'crystal',
specialEffect: 'guardianConstruct',
summonCost: [elemCost('crystal', 20), elemCost('metal', 15), rawCost(15)],
unlockRequirement: {
type: 'attunement_level',
attunement: 'fabricator',
level: 5,
},
};
// ─── FRAME REGISTRY ─────────────────────────────────────────────────────
export const FRAMES: Record<string, FrameDefinition> = {
[EARTH_FRAME.id]: EARTH_FRAME,
[SAND_FRAME.id]: SAND_FRAME,
[FROST_FRAME.id]: FROST_FRAME,
[CRYSTAL_FRAME.id]: CRYSTAL_FRAME,
[STEEL_FRAME.id]: STEEL_FRAME,
[SHADOWGLASS_FRAME.id]: SHADOWGLASS_FRAME,
[CRYSTAL_STEEL_HYBRID_FRAME.id]: CRYSTAL_STEEL_HYBRID_FRAME,
};
export const ALL_FRAMES = [
EARTH_FRAME,
SAND_FRAME,
FROST_FRAME,
CRYSTAL_FRAME,
STEEL_FRAME,
SHADOWGLASS_FRAME,
CRYSTAL_STEEL_HYBRID_FRAME,
];
@@ -0,0 +1,102 @@
// ─── Golem Enchantment Definitions ───────────────────────────────────────
// Optional sword effects applied to golem basic attacks.
// Requires Enchanter 5 + Fabricator 5.
import type { GolemEnchantmentDefinition } from './types';
import { elemCost } from './types';
const SWORD_FIRE: GolemEnchantmentDefinition = {
id: 'sword_fire',
name: 'Sword: Fire',
description: 'Applies Burn DoT on basic attack.',
effect: 'burn',
capacityCost: 10,
summonCost: [elemCost('fire', 5)],
};
const SWORD_FROST: GolemEnchantmentDefinition = {
id: 'sword_frost',
name: 'Sword: Frost',
description: 'Applies additional Slow on basic attack.',
effect: 'slow',
capacityCost: 10,
summonCost: [elemCost('frost', 5)],
};
const SWORD_LIGHTNING: GolemEnchantmentDefinition = {
id: 'sword_lightning',
name: 'Sword: Lightning',
description: 'Chance to Shock (stun) on basic attack.',
effect: 'shock',
capacityCost: 12,
summonCost: [elemCost('lightning', 6)],
};
const SWORD_SHADOW: GolemEnchantmentDefinition = {
id: 'sword_shadow',
name: 'Sword: Shadow',
description: 'Chance to Weaken (reduce enemy damage) on basic attack.',
effect: 'weaken',
capacityCost: 12,
summonCost: [elemCost('dark', 6)],
};
const SWORD_METAL: GolemEnchantmentDefinition = {
id: 'sword_metal',
name: 'Sword: Metal',
description: 'Bonus Armor Pierce on basic attack.',
effect: 'armorPierce',
capacityCost: 8,
summonCost: [elemCost('metal', 5)],
};
const SWORD_CRYSTAL: GolemEnchantmentDefinition = {
id: 'sword_crystal',
name: 'Sword: Crystal',
description: 'Bonus Critical Chance on basic attack.',
effect: 'criticalChance',
capacityCost: 14,
summonCost: [elemCost('crystal', 7)],
};
const SWORD_WATER: GolemEnchantmentDefinition = {
id: 'sword_water',
name: 'Sword: Water',
description: 'Applies Soak on basic attack (increases lightning damage taken).',
effect: 'soak',
capacityCost: 8,
summonCost: [elemCost('water', 4)],
};
const SWORD_EARTH: GolemEnchantmentDefinition = {
id: 'sword_earth',
name: 'Sword: Earth',
description: 'Bonus damage to shielded enemies on basic attack.',
effect: 'shieldBreak',
capacityCost: 10,
summonCost: [elemCost('earth', 5)],
};
// ─── ENCHANTMENT REGISTRY ────────────────────────────────────────────────
export const GOLEM_ENCHANTMENTS: Record<string, GolemEnchantmentDefinition> = {
[SWORD_FIRE.id]: SWORD_FIRE,
[SWORD_FROST.id]: SWORD_FROST,
[SWORD_LIGHTNING.id]: SWORD_LIGHTNING,
[SWORD_SHADOW.id]: SWORD_SHADOW,
[SWORD_METAL.id]: SWORD_METAL,
[SWORD_CRYSTAL.id]: SWORD_CRYSTAL,
[SWORD_WATER.id]: SWORD_WATER,
[SWORD_EARTH.id]: SWORD_EARTH,
};
export const ALL_GOLEM_ENCHANTMENTS = [
SWORD_FIRE,
SWORD_FROST,
SWORD_LIGHTNING,
SWORD_SHADOW,
SWORD_METAL,
SWORD_CRYSTAL,
SWORD_WATER,
SWORD_EARTH,
];
+7 -13
View File
@@ -1,14 +1,8 @@
// ─── Golem Definitions Data ─────────────────────────
// Combined golem definitions from all golem modules.
// Extracted to a standalone module to avoid circular dependencies
// between index.ts and utils.ts.
// ─── Golem Definitions Data ──────────────────────────────────────────────
// Combined component registries for the component-based golem system.
// Extracted to a standalone module to avoid circular dependencies.
import { BASE_GOLEMS } from './base-golems';
import { ELEMENTAL_GOLEMS } from './elemental-golems';
import { HYBRID_GOLEMS } from './hybrid-golems';
export const GOLEMS_DEF = {
...BASE_GOLEMS,
...ELEMENTAL_GOLEMS,
...HYBRID_GOLEMS,
};
export { CORES, ALL_CORES } from './cores';
export { FRAMES, ALL_FRAMES } from './frames';
export { MIND_CIRCUITS, ALL_MIND_CIRCUITS } from './mindCircuits';
export { GOLEM_ENCHANTMENTS, ALL_GOLEM_ENCHANTMENTS } from './golemEnchantments';
+27 -19
View File
@@ -1,23 +1,31 @@
// ─── Golem Definitions Index ───────────────────────
// Re-exports from all golem modules
// ─── Golem Definitions Index ─────────────────────────────────────────────
// Barrel exports for the component-based golem system.
// Re-export types
export type { GolemDef, GolemManaCost } from './types';
export type {
CoreDefinition,
CoreId,
FrameDefinition,
FrameId,
FrameSpecial,
MindCircuitDefinition,
MindCircuitId,
CircuitBehavior,
GolemEnchantmentDefinition,
GolemDesign,
ComputedGolemStats,
GolemManaCost,
GolemUnlockRequirement,
ActiveGolemV2,
} from './types';
// Re-export combined golems data (extracted to avoid circular deps)
export { GOLEMS_DEF } from './golems-data';
export { elemCost, rawCost } from './types';
// Re-export utility functions
export {
getGolemSlots,
isGolemUnlocked,
getUnlockedGolems,
getGolemDamage,
getGolemAttackSpeed,
getGolemFloorDuration,
getGolemMaintenanceMultiplier,
canAffordGolemSummon,
deductGolemSummonCost,
canAffordGolemMaintenance,
deductGolemMaintenance,
} from './utils';
// Re-export component registries
export { CORES, ALL_CORES } from './cores';
export { FRAMES, ALL_FRAMES } from './frames';
export { MIND_CIRCUITS, ALL_MIND_CIRCUITS } from './mindCircuits';
export { GOLEM_ENCHANTMENTS, ALL_GOLEM_ENCHANTMENTS } from './golemEnchantments';
// Legacy re-exports (deprecated, kept for migration)
export type { GolemDef } from './types';
+77
View File
@@ -0,0 +1,77 @@
// ─── Mind Circuit Definitions ────────────────────────────────────────────
// Behavior logic for golems: basic attacks, spell casting patterns.
import type { MindCircuitDefinition } from './types';
import { elemCost, rawCost } from './types';
const SIMPLE_CIRCUIT: MindCircuitDefinition = {
id: 'simple',
name: 'Simple Logic Circuit',
description: 'Performs basic attacks only. Targets nearest enemy. No spell casting.',
spellSlots: 0,
behavior: 'basicOnly',
summonCost: [rawCost(3)],
unlockRequirement: {
type: 'attunement_level',
attunement: 'fabricator',
level: 1,
},
};
const INTERMEDIATE_CIRCUIT: MindCircuitDefinition = {
id: 'intermediate',
name: 'Intermediate Logic Circuit',
description: 'Casts 1 selected spell when mana is available. Otherwise performs basic attacks.',
spellSlots: 1,
behavior: 'castSpell1',
summonCost: [elemCost('crystal', 8)],
unlockRequirement: {
type: 'dual_attunement',
attunements: ['enchanter', 'fabricator'],
levels: [2, 3],
},
};
const ADVANCED_CIRCUIT: MindCircuitDefinition = {
id: 'advanced',
name: 'Advanced Logic Circuit',
description: 'Casts 2 selected spells in alternating order: A → B → A → B... Falls back to basic attacks if mana is insufficient.',
spellSlots: 2,
behavior: 'alternate2',
summonCost: [elemCost('crystal', 12)],
unlockRequirement: {
type: 'dual_attunement',
attunements: ['enchanter', 'fabricator'],
levels: [3, 4],
},
};
const GUARDIAN_CIRCUIT: MindCircuitDefinition = {
id: 'guardian',
name: 'Guardian Circuit',
description: 'Required for Guardian Constructs. Cycles through one spell per mana type from the Guardian Core. Falls back to basic attacks if mana is insufficient.',
spellSlots: 4, // Typically 3-4 depending on guardian
behavior: 'cycleAll',
summonCost: [elemCost('crystal', 25), rawCost(10)],
unlockRequirement: {
type: 'dual_attunement',
attunements: ['invoker', 'fabricator'],
levels: [5, 5],
},
};
// ─── MIND CIRCUIT REGISTRY ───────────────────────────────────────────────
export const MIND_CIRCUITS: Record<string, MindCircuitDefinition> = {
[SIMPLE_CIRCUIT.id]: SIMPLE_CIRCUIT,
[INTERMEDIATE_CIRCUIT.id]: INTERMEDIATE_CIRCUIT,
[ADVANCED_CIRCUIT.id]: ADVANCED_CIRCUIT,
[GUARDIAN_CIRCUIT.id]: GUARDIAN_CIRCUIT,
};
export const ALL_MIND_CIRCUITS = [
SIMPLE_CIRCUIT,
INTERMEDIATE_CIRCUIT,
ADVANCED_CIRCUIT,
GUARDIAN_CIRCUIT,
];
+149 -14
View File
@@ -1,8 +1,11 @@
// ─── Golem Types ─────────────────────────────────────────────────
// ─── Golem Component Types ──────────────────────────────────────────────
// Component-based construction system: Core + Frame + Mind Circuit + Enchantments.
// Replaces the legacy predefined GolemDef system.
import type { SpellCost } from '../../types';
// Golem mana cost helper
// ─── Mana Cost Helpers ───────────────────────────────────────────────────
export function elemCost(element: string, amount: number): SpellCost {
return { type: 'element', element, amount };
}
@@ -17,19 +20,151 @@ export interface GolemManaCost {
amount: number;
}
// ─── Unlock Requirements ─────────────────────────────────────────────────
export interface GolemUnlockRequirement {
type: 'attunement_level' | 'mana_unlocked' | 'dual_attunement' | 'guardian_pact';
attunement?: string;
level?: number;
manaType?: string;
attunements?: string[];
levels?: number[];
}
// ─── Core Definition ─────────────────────────────────────────────────────
export type CoreId = 'basic' | 'intermediate' | 'advanced' | 'guardian';
export interface CoreDefinition {
id: CoreId;
tier: 1 | 2 | 3 | 4;
name: string;
description: string;
/** Mana types available (basic = [earth], guardian = guardian-specific) */
manaTypes: string[];
manaCapacity: number;
manaRegen: number;
maxRoomDuration: number;
summonCost: GolemManaCost[];
/** Primary mana type for upkeep calculation */
primaryManaType: string;
tierMultiplier: number; // For enchantment capacity: 1.0 / 1.5 / 2.0 / 3.0
unlockRequirement: GolemUnlockRequirement;
}
// ─── Frame Definition ────────────────────────────────────────────────────
export type FrameId = 'earth' | 'sand' | 'frost' | 'crystal' | 'steel' | 'shadowglass' | 'crystalSteelHybrid';
export type FrameSpecial = 'none' | 'aoe' | 'slow' | 'guardianConstruct';
export interface FrameDefinition {
id: FrameId;
name: string;
description: string;
baseDamage: number;
attackSpeed: number; // Attacks per in-game hour
armorPierce: number; // 01 fraction of enemy armor bypassed
magicAffinity: number; // 0.01.0+, spell damage efficiency
aoeTargets: number; // 1 = single target, >1 = AoE
/** Element for elemental matchup (derived from unlock mana type) */
element?: string;
specialEffect: FrameSpecial;
summonCost: GolemManaCost[];
unlockRequirement: GolemUnlockRequirement;
}
// ─── Mind Circuit Definition ────────────────────────────────────────────
export type MindCircuitId = 'simple' | 'intermediate' | 'advanced' | 'guardian';
export type CircuitBehavior = 'basicOnly' | 'castSpell1' | 'alternate2' | 'cycleAll';
export interface MindCircuitDefinition {
id: MindCircuitId;
name: string;
description: string;
spellSlots: number;
behavior: CircuitBehavior;
summonCost: GolemManaCost[];
unlockRequirement: GolemUnlockRequirement;
}
// ─── Golem Enchantment Definition ───────────────────────────────────────
export interface GolemEnchantmentDefinition {
id: string;
name: string;
description: string;
effect: string;
capacityCost: number;
summonCost: GolemManaCost[];
}
// ─── Golem Design (Player-Created) ──────────────────────────────────────
export interface GolemDesign {
id: string; // Player-assigned or auto-generated
name: string; // Player-defined name
core: CoreDefinition;
frame: FrameDefinition;
mindCircuit: MindCircuitDefinition;
enchantments: GolemEnchantmentDefinition[]; // Optional, 0-N
/** Player-selected mana types for cores that support choice */
selectedManaTypes: string[];
/** Player-selected spell IDs for mind circuits with spell slots */
selectedSpells: string[];
}
// ─── Computed Design Stats (derived from components) ────────────────────
export interface ComputedGolemStats {
maxRoomDuration: number;
totalSummonCost: GolemManaCost[];
upkeepCostPerHour: GolemManaCost[];
manaCapacity: number;
manaRegen: number;
baseDamage: number;
attackSpeed: number;
armorPierce: number;
magicAffinity: number;
aoeTargets: number;
spellSlots: number;
availableManaTypes: string[];
enchantmentCapacity: number;
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;
}
// ─── Legacy Type (kept for backward compat during migration) ────────────
/** @deprecated Use GolemDesign instead */
export interface GolemDef {
id: string;
name: string;
description: string;
baseManaType: string; // The primary mana type this golem uses
summonCost: GolemManaCost[]; // Cost to summon (can be multiple types)
maintenanceCost: GolemManaCost[]; // Cost per hour to maintain
damage: number; // Base damage per attack
attackSpeed: number; // Attacks per hour
hp: number; // Golem HP (for display, they don't take damage)
armorPierce: number; // Armor piercing (0-1)
isAoe: boolean; // Whether golem attacks are AOE
aoeTargets: number; // Number of targets for AOE
baseManaType: string;
summonCost: GolemManaCost[];
maintenanceCost: GolemManaCost[];
damage: number;
attackSpeed: number;
hp: number;
armorPierce: number;
isAoe: boolean;
aoeTargets: number;
unlockCondition: {
type: 'attunement_level' | 'mana_unlocked' | 'dual_attunement';
attunement?: string;
@@ -38,7 +173,7 @@ export interface GolemDef {
attunements?: string[];
levels?: number[];
};
tier: number; // Power tier (1-4)
maxRoomDuration: number; // Rooms before golem disappears (spec §9.6)
specialAbilities?: { name: string; description: string }[]; // Special abilities
tier: number;
maxRoomDuration: number;
specialAbilities?: { name: string; description: string }[];
}
+189 -174
View File
@@ -1,204 +1,219 @@
// ─── Golem Helper Functions ─────────────────────────
// ─── Golem Helper Functions ──────────────────────────────────────────────
// Component-based construction system utilities.
import type { GolemDef, GolemManaCost } from './types';
import { GOLEMS_DEF } from './golems-data';
import type {
ComputedGolemStats,
GolemDesign,
GolemManaCost,
GolemUnlockRequirement,
ActiveGolemV2,
} from './types';
import { CORES } from './cores';
import { FRAMES } from './frames';
import { MIND_CIRCUITS } from './mindCircuits';
// Get golem slots based on Fabricator attunement level
// Level 2 = 1, Level 4 = 2, Level 6 = 3, Level 8 = 4, Level 10 = 5
// ─── Golem Slots ──────────────────────────────────────────────────────────
/**
* Get base golem slots from Fabricator attunement level.
* Level 2 = 1, Level 4 = 2, Level 6 = 3, Level 8 = 4, Level 10 = 5
*/
export function getGolemSlots(fabricatorLevel: number): number {
if (fabricatorLevel < 2) return 0;
return Math.floor(fabricatorLevel / 2);
}
// Check if a golem is unlocked based on player state
export function isGolemUnlocked(
golemId: string,
// ─── Unlock Checks ────────────────────────────────────────────────────────
/**
* Check if a component is unlocked based on player state.
*/
export function isComponentUnlocked(
requirement: GolemUnlockRequirement,
attunements: Record<string, { active: boolean; level: number }>,
unlockedElements: string[]
unlockedElements: string[],
signedGuardianPacts: number[],
): boolean {
const golem = GOLEMS_DEF[golemId];
if (!golem) return false;
const condition = golem.unlockCondition;
switch (condition.type) {
case 'attunement_level':
const attState = attunements[condition.attunement || ''];
return attState?.active && (attState.level || 1) >= (condition.level || 1);
switch (requirement.type) {
case 'attunement_level': {
const attState = attunements[requirement.attunement || ''];
return !!attState?.active && (attState.level || 1) >= (requirement.level || 1);
}
case 'mana_unlocked':
return unlockedElements.includes(condition.manaType || '');
case 'dual_attunement':
if (!condition.attunements || !condition.levels) return false;
return condition.attunements.every((attId, idx) => {
return unlockedElements.includes(requirement.manaType || '');
case 'dual_attunement': {
if (!requirement.attunements || !requirement.levels) return false;
return requirement.attunements.every((attId, idx) => {
const att = attunements[attId];
return att?.active && (att.level || 1) >= condition.levels![idx];
return att?.active && (att.level || 1) >= requirement.levels![idx];
});
}
case 'guardian_pact': {
// Requires dual attunement plus at least one guardian pact
if (!requirement.attunements || !requirement.levels) return false;
const attOk = requirement.attunements.every((attId, idx) => {
const att = attunements[attId];
return att?.active && (att.level || 1) >= requirement.levels![idx];
});
return attOk && signedGuardianPacts.length > 0;
}
default:
return false;
}
}
// Get all unlocked golems for a player
export function getUnlockedGolems(
attunements: Record<string, { active: boolean; level: number }>,
unlockedElements: string[]
): GolemDef[] {
return Object.values(GOLEMS_DEF).filter(golem =>
isGolemUnlocked(golem.id, attunements, unlockedElements)
) as GolemDef[];
// ─── Computed Stats ───────────────────────────────────────────────────────
/**
* Compute all derived stats for a golem design from its components.
*/
export function computeGolemStats(design: GolemDesign): ComputedGolemStats {
const core = design.core;
const frame = design.frame;
const circuit = design.mindCircuit;
const enchantments = design.enchantments;
// Total summon cost from all components
const totalSummonCost: GolemManaCost[] = [
...core.summonCost,
...frame.summonCost,
...circuit.summonCost,
...enchantments.flatMap((e) => e.summonCost),
];
// Player upkeep = Core.manaRegen × 2 per hour (spec §13)
const upkeepCostPerHour: GolemManaCost[] = [
{
type: 'element',
element: core.primaryManaType,
amount: core.manaRegen * 2,
},
];
// Enchantment capacity = Frame.MagicAffinity × Core.TierMultiplier
const enchantmentCapacity = frame.magicAffinity * core.tierMultiplier;
return {
maxRoomDuration: core.maxRoomDuration,
totalSummonCost,
upkeepCostPerHour,
manaCapacity: core.manaCapacity,
manaRegen: core.manaRegen,
baseDamage: frame.baseDamage,
attackSpeed: frame.attackSpeed,
armorPierce: frame.armorPierce,
magicAffinity: frame.magicAffinity,
aoeTargets: frame.aoeTargets,
spellSlots: circuit.spellSlots,
availableManaTypes: design.selectedManaTypes.length > 0
? design.selectedManaTypes
: core.manaTypes,
enchantmentCapacity,
specialEffect: frame.specialEffect,
};
}
// Calculate golem damage with skill bonuses
// ─── Summoning Cost Checks ────────────────────────────────────────────────
/**
* Check if player can afford to summon a golem design.
*/
export function canAffordGolemDesign(
design: GolemDesign,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
): { canAfford: boolean; missing: string } {
const stats = computeGolemStats(design);
for (const cost of stats.totalSummonCost) {
if (cost.type === 'raw') {
if (rawMana < cost.amount) {
return { canAfford: false, missing: `raw mana (${cost.amount} needed)` };
}
} else if (cost.element) {
const elem = elements[cost.element];
if (!elem || !elem.unlocked) {
return { canAfford: false, missing: `${cost.element} mana (not unlocked)` };
}
if (elem.current < cost.amount) {
return { canAfford: false, missing: `${cost.element} mana (${cost.amount} needed, have ${elem.current})` };
}
}
}
return { canAfford: true, missing: '' };
}
// ─── Active Golem V2 Helpers ──────────────────────────────────────────────
/**
* Create a new ActiveGolemV2 from a GolemDesign for combat.
*/
export function createActiveGolem(
design: GolemDesign,
currentFloor: number,
): ActiveGolemV2 {
return {
designId: design.id,
design,
summonedFloor: currentFloor,
attackProgress: 0,
roomsRemaining: design.core.maxRoomDuration,
currentMana: design.core.manaCapacity, // Starts full
spellCastIndex: 0,
};
}
// ─── Component Lookups ────────────────────────────────────────────────────
/** Get a CoreDefinition by ID */
export function getCore(id: string) {
return CORES[id] || null;
}
/** Get a FrameDefinition by ID */
export function getFrame(id: string) {
return FRAMES[id] || null;
}
/** Get a MindCircuitDefinition by ID */
export function getMindCircuit(id: string) {
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>
_skills: Record<string, number>,
): number {
const golem = GOLEMS_DEF[golemId];
if (!golem) return 0;
let damage = golem.damage;
// Golem Mastery skill bonus
const masteryBonus = 1 + (skills.golemMastery || 0) * 0.1;
damage *= masteryBonus;
return damage;
// Legacy lookup — returns 0 for component-based golems
return 0;
}
// Calculate golem attack speed with skill bonuses
/**
* @deprecated Use computeGolemStats instead
*/
export function getGolemAttackSpeed(
golemId: string,
skills: Record<string, number>
_skills: Record<string, number>,
): number {
const golem = GOLEMS_DEF[golemId];
if (!golem) return 0;
let speed = golem.attackSpeed;
// Golem Efficiency skill bonus
const efficiencyBonus = 1 + (skills.golemEfficiency || 0) * 0.05;
speed *= efficiencyBonus;
return speed;
return 0;
}
// Get floors golems can last (base 1, +1 per Golem Longevity skill level)
export function getGolemFloorDuration(skills: Record<string, number>): number {
return 1 + (skills.golemLongevity || 0);
}
// Get maintenance cost multiplier (Golem Siphon reduces by 10% per level)
export function getGolemMaintenanceMultiplier(skills: Record<string, number>): number {
return 1 - (skills.golemSiphon || 0) * 0.1;
}
// Check if player can afford golem summon cost
export function canAffordGolemSummon(
golemId: string,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>
): boolean {
const golem = GOLEMS_DEF[golemId];
if (!golem) return false;
for (const cost of golem.summonCost) {
if (cost.type === 'raw') {
if (rawMana < cost.amount) return false;
} else if (cost.element) {
const elem = elements[cost.element];
if (!elem || !elem.unlocked || elem.current < cost.amount) return false;
}
}
return true;
}
// Deduct golem summon cost from mana pools
export function deductGolemSummonCost(
golemId: string,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>
): { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> } {
const golem = GOLEMS_DEF[golemId];
if (!golem) return { rawMana, elements };
let newRawMana = rawMana;
let newElements = { ...elements };
for (const cost of golem.summonCost) {
if (cost.type === 'raw') {
newRawMana -= cost.amount;
} else if (cost.element && newElements[cost.element]) {
newElements = {
...newElements,
[cost.element]: {
...newElements[cost.element],
current: newElements[cost.element].current - cost.amount,
},
};
}
}
return { rawMana: newRawMana, elements: newElements };
}
// Check if player can afford golem maintenance for one tick
export function canAffordGolemMaintenance(
golemId: string,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
skills: Record<string, number>
): boolean {
const golem = GOLEMS_DEF[golemId];
if (!golem) return false;
const maintenanceMult = getGolemMaintenanceMultiplier(skills);
for (const cost of golem.maintenanceCost) {
const adjustedAmount = cost.amount * maintenanceMult;
if (cost.type === 'raw') {
if (rawMana < adjustedAmount) return false;
} else if (cost.element) {
const elem = elements[cost.element];
if (!elem || !elem.unlocked || elem.current < adjustedAmount) return false;
}
}
return true;
}
// Deduct golem maintenance cost for one tick
export function deductGolemMaintenance(
golemId: string,
rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>,
skills: Record<string, number>
): { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> } {
const golem = GOLEMS_DEF[golemId];
if (!golem) return { rawMana, elements };
const maintenanceMult = getGolemMaintenanceMultiplier(skills);
let newRawMana = rawMana;
let newElements = { ...elements };
for (const cost of golem.maintenanceCost) {
const adjustedAmount = cost.amount * maintenanceMult;
if (cost.type === 'raw') {
newRawMana -= adjustedAmount;
} else if (cost.element && newElements[cost.element]) {
newElements = {
...newElements,
[cost.element]: {
...newElements[cost.element],
current: newElements[cost.element].current - adjustedAmount,
},
};
}
}
return { rawMana: newRawMana, elements: newElements };
/**
* @deprecated Component-based system doesn't use skill-based maintenance multiplier
*/
export function getGolemMaintenanceMultiplier(_skills: Record<string, number>): number {
return 1;
}