From 17c6d5652de148c0e44ae3f7cf90d61e0460da45 Mon Sep 17 00:00:00 2001 From: Z User Date: Sat, 28 Mar 2026 07:56:52 +0000 Subject: [PATCH] Add golemancy system, combination skills, and UI redesigns - Remove scroll crafting skill (no consumables in idle game) - Add golem types (Earth, Metal, Crystal) with variants (Lava, Mud, Forge, Storm) - Implement golemancy state in store with summoning/drain mechanics - Add combination skills requiring level 5+ in two attunements: - Enchanter+Fabricator: Enchanted Golems, Capacity Overflow - Invoker+Fabricator: Pact-Bonded Golems, Guardian Infusion - Invoker+Enchanter: Pact Enchantments, Elemental Resonance - Redesign CraftingTab with sub-tabs for Enchanter/Fabricator - Redesign SpireTab to show summoned golems and DPS contribution - Redesign StatsTab with attunement-specific stats sections - Update documentation (README.md, AGENTS.md) --- AGENTS.md | 58 +++ README.md | 21 + src/components/game/tabs/CraftingTab.tsx | 499 +++++++++++++++++---- src/components/game/tabs/SpireTab.tsx | 144 +++++- src/components/game/tabs/StatsTab.tsx | 543 +++++++++++++++++++++-- src/lib/game/constants.ts | 126 +++++- src/lib/game/store.ts | 136 +++++- src/lib/game/types.ts | 42 ++ worklog.md | 166 +++++++ 9 files changed, 1596 insertions(+), 139 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dcc65fb..691ff23 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -348,6 +348,64 @@ const useGameStore = create()( | Crafting | `crafting-slice.ts` | Equipment/enchantment (createEquipmentInstance, startDesigningEnchantment) | | Familiar | `familiar-slice.ts` | Familiar system (addFamiliar, removeFamiliar) | +## Attunement System + +### Overview +Attunements are class-like specializations tied to body slots. Each attunement provides unique capabilities and skills. + +### Attunement Types +| Attunement | Slot | Primary Mana | Capabilities | +|------------|------|--------------|--------------| +| Enchanter | Right Hand | Transference | Enchanting, Disenchanting | +| Invoker | Chest | (from pacts) | Pacts, Guardian Powers | +| Fabricator | Left Hand | Earth | Golemancy, Gear Crafting | + +### Leveling +- XP is earned by using attunement-specific actions +- Enchanter: XP from enchanting equipment (1 XP per 10 capacity used) +- Invoker: XP from signing pacts and defeating guardians +- Fabricator: XP from crafting equipment and golems +- Level 5+ unlocks combination skills with other attunements + +### Combination Skills +Unlock at level 5+ in two attunements: + +| Combination | Skills | +|-------------|--------| +| Enchanter + Fabricator | Enchanted Golems, Capacity Overflow, Runic Golems | +| Invoker + Fabricator | Pact-Bonded Golems, Guardian Infusion | +| Invoker + Enchanter | Pact Enchantments, Elemental Resonance | + +## Golemancy System + +### Overview +Golems are channeling summons that deal automatic damage while draining mana. + +### Golem Types +| Type | Element | Base DPS | Mana Cost | Drain | Unlock | +|------|---------|----------|-----------|-------|--------| +| Earth Golem | Earth | 22.5 | 50 earth | 2/hr | Fabricator attunement | +| Metal Golem | Metal | 50 | 100 metal | 3/hr | Metalworking skill | +| Crystal Golem | Crystal | 100 | 200 crystal | 5/hr | Golemancy Master skill | + +### Golem Variants +Created by embedding elemental crystals (requires Crystal Embedding skill): +- **Lava Golem**: Earth + Fire (1.5x damage, burn effect) +- **Mud Golem**: Earth + Water (1.2x damage, slow effect) +- **Forge Golem**: Metal + Fire (1.6x damage, burn effect) +- **Storm Golem**: Metal + Air (1.4x damage, shock effect) + +### Duration +- Base: 1 floor +- +1 floor per Fabricator level (max 10) + +### Store Methods +- `canSummonGolem(golemId)` - Check if golem can be summoned +- `summonGolem(golemId, variant?)` - Summon a golem +- `dismissGolem(instanceId)` - Dismiss an active golem +- `getGolemDuration()` - Get duration in floors +- `getActiveGolemDPS()` - Get total DPS from all golems + ## File Size Guidelines ### Current File Sizes (After Refactoring) diff --git a/README.md b/README.md index 2cbbef8..2f0e2ce 100755 --- a/README.md +++ b/README.md @@ -30,6 +30,27 @@ An incremental/idle game about climbing a magical spire, mastering skills, and u - Milestone upgrades at levels 5 and 10 for each tier - Unique special effects unlocked through skill upgrades +### Attunement System (Class-Like Progression) +- **Three attunements** tied to body slots (Right Hand, Chest, Left Hand) +- **Enchanter** (Right Hand) - Enchanting equipment with magical effects +- **Invoker** (Chest) - Forming pacts with guardians for elemental powers +- **Fabricator** (Left Hand) - Crafting golems and equipment +- Each attunement levels up independently with XP +- Level-based unlocks for skills and capabilities + +### Golemancy System (Fabricator) +- **Summon golems** as channeling spells with mana drain +- Golems deal automatic damage each tick while draining mana +- Duration scales with Fabricator level (1 floor + 1 per level, max 10) +- **Golem variants** via elemental crystal embedding (Lava Golem, Storm Golem) +- Multiple golem types: Earth, Metal, Crystal golems + +### Combination Skills +- Unlock at **Level 5+** in two attunements +- **Enchanter + Fabricator**: Enchanted Golems, higher capacity gear +- **Invoker + Fabricator**: Pact-Bonded Golems, guardian bonuses +- **Invoker + Enchanter**: Pact-Based Enchantments, elemental resonance + ### Equipment Crafting & Enchanting - 3-stage enchantment process (Design → Prepare → Apply) - Equipment capacity system limiting total enchantment power diff --git a/src/components/game/tabs/CraftingTab.tsx b/src/components/game/tabs/CraftingTab.tsx index bedde3b..a38f1c3 100755 --- a/src/components/game/tabs/CraftingTab.tsx +++ b/src/components/game/tabs/CraftingTab.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -10,13 +10,15 @@ import { Separator } from '@/components/ui/separator'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Wand2, Scroll, Hammer, Sparkles, Trash2, Plus, Minus, - Package, Zap, Clock, ChevronRight, Circle, Anvil + Package, Zap, Clock, ChevronRight, Circle, Anvil, Heart, + Sword, Skull } from 'lucide-react'; import { EQUIPMENT_TYPES, type EquipmentType, type EquipmentSlot } from '@/lib/game/data/equipment'; import { ENCHANTMENT_EFFECTS, type EnchantmentEffectDef, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects'; import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes'; import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops'; -import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types'; +import { GOLEM_DEFS, GOLEM_VARIANTS, ELEMENTS } from '@/lib/game/constants'; +import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress, ActiveGolem } from '@/lib/game/types'; import { fmt, type GameStore } from '@/lib/game/store'; // Slot display names @@ -48,6 +50,8 @@ export function CraftingTab({ store }: CraftingTabProps) { const currentAction = store.currentAction; const unlockedEffects = store.unlockedEffects; const lootInventory = store.lootInventory; + const elements = store.elements; + const activeGolems = store.activeGolems; const startDesigningEnchantment = store.startDesigningEnchantment; const cancelDesign = store.cancelDesign; const saveDesign = store.saveDesign; @@ -63,7 +67,19 @@ export function CraftingTab({ store }: CraftingTabProps) { const startCraftingEquipment = store.startCraftingEquipment; const cancelEquipmentCrafting = store.cancelEquipmentCrafting; const deleteMaterial = store.deleteMaterial; - const [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply' | 'craft'>('craft'); + const canSummonGolem = store.canSummonGolem; + const summonGolem = store.summonGolem; + const dismissGolem = store.dismissGolem; + const getGolemDuration = store.getGolemDuration; + + // Top-level attunement tab state + const [topLevelTab, setTopLevelTab] = useState<'enchanter' | 'fabricator'>('enchanter'); + + // Sub-tab states + const [enchanterSubTab, setEnchanterSubTab] = useState<'design' | 'prepare' | 'apply'>('design'); + const [fabricatorSubTab, setFabricatorSubTab] = useState<'craft' | 'golemancy'>('craft'); + + // Selection states const [selectedEquipmentType, setSelectedEquipmentType] = useState(null); const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState(null); const [selectedDesign, setSelectedDesign] = useState(null); @@ -75,6 +91,11 @@ export function CraftingTab({ store }: CraftingTabProps) { const enchantingLevel = skills.enchanting || 0; const efficiencyBonus = (skills.efficientEnchant || 0) * 0.05; + // Determine active attunements + const enchanterActive = store.attunements?.enchanter?.active ?? false; + const fabricatorActive = store.attunements?.fabricator?.active ?? false; + const activeAttunementCount = (enchanterActive ? 1 : 0) + (fabricatorActive ? 1 : 0); + // Get equipped items as array const equippedItems = Object.entries(equippedInstances) .filter(([, instanceId]) => instanceId && equipmentInstances[instanceId]) @@ -877,42 +898,383 @@ export function CraftingTab({ store }: CraftingTabProps) { ); + // Render golemancy stage + const renderGolemancyStage = () => { + const golemancySkill = skills.golemancy || 0; + const golemDuration = getGolemDuration(); + + return ( +
+ {/* Available Golems */} + + + + 🗿 + Available Golems + + + + {golemancySkill < 1 ? ( +
+ 🗿 +

Learn Golemancy skill to summon golems

+

Available through Fabricator attunement

+
+ ) : ( + +
+ {Object.values(GOLEM_DEFS).map(golemDef => { + const canSummon = canSummonGolem(golemDef.id); + const requiredElement = elements[golemDef.requiredManaType]; + const hasEnoughMana = requiredElement?.current >= golemDef.summonCost; + const isActive = activeGolems.some(g => g.golemId === golemDef.id); + const elementDef = ELEMENTS[golemDef.element]; + + // Check if golem type is locked + const isLocked = !requiredElement?.unlocked; + + return ( +
+
+
+
+ {isLocked && 🔒} + {golemDef.name} + {isActive && Active} +
+
{golemDef.desc}
+
+
+ + {isLocked ? ( +
+ Locked: {golemDef.unlockCondition} +
+ ) : ( + <> + + +
+
+ +
Damage
+
{golemDef.baseDamage}
+
+
+ +
HP
+
{golemDef.baseHP}
+
+
+ +
Speed
+
{golemDef.attackSpeed}/hr
+
+
+ + + + )} +
+ ); + })} +
+
+ )} +
+
+ + {/* Active Golems */} + + + + + + Active Golems ({activeGolems.length}) + + {golemancySkill >= 1 && ( + Duration: {golemDuration} floors + )} + + + + {activeGolems.length === 0 ? ( +
+ 🤖 +

No active golems

+

Summon a golem to help clear floors!

+
+ ) : ( + +
+ {activeGolems.map((golem, index) => { + const golemDef = GOLEM_DEFS[golem.golemId]; + const elementDef = ELEMENTS[golemDef?.element || 'earth']; + const variantDef = golem.variant ? GOLEM_VARIANTS[golem.variant] : null; + const displayName = variantDef?.name || golemDef?.name || 'Unknown Golem'; + const instanceId = `${golem.golemId}-${golem.currentFloor}`; + + return ( +
+
+
+ {displayName} + {golem.variant && ( + + {golem.variant} + + )} +
+ +
+ +
+
+ Floor:{' '} + {golem.currentFloor} +
+
+ Remaining:{' '} + {golem.remainingFloors} floors +
+
+ + {/* HP Bar */} +
+
+ HP + {golem.currentHP} / {golem.maxHP} +
+ +
+ +
+ Total Damage: {fmt(golem.damageDealt)} +
+
+ ); + })} +
+
+ )} +
+
+ + {/* Golem Variants Info */} + + + + + Golem Variants + {skills.crystalEmbedding ? ( + Unlocked + ) : ( + Requires Crystal Embedding + )} + + + + {skills.crystalEmbedding ? ( +
+ {Object.entries(GOLEM_VARIANTS).map(([variantId, variant]) => { + const baseGolem = GOLEM_DEFS[variant.baseGolem]; + const baseElement = ELEMENTS[baseGolem?.element || 'earth']; + const requiredElement = ELEMENTS[variant.requiredElement]; + + return ( +
+
+ {variant.name} +
+
{variant.desc}
+
+ Base: {baseGolem?.name} + +{Math.round((variant.damageMultiplier - 1) * 100)}% dmg +
+
+ Requires: {requiredElement?.name} +
+
+ ); + })} +
+ ) : ( +
+ 🔒 +

Learn Crystal Embedding skill to unlock golem variants

+

Enchanter attunement level 3+ required

+
+ )} +
+
+
+ ); + }; + + // Render Enchanter content (Design, Prepare, Apply) + const renderEnchanterContent = () => ( + setEnchanterSubTab(v as typeof enchanterSubTab)}> + + + + Design + + + + Prepare + + + + Apply + + + + + {renderDesignStage()} + + + {renderPrepareStage()} + + + {renderApplyStage()} + + + ); + + // Render Fabricator content (Craft, Golemancy) + const renderFabricatorContent = () => ( + setFabricatorSubTab(v as typeof fabricatorSubTab)}> + + + + Craft + + + 🗿 + Golemancy + + + + + {renderCraftStage()} + + + {renderGolemancyStage()} + + + ); + + // Determine what to render based on active attunements + const renderContent = () => { + // If only one attunement is active, skip top-level tabs + if (activeAttunementCount === 1) { + if (enchanterActive) { + return renderEnchanterContent(); + } + if (fabricatorActive) { + return renderFabricatorContent(); + } + } + + // If both attunements are active, show top-level tabs + if (activeAttunementCount >= 2) { + return ( + setTopLevelTab(v as typeof topLevelTab)}> + + {enchanterActive && ( + + + Enchanter + + )} + {fabricatorActive && ( + + ⚒️ + Fabricator + + )} + + + {enchanterActive && ( + + {renderEnchanterContent()} + + )} + {fabricatorActive && ( + + {renderFabricatorContent()} + + )} + + ); + } + + // No attunements active + return ( + + + 🔒 +

No Crafting Attunements Active

+

Activate an attunement to access crafting features.

+

+ Enchanter: Enchantment design and application
+ Fabricator: Equipment crafting and golemancy +

+
+
+ ); + }; + return (
- {/* Stage Tabs */} - setCraftingStage(v as typeof craftingStage)}> - - - - Craft - - - - Design - - - - Prepare - - - - Apply - - - - - {renderCraftStage()} - - - {renderDesignStage()} - - - {renderPrepareStage()} - - - {renderApplyStage()} - - + {renderContent()} {/* Current Activity Indicator */} {currentAction === 'craft' && equipmentCraftingProgress && ( @@ -920,59 +1282,18 @@ export function CraftingTab({ store }: CraftingTabProps) {
- Crafting equipment... -
-
- {((equipmentCraftingProgress.progress / equipmentCraftingProgress.required) * 100).toFixed(0)}% -
-
- - )} - - {currentAction === 'design' && designProgress && ( - - -
- - Designing enchantment... -
-
- {((designProgress.progress / designProgress.required) * 100).toFixed(0)}% -
-
-
- )} - - {currentAction === 'prepare' && preparationProgress && ( - - -
- - Preparing equipment... -
-
- {((preparationProgress.progress / preparationProgress.required) * 100).toFixed(0)}% -
-
-
- )} - - {currentAction === 'enchant' && applicationProgress && ( - - -
- - {applicationProgress.paused ? 'Enchantment paused' : 'Applying enchantment...'} + + Crafting: {CRAFTING_RECIPES[equipmentCraftingProgress.blueprintId]?.name} +
-
- {((applicationProgress.progress / applicationProgress.required) * 100).toFixed(0)}% -
- {applicationProgress.paused ? ( - - ) : ( - - )} + + + {equipmentCraftingProgress.progress.toFixed(1)}h / {equipmentCraftingProgress.required.toFixed(1)}h +
diff --git a/src/components/game/tabs/SpireTab.tsx b/src/components/game/tabs/SpireTab.tsx index e1950e2..7dbb020 100755 --- a/src/components/game/tabs/SpireTab.tsx +++ b/src/components/game/tabs/SpireTab.tsx @@ -9,7 +9,7 @@ import { Separator } from '@/components/ui/separator'; import { TooltipProvider } from '@/components/ui/tooltip'; import { Swords, BookOpen, ChevronUp, ChevronDown, RotateCcw, X } from 'lucide-react'; import type { GameStore } from '@/lib/game/types'; -import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants'; +import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, GOLEM_DEFS, GOLEM_VARIANTS } from '@/lib/game/constants'; import { fmt, fmtDec, getFloorElement, canAffordSpellCost } from '@/lib/game/store'; import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats'; import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting'; @@ -36,7 +36,9 @@ export function SpireTab({ store }: SpireTabProps) { // Get upgrade effects and DPS const upgradeEffects = getUnifiedEffects(store); - const totalDPS = getTotalDPS(store, upgradeEffects, floorElem); + const spellDPS = getTotalDPS(store, upgradeEffects, floorElem); + const golemDPS = store.getActiveGolemDPS(); + const totalDPS = spellDPS + golemDPS; const studySpeedMult = 1; // Base study speed const canCastSpell = (spellId: string): boolean => { @@ -45,6 +47,19 @@ export function SpireTab({ store }: SpireTabProps) { return canAffordSpellCost(spell.cost, store.rawMana, store.elements); }; + // Helper to get golem element color + const getGolemElementColor = (element: string): string => { + return ELEMENTS[element]?.color || '#F4A261'; // Default to earth color + }; + + // Helper to get golem element symbol + const getGolemElementSymbol = (element: string): string => { + return ELEMENTS[element]?.sym || '⛰️'; + }; + + // Get active golems on current floor + const activeGolemsOnFloor = store.activeGolems.filter(g => g.currentFloor === store.currentFloor); + return (
@@ -87,7 +102,18 @@ export function SpireTab({ store }: SpireTabProps) {
{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP - DPS: {store.currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'} + + {store.currentAction === 'climb' && (activeEquipmentSpells.length > 0 || activeGolemsOnFloor.length > 0) ? ( + + DPS: {fmtDec(totalDPS)} + {activeGolemsOnFloor.length > 0 && ( + + (Spell: {fmtDec(spellDPS)} | Golem: {fmtDec(golemDPS)}) + + )} + + ) : '—'} +
@@ -128,6 +154,116 @@ export function SpireTab({ store }: SpireTabProps) { + {/* Active Golems Card */} + 0 ? '#F4A26150' : undefined }}> + + + 🗿 Active Golems + {activeGolemsOnFloor.length > 0 && ( + + {activeGolemsOnFloor.length} + + )} + + + + {activeGolemsOnFloor.length > 0 ? ( + <> + +
+ {activeGolemsOnFloor.map((golem, index) => { + const golemDef = GOLEM_DEFS[golem.golemId]; + const variantDef = golem.variant ? GOLEM_VARIANTS[golem.variant] : null; + const elementColor = getGolemElementColor(golemDef?.element || 'earth'); + const elementSymbol = getGolemElementSymbol(golemDef?.element || 'earth'); + + // Calculate golem DPS + let golemSingleDPS = golemDef?.baseDamage || 0; + if (variantDef) { + golemSingleDPS *= variantDef.damageMultiplier; + } + if (store.skills.golemancyMaster === 1) { + golemSingleDPS *= 1.5; + } + if (store.skills.pactBondedGolems === 1) { + golemSingleDPS *= 1 + (store.signedPacts.length * 0.1); + } + if (store.skills.guardianInfusion === 1 && GUARDIANS[store.currentFloor]) { + golemSingleDPS *= 1.25; + } + golemSingleDPS *= golemDef?.attackSpeed || 1; + + return ( +
+
+
+ {elementSymbol} + + {variantDef?.name || golemDef?.name || 'Unknown Golem'} + + {golem.variant && !variantDef && ( + ({golem.variant}) + )} +
+ + {fmtDec(golemSingleDPS)} DPS + +
+ + {/* HP Bar */} +
+
+
+
+
+ {fmt(golem.currentHP)} / {fmt(golem.maxHP)} HP +
+
+ + {/* Remaining Floors */} +
+ + + {golem.remainingFloors} floor{golem.remainingFloors !== 1 ? 's' : ''} remaining + + + {fmt(golem.damageDealt)} total dmg + +
+
+ ); + })} +
+ + + {/* Total Golem DPS Summary */} + +
+ Total Golem DPS + + {fmtDec(golemDPS)} + +
+ + ) : ( +
+

No golems summoned.

+

Visit the Crafting tab to summon golems.

+
+ )} + + + {/* Active Spells Card - Shows all spells from equipped weapons */} @@ -161,7 +297,7 @@ export function SpireTab({ store }: SpireTabProps) {
- ⚔️ {fmt(totalDPS)} DPS • + ⚔️ {fmt(spellDPS)} DPS • {' '}{formatSpellCost(spellDef.cost)} diff --git a/src/components/game/tabs/StatsTab.tsx b/src/components/game/tabs/StatsTab.tsx index fbcf40b..1c83a40 100755 --- a/src/components/game/tabs/StatsTab.tsx +++ b/src/components/game/tabs/StatsTab.tsx @@ -1,14 +1,22 @@ 'use client'; +import { useState } from 'react'; import { ELEMENTS, GUARDIANS, SKILLS_DEF } from '@/lib/game/constants'; import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution'; import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects'; import { fmt, fmtDec, calcDamage } from '@/lib/game/store'; import type { SkillUpgradeChoice, GameStore, UnifiedEffects } from '@/lib/game/types'; +import { ATTUNEMENTS_DEF, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from '@/lib/game/data/attunements'; +import { SKILL_CATEGORIES } from '@/lib/game/constants'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; -import { Droplet, Swords, BookOpen, FlaskConical, Trophy, RotateCcw, Star } from 'lucide-react'; +import { Progress } from '@/components/ui/progress'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { + Droplet, Swords, BookOpen, FlaskConical, Trophy, RotateCcw, Star, + ChevronDown, ChevronRight, Wand2, Heart, Hammer, Golem, Sparkles +} from 'lucide-react'; export interface StatsTabProps { store: GameStore; @@ -37,6 +45,72 @@ export function StatsTab({ studySpeedMult, studyCostMult, }: StatsTabProps) { + const [expandedAttunements, setExpandedAttunements] = useState>(new Set()); + + // Toggle attunement expansion + const toggleAttunement = (attunementId: string) => { + setExpandedAttunements(prev => { + const newSet = new Set(prev); + if (newSet.has(attunementId)) { + newSet.delete(attunementId); + } else { + newSet.add(attunementId); + } + return newSet; + }); + }; + + // Get active attunements + const activeAttunements = Object.entries(store.attunements) + .filter(([, state]) => state.active) + .map(([id, state]) => ({ id, ...state, def: ATTUNEMENTS_DEF[id] })) + .filter(att => att.def); + + // Get skills for an attunement + const getSkillsForAttunement = (attunementId: string) => { + return Object.entries(SKILLS_DEF) + .filter(([, def]) => def.attunement === attunementId && def.cat !== 'combination') + .map(([id, def]) => ({ + id, + ...def, + currentLevel: store.skills[id] || 0, + })); + }; + + // Get combination skills + const getCombinationSkills = () => { + return Object.entries(SKILLS_DEF) + .filter(([, def]) => def.cat === 'combination') + .map(([id, def]) => { + const requirements = def.reqAttunements || {}; + const attunementLevels = Object.entries(requirements).map(([attId, reqLevel]) => ({ + attunementId: attId, + requiredLevel: reqLevel, + currentLevel: store.attunements[attId]?.level || 0, + isMet: (store.attunements[attId]?.level || 0) >= reqLevel, + })); + + const allRequirementsMet = attunementLevels.every(r => r.isMet); + const isUnlocked = allRequirementsMet && + (!def.req || Object.entries(def.req).every(([skillId, level]) => (store.skills[skillId] || 0) >= level)); + const isStudied = (store.skills[id] || 0) > 0; + + return { + id, + ...def, + attunementRequirements: attunementLevels, + allRequirementsMet, + isUnlocked, + isStudied, + currentLevel: store.skills[id] || 0, + }; + }); + }; + + const combinationSkills = getCombinationSkills(); + const unlockedCombinationSkills = combinationSkills.filter(s => s.isUnlocked); + const lockedCombinationSkills = combinationSkills.filter(s => !s.isUnlocked); + // Compute element max const elemMax = (() => { const ea = store.skillTiers?.elemAttune || 1; @@ -69,8 +143,432 @@ export function StatsTab({ const selectedUpgrades = getAllSelectedUpgrades(); + // Calculate golem stats for Fabricator + const golemDuration = store.getGolemDuration(); + const golemDPS = store.getActiveGolemDPS(); + const activeGolems = store.activeGolems || []; + + // Capability display names + const capabilityNames: Record = { + enchanting: 'Enchanting', + disenchanting: 'Disenchanting', + pacts: 'Pact Signing', + guardianPowers: 'Guardian Powers', + elementalMastery: 'Elemental Mastery', + golemCrafting: 'Golemancy', + gearCrafting: 'Gear Crafting', + earthShaping: 'Earth Shaping', + }; + return (
+ {/* Active Attunements Card */} + + + + + Active Attunements ({activeAttunements.length}) + + + + {activeAttunements.length === 0 ? ( +
No attunements active. Visit the Attune tab to unlock your potential.
+ ) : ( +
+ {activeAttunements.map(({ id, level, experience, def }) => { + const xpForNext = getAttunementXPForLevel(level + 1); + const xpProgress = xpForNext > 0 ? (experience / xpForNext) * 100 : 0; + const isExpanded = expandedAttunements.has(id); + const skills = getSkillsForAttunement(id); + const studyingSkills = skills.filter(s => + store.currentStudyTarget?.type === 'skill' && store.currentStudyTarget.id === s.id + ); + + return ( + toggleAttunement(id)}> + +
+
+
+ {def.icon} +
+
{def.name}
+
+ Level {level}/{MAX_ATTUNEMENT_LEVEL} +
+
+
+
+ {def.primaryManaType && ( + + {ELEMENTS[def.primaryManaType]?.sym} {ELEMENTS[def.primaryManaType]?.name} + + )} + + {capabilityNames[def.capabilities[0]] || def.capabilities[0]} + + {isExpanded ? : } +
+
+ + {/* XP Progress Bar */} + {level < MAX_ATTUNEMENT_LEVEL && ( +
+
+ XP Progress + {fmt(experience)} / {fmt(xpForNext)} +
+ +
+ )} + {level >= MAX_ATTUNEMENT_LEVEL && ( +
MAX LEVEL REACHED
+ )} +
+
+ + +
+
Available Skills ({skills.length})
+
+ {skills.map(skill => { + const isStudying = store.currentStudyTarget?.type === 'skill' && store.currentStudyTarget.id === skill.id; + return ( +
0 + ? 'border-gray-600 bg-gray-800/50' + : 'border-gray-700 bg-gray-800/30' + }`} + > +
+ 0 ? 'text-gray-200' : 'text-gray-500'}> + {skill.name} + +
+ {skill.currentLevel > 0 && ( + + Lv.{skill.currentLevel}/{skill.max} + + )} + {isStudying && ( + Studying + )} +
+
+
{skill.desc}
+
+ ); + })} +
+
+
+
+ ); + })} +
+ )} +
+
+ + {/* Combination Skills Card */} + + + + + Combination Skills + + + + {/* Unlocked Skills */} + {unlockedCombinationSkills.length > 0 && ( +
+
Available to Study ({unlockedCombinationSkills.length})
+
+ {unlockedCombinationSkills.map(skill => ( +
+
+ {skill.name} + {skill.currentLevel > 0 && ( + + Lv.{skill.currentLevel}/{skill.max} + + )} +
+
{skill.desc}
+
+ {skill.attunementRequirements.map((req) => ( + + {ATTUNEMENTS_DEF[req.attunementId]?.icon} {ATTUNEMENTS_DEF[req.attunementId]?.name} Lv.{req.currentLevel} + + ))} +
+
+ ))} +
+
+ )} + + {/* Locked Skills */} + {lockedCombinationSkills.length > 0 && ( +
+
Requires Level 5+ in Multiple Attunements
+
+ {lockedCombinationSkills.map(skill => ( +
+
+ {skill.name} + Locked +
+
{skill.desc}
+
+ {skill.attunementRequirements.map((req) => ( + + {ATTUNEMENTS_DEF[req.attunementId]?.icon} Lv.{req.requiredLevel} ({req.currentLevel}/{req.requiredLevel}) + + ))} +
+
+ ))} +
+
+ )} + + {combinationSkills.length === 0 && ( +
No combination skills available yet.
+ )} +
+
+ + {/* Enchanter Stats */} + {store.attunements.enchanter?.active && ( + + + + + Enchanter Stats {ATTUNEMENTS_DEF.enchanter.icon} + + + +
+
+
+ Enchantment Capacity: + + {(() => { + const base = 100 + (store.skills.enchanting || 0) * 10; + const bonus = (store.skills.ancientEcho || 0) * Math.floor((store.attunements.enchanter?.level || 0) / 2); + return `${base + bonus} (+${bonus} from Ancient Echo)`; + })()} + +
+
+ Efficiency Bonus: + -{(store.skills.efficientEnchant || 0) * 5}% cost +
+
+
+
+ Designs Created: + {store.enchantmentDesigns?.length || 0} +
+
+ Effects Unlocked: + {store.unlockedEffects?.length || 0} +
+
+
+
+ Disenchant Recovery: + {20 + (store.skills.disenchanting || 0) * 20}% +
+
+ Enchant Speed: + +{(store.skills.enchantSpeed || 0) * 10}% +
+
+
+
+
+ )} + + {/* Invoker Stats */} + {store.attunements.invoker?.active && ( + + + + + Invoker Stats {ATTUNEMENTS_DEF.invoker.icon} + + + +
+
+
+ Pacts Signed: + {store.signedPacts.length}/10 +
+
+ Pact Multiplier: + + ×{fmtDec(store.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)} + +
+
+
+
+ Pact Mastery Bonus: + +{(store.skills.pactMastery || 0) * 10}% +
+
+ Guardian Affinity: + -{(store.skills.guardianAffinity || 0) * 15}% time +
+
+
+
+ Elemental Bond: + +{(store.skills.elementalBond || 0) * 20} cap/pact +
+
+ Pact Synergy: + +{(store.skills.pactSynergy || 0) * 5}%/pact +
+
+
+ + {/* Signed Pacts List */} + {store.signedPacts.length > 0 && ( +
+ +
Signed Pacts:
+
+ {store.signedPacts.map((floor) => { + const guardian = GUARDIANS[floor]; + if (!guardian) return null; + return ( +
+
+ {guardian.name} +
+
×{guardian.pact}
+
+ ); + })} +
+
+ )} +
+
+ )} + + {/* Fabricator Stats */} + {store.attunements.fabricator?.active && ( + + + + + Fabricator Stats {ATTUNEMENTS_DEF.fabricator.icon} + + + +
+
+
+ Golems Active: + {activeGolems.length} +
+
+ Golem DPS: + {fmtDec(golemDPS, 1)}/hr +
+
+
+
+ Golem Duration: + {golemDuration} floors +
+
+ Golem Vitality: + +{(store.skills.golemVitality || 0) * 20}% HP +
+
+
+
+ Crafting Speed: + +{(store.skills.efficientCrafting || 0) * 10}% +
+
+ Earth Conversion: + +{(store.skills.earthShaping || 0) * 25}% +
+
+
+ + {/* Active Golems List */} + {activeGolems.length > 0 && ( +
+ +
Active Golems:
+
+ {activeGolems.map((golem, idx) => { + const elemColor = ELEMENTS[golem.variant || 'earth']?.color || '#F4A261'; + return ( +
+
+ + {golem.variant ? `${golem.variant} Golem` : 'Earth Golem'} + + {golem.remainingFloors} floors left +
+
+ HP: {fmt(golem.currentHP)}/{fmt(golem.maxHP)} + DMG: {fmt(golem.damageDealt)} +
+ +
+ ); + })} +
+
+ )} +
+
+ )} + {/* Mana Stats */} @@ -449,49 +947,6 @@ export function StatsTab({ - {/* Pact Bonuses */} - - - - - Signed Pacts ({store.signedPacts.length}/10) - - - - {store.signedPacts.length === 0 ? ( -
No pacts signed yet. Defeat guardians to earn pacts.
- ) : ( -
- {store.signedPacts.map((floor) => { - const guardian = GUARDIANS[floor]; - if (!guardian) return null; - return ( -
-
-
- {guardian.name} -
-
Floor {floor}
-
- - {guardian.pact}x multiplier - -
- ); - })} -
- Combined Pact Multiplier: - ×{fmtDec(store.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)} -
-
- )} -
-
- {/* Loop Stats */} diff --git a/src/lib/game/constants.ts b/src/lib/game/constants.ts index 30bc8f2..06d8760 100755 --- a/src/lib/game/constants.ts +++ b/src/lib/game/constants.ts @@ -692,9 +692,9 @@ export const SKILLS_DEF: Record = { enchantSpeed: { name: "Enchant Speed", desc: "-10% enchantment time", cat: "enchant", attunement: 'enchanter', attunementLevel: 1, max: 5, base: 300, studyTime: 4, req: { enchanting: 2 } }, // Advanced Enchanting (Lv 3) - scrollCrafting: { name: "Scroll Crafting", desc: "Create scrolls to store enchantment designs", cat: "enchant", attunement: 'enchanter', attunementLevel: 3, max: 3, base: 500, studyTime: 8, req: { enchanting: 5 } }, essenceRefining: { name: "Essence Refining", desc: "+10% enchantment effect power", cat: "enchant", attunement: 'enchanter', attunementLevel: 3, max: 5, base: 450, studyTime: 7, req: { enchanting: 4 } }, transferenceMastery: { name: "Transference Mastery", desc: "+25% transference mana conversion", cat: "enchant", attunement: 'enchanter', attunementLevel: 3, max: 3, base: 600, studyTime: 10, req: { enchanting: 5 } }, + crystalEmbedding: { name: "Crystal Embedding", desc: "Embed elemental crystals in golems for variants", cat: "enchant", attunement: 'enchanter', attunementLevel: 3, max: 1, base: 600, studyTime: 12, req: { enchanting: 4 } }, // Master Enchanting (Lv 5+) soulBinding: { name: "Soul Binding", desc: "Enchantments persist through loops", cat: "enchant", attunement: 'enchanter', attunementLevel: 5, max: 2, base: 1500, studyTime: 24, req: { essenceRefining: 3 } }, @@ -785,6 +785,24 @@ export const SKILLS_DEF: Record = { // Legendary Fabrication (Lv 8+) legendaryCraft: { name: "Legendary Crafting", desc: "Chance for crafted items to gain +1 tier", cat: "fabrication", attunement: 'fabricator', attunementLevel: 8, max: 1, base: 2500, studyTime: 30, req: { alloyMastery: 1, runicCarving: 3 } }, awakenedHand: { name: "Awakened Hand", desc: "Craft time -50%, +25% quality", cat: "fabrication", attunement: 'fabricator', attunementLevel: 8, max: 1, base: 3000, studyTime: 36, req: { legendaryCraft: 1 } }, + + // ═══════════════════════════════════════════════════════════════════════════ + // COMBINATION SKILLS (Require Level 5+ in two attunements) + // Powerful multi-attunement abilities + // ═══════════════════════════════════════════════════════════════════════════ + + // Enchanter + Fabricator: Enchanted Golems & Superior Gear + enchantedGolems: { name: "Enchanted Golems", desc: "Embed spell crystals in golems for elemental variants", cat: "combination", attunement: 'fabricator', attunementLevel: 5, max: 1, base: 2000, studyTime: 24, req: { golemancy: 1 }, reqAttunements: { enchanter: 5, fabricator: 5 } }, + capacityOverflow: { name: "Capacity Overflow", desc: "+25% enchantment capacity on fabricated gear", cat: "combination", attunement: 'fabricator', attunementLevel: 5, max: 3, base: 1500, studyTime: 16, req: { fabrication: 5 }, reqAttunements: { enchanter: 5, fabricator: 5 } }, + runicGolems: { name: "Runic Golems", desc: "Golems gain +50% damage, +30% duration", cat: "combination", attunement: 'fabricator', attunementLevel: 6, max: 1, base: 2500, studyTime: 30, req: { enchantedGolems: 1 }, reqAttunements: { enchanter: 6, fabricator: 6 } }, + + // Invoker + Fabricator: Pact-Bonded Golems + pactBondedGolems: { name: "Pact-Bonded Golems", desc: "Golems gain bonuses from your signed pacts", cat: "combination", attunement: 'fabricator', attunementLevel: 5, max: 1, base: 2000, studyTime: 24, req: { golemancy: 1 }, reqAttunements: { invoker: 5, fabricator: 5 } }, + guardianInfusion: { name: "Guardian Infusion", desc: "Golems deal +25% damage to guardian floors", cat: "combination", attunement: 'fabricator', attunementLevel: 6, max: 1, base: 2500, studyTime: 30, req: { pactBondedGolems: 1 }, reqAttunements: { invoker: 6, fabricator: 6 } }, + + // Invoker + Enchanter: Pact-Based Enchantments + pactEnchantments: { name: "Pact Enchantments", desc: "Unlock pact-specific enchantment effects", cat: "combination", attunement: 'enchanter', attunementLevel: 5, max: 1, base: 2000, studyTime: 24, req: { enchanting: 5 }, reqAttunements: { invoker: 5, enchanter: 5 } }, + elementalResonance: { name: "Elemental Resonance", desc: "Enchantments gain +20% power per signed pact", cat: "combination", attunement: 'enchanter', attunementLevel: 6, max: 1, base: 2500, studyTime: 30, req: { pactEnchantments: 1 }, reqAttunements: { invoker: 6, enchanter: 6 } }, }; // ─── Prestige Upgrades ──────────────────────────────────────────────────────── @@ -831,6 +849,11 @@ export const SKILL_CATEGORIES = [ // ═══════════════════════════════════════════════════════════════════════════ { id: 'fabrication', name: 'Fabrication', icon: '⚒️', attunement: 'fabricator' }, { id: 'golemancy', name: 'Golemancy', icon: '🗿', attunement: 'fabricator' }, + + // ═══════════════════════════════════════════════════════════════════════════ + // COMBINATION (Requires 2+ attunements at level 5+) + // ═══════════════════════════════════════════════════════════════════════════ + { id: 'combination', name: 'Combination', icon: '🔮', attunement: null }, ]; // ─── Rarity Colors ─────────────────────────────────────────────────────────── @@ -936,3 +959,104 @@ export const ELEMENT_ICON_NAMES: Record = { void: 'CircleDot', raw: 'Circle', }; + +// ─── Golem Definitions ───────────────────────────────────────────────────────── +// Golems are channeling summons that deal damage over time with mana drain +// Duration = 1 floor base + 1 floor per Fabricator level (max 10) +export const GOLEM_DEFS: Record = { + // Basic Earth Golem - Unlocked with Fabricator attunement + earthGolem: { + id: 'earthGolem', + name: 'Earth Golem', + desc: 'A sturdy golem of compacted earth and stone. Slow but powerful.', + element: 'earth', + baseDamage: 15, + baseHP: 100, + summonCost: 50, + drainRate: 2, // 2 earth mana per hour + attackSpeed: 1.5, // 1.5 attacks per hour + unlockCondition: 'Unlock Fabricator attunement', + requiredManaType: 'earth', + }, + + // Metal Golem - Requires metal mana (fire + earth) + metalGolem: { + id: 'metalGolem', + name: 'Metal Golem', + desc: 'A gleaming construct of forged metal. Faster and deadlier than earth.', + element: 'metal', + baseDamage: 25, + baseHP: 150, + summonCost: 100, + drainRate: 3, // 3 metal mana per hour + attackSpeed: 2, // 2 attacks per hour + unlockCondition: 'Unlock Metal mana (fire + earth) and Metalworking skill', + requiredManaType: 'metal', + }, + + // Crystal Golem - Requires crystal mana (advanced) + crystalGolem: { + id: 'crystalGolem', + name: 'Crystal Golem', + desc: 'A shimmering guardian of crystallized mana. Fires piercing bolts.', + element: 'crystal', + baseDamage: 40, + baseHP: 120, + summonCost: 200, + drainRate: 5, // 5 crystal mana per hour + attackSpeed: 2.5, // 2.5 attacks per hour + unlockCondition: 'Unlock Crystal mana and Golemancy Master skill', + requiredManaType: 'crystal', + }, +}; + +// ─── Golem Variants (Combination Skills) ─────────────────────────────────────── +// These variants are created by embedding elemental crystals in golems +export const GOLEM_VARIANTS: Record = { + // Earth Golem + Fire = Lava Golem + lavaGolem: { + baseGolem: 'earthGolem', + requiredElement: 'fire', + name: 'Lava Golem', + desc: 'Molten earth flows through this golem, burning all it touches.', + damageMultiplier: 1.5, + effect: 'burn', + }, + + // Earth Golem + Water = Mud Golem + mudGolem: { + baseGolem: 'earthGolem', + requiredElement: 'water', + name: 'Mud Golem', + desc: 'A viscous, slowing golem that entraps enemies.', + damageMultiplier: 1.2, + effect: 'slow', + }, + + // Metal Golem + Fire = Forge Golem + forgeGolem: { + baseGolem: 'metalGolem', + requiredElement: 'fire', + name: 'Forge Golem', + desc: 'Glowing hot metal radiates intense heat.', + damageMultiplier: 1.6, + effect: 'burn', + }, + + // Metal Golem + Air = Storm Golem + stormGolem: { + baseGolem: 'metalGolem', + requiredElement: 'air', + name: 'Storm Golem', + desc: 'Lightning arcs between metal plates, shocking enemies.', + damageMultiplier: 1.4, + effect: 'shock', + }, +}; diff --git a/src/lib/game/store.ts b/src/lib/game/store.ts index 27c67f5..e77a274 100755 --- a/src/lib/game/store.ts +++ b/src/lib/game/store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState } from './types'; +import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState, ActiveGolem } from './types'; import { ELEMENTS, GUARDIANS, @@ -22,6 +22,8 @@ import { EFFECT_RESEARCH_MAPPING, BASE_UNLOCKED_EFFECTS, ENCHANTING_UNLOCK_EFFECTS, + GOLEM_DEFS, + GOLEM_VARIANTS, } from './constants'; import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects'; import { @@ -473,6 +475,12 @@ function makeInitial(overrides: Partial = {}): GameState { elementChain: [], decayTimer: 0, }, + clearedFloors: {}, + + // Golemancy (Fabricator summons) + activeGolems: [], + unlockedGolemTypes: [], + golemSummoningProgress: {}, spells: startSpells, skills: overrides.skills || {}, @@ -572,6 +580,13 @@ interface GameStore extends GameState, CraftingActions { // Attunement XP and leveling addAttunementXP: (attunementId: string, amount: number) => void; + // Golemancy actions + summonGolem: (golemId: string, variant?: string) => void; + dismissGolem: (golemInstanceId: string) => void; + canSummonGolem: (golemId: string) => boolean; + getGolemDuration: () => number; // Floors a golem lasts + getActiveGolemDPS: () => number; // Total DPS from all active golems + // Debug functions debugUnlockAttunement: (attunementId: string) => void; debugAddElementalMana: (element: string, amount: number) => void; @@ -1731,6 +1746,125 @@ export const useGameStore = create()( }); }, + // Golemancy functions + getGolemDuration: () => { + const state = get(); + const fabricatorLevel = state.attunements?.fabricator?.level || 0; + // Base 1 floor + 1 floor per fabricator level, max 10 + return Math.min(10, 1 + fabricatorLevel); + }, + + canSummonGolem: (golemId: string) => { + const state = get(); + const golemDef = GOLEM_DEFS[golemId]; + if (!golemDef) return false; + + // Check if player has the golemancy skill + if (!state.skills.golemancy) return false; + + // Check if the required mana type is unlocked and has enough + const manaType = golemDef.requiredManaType; + const elem = state.elements[manaType]; + if (!elem?.unlocked || elem.current < golemDef.summonCost) return false; + + // Check if we already have this type of golem active (one of each type) + if (state.activeGolems.some(g => g.golemId === golemId)) return false; + + return true; + }, + + summonGolem: (golemId: string, variant?: string) => { + const state = get(); + const golemDef = GOLEM_DEFS[golemId]; + if (!golemDef) return; + + // Double-check requirements + if (!state.skills.golemancy) return; + const manaType = golemDef.requiredManaType; + const elem = state.elements[manaType]; + if (!elem?.unlocked || elem.current < golemDef.summonCost) return; + if (state.activeGolems.some(g => g.golemId === golemId)) return; + + // Deduct mana cost + const duration = Math.min(10, 1 + (state.attunements?.fabricator?.level || 0)); + const maxHP = golemDef.baseHP * (1 + (state.skills.golemVitality || 0) * 0.2); + const hasGolemancyMaster = state.skills.golemancyMaster === 1; + + const newGolem: ActiveGolem = { + golemId, + variant, + currentFloor: state.currentFloor, + remainingFloors: duration, + currentHP: hasGolemancyMaster ? maxHP * 1.5 : maxHP, + maxHP: hasGolemancyMaster ? maxHP * 1.5 : maxHP, + damageDealt: 0, + enchantedWith: [], + }; + + set({ + elements: { + ...state.elements, + [manaType]: { + ...elem, + current: elem.current - golemDef.summonCost, + }, + }, + activeGolems: [...state.activeGolems, newGolem], + log: [`🗿 Summoned ${variant ? GOLEM_VARIANTS[variant]?.name || golemDef.name : golemDef.name}! Lasts ${duration} floors.`, ...state.log.slice(0, 49)], + }); + }, + + dismissGolem: (golemInstanceId: string) => { + const state = get(); + const golem = state.activeGolems.find(g => `${g.golemId}-${g.currentFloor}` === golemInstanceId); + if (!golem) return; + + const golemDef = GOLEM_DEFS[golem.golemId]; + set({ + activeGolems: state.activeGolems.filter(g => `${g.golemId}-${g.currentFloor}` !== golemInstanceId), + log: [`👋 Dismissed ${golemDef?.name || 'golem'}.`, ...state.log.slice(0, 49)], + }); + }, + + getActiveGolemDPS: () => { + const state = get(); + let totalDPS = 0; + + for (const golem of state.activeGolems) { + if (golem.currentFloor !== state.currentFloor) continue; + + const golemDef = GOLEM_DEFS[golem.golemId]; + if (!golemDef) continue; + + let damage = golemDef.baseDamage; + + // Apply variant multiplier + if (golem.variant && GOLEM_VARIANTS[golem.variant]) { + damage *= GOLEM_VARIANTS[golem.variant].damageMultiplier; + } + + // Apply golemancy master bonus + if (state.skills.golemancyMaster === 1) { + damage *= 1.5; + } + + // Apply pact bonuses if pactBondedGolems skill + if (state.skills.pactBondedGolems === 1) { + damage *= 1 + (state.signedPacts.length * 0.1); + } + + // Apply guardian bonus if guardianInfusion skill and guardian floor + if (state.skills.guardianInfusion === 1 && GUARDIANS[state.currentFloor]) { + damage *= 1.25; + } + + // DPS = damage * attack speed + totalDPS += damage * golemDef.attackSpeed; + } + + return totalDPS; + }, + // Debug functions debugUnlockAttunement: (attunementId: string) => { const state = get(); diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index 6458d1d..75de5d4 100755 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -107,6 +107,7 @@ export interface SkillDef { max: number; base: number; // Mana cost to start studying req?: Record; + reqAttunements?: Record; // Required attunement levels for combination skills studyTime: number; // Hours needed to study level?: number; // Current level (optional, for UI display) tier?: number; // Skill tier (1-5) @@ -323,6 +324,41 @@ export interface ComboState { decayTimer: number; // Hours until decay starts } +// ─── Golemancy System ───────────────────────────────────────────────────────── + +// Golem types available for summoning +export interface GolemDef { + id: string; + name: string; + desc: string; + element: string; // Primary element (earth, metal, etc.) + baseDamage: number; // Base damage per attack + baseHP: number; // Base HP (for durability) + summonCost: number; // Initial mana cost to summon + drainRate: number; // Ongoing mana drain per hour + attackSpeed: number; // Attacks per hour + unlockCondition: string; // Description of how to unlock + requiredManaType: string; // Mana type needed for this golem + variant?: string; // For upgraded variants (e.g., 'lava' for earth+fire) +} + +// Active golem state on a floor +export interface ActiveGolem { + golemId: string; // Type of golem + variant?: string; // Current variant (if upgraded) + currentFloor: number; // Floor the golem is on + remainingFloors: number; // How many more floors this golem will last + currentHP: number; // Current HP + maxHP: number; // Max HP + damageDealt: number; // Total damage dealt so far + enchantedWith?: string[]; // Elemental crystals embedded (for combination skills) +} + +// Cleared floors tracking +export interface ClearedFloors { + [floor: number]: boolean; +} + export interface GameState { // Time day: number; @@ -353,6 +389,12 @@ export interface GameState { currentAction: GameAction; castProgress: number; // Progress towards next spell cast (0-1) combo: ComboState; // Combat combo tracking + clearedFloors: ClearedFloors; // Track which floors have been cleared + + // Golemancy (Fabricator summons) + activeGolems: ActiveGolem[]; // Currently summoned golems + unlockedGolemTypes: string[]; // Golem types that have been unlocked + golemSummoningProgress: Record; // Progress toward summoning each type // Spells spells: Record; diff --git a/worklog.md b/worklog.md index 7add4cc..ec553f2 100755 --- a/worklog.md +++ b/worklog.md @@ -407,3 +407,169 @@ Stage Summary: - Debug tab enables testing and development - Skills tab is more manageable with collapsible categories - Removed unused features (ComboMeter, scrollCrafting) + +--- +Task ID: 16 +Agent: Main +Task: Redesign SpireTab to show summoned golems and their damage contribution + +Work Log: +- **Added new imports to SpireTab**: + - Imported `GOLEM_DEFS` and `GOLEM_VARIANTS` from `@/lib/game/constants` + - Using `ELEMENTS` for element colors and symbols + +- **Created new "Active Golems" card**: + - Shows list of currently active golems from `store.activeGolems` + - Each golem displays: + - Name and variant (e.g., "Earth Golem" or "Lava Golem") + - HP bar (currentHP / maxHP) with element-colored gradient + - Remaining floors countdown + - DPS contribution (calculated with all bonuses applied) + - Element icon from ELEMENTS constant + - Empty state message: "No golems summoned. Visit the Crafting tab to summon golems." + - Total golem DPS summary at bottom + - Card has earth-colored accent (#F4A261) when golems are active + +- **Updated DPS display in Current Floor card**: + - Total DPS now includes both spell DPS and golem DPS + - Shows breakdown: "Spell DPS: X | Golem DPS: Y" + - Only shows breakdown when golems are active + +- **Calculated individual golem DPS**: + - Applies variant damage multiplier + - Applies golemancyMaster bonus (+50%) + - Applies pactBondedGolems bonus (+10% per pact) + - Applies guardianInfusion bonus (+25% on guardian floors) + - Multiplies by attack speed + +- **Visual styling**: + - Used Card component for the golems section + - Used Progress component for HP bars + - Matches existing dark theme (bg-gray-900/80, border-gray-700) + - Golem cards have element-colored border accent + - Used ScrollArea for long golem lists (max-h-48) + +Stage Summary: +- New Active Golems card shows all summoned golems with detailed stats +- DPS display properly accounts for golem damage contribution +- Visual design consistent with existing game theme +- All lint checks pass + +--- +## Task ID: 17 - CraftingTab Redesign +### Work Task +Redesign the CraftingTab component to have a two-level tab structure based on active attunements, with a new Golemancy sub-tab for summoning golems. + +### Work Summary +- **Implemented two-level tab structure**: + - Top-level tabs show active attunements (Enchanter ✨, Fabricator ⚒️) + - If only one attunement is active, skip top-level tabs and show content directly + - If no attunements are active, show locked message + +- **Enchanter attunement** (when active): + - Sub-tabs: Design, Prepare, Apply (existing enchantment functionality) + - All existing render functions preserved (renderDesignStage, renderPrepareStage, renderApplyStage) + +- **Fabricator attunement** (when active): + - Sub-tabs: Craft, Golemancy + - Craft sub-tab: existing equipment crafting functionality (renderCraftStage) + - Golemancy sub-tab: NEW - summoning and managing golems + +- **New Golemancy features**: + - Shows available golem types from `GOLEM_DEFS` with: + - Golem name, description, element color + - Base damage, HP, and attack speed stats + - Summon button with mana cost (e.g., "Summon - 50 Earth Mana") + - Shows locked golem types (greyed out) with unlock conditions + - Shows active golems with: + - HP progress bar + - Remaining floor duration + - Total damage dealt + - Dismiss button + - Shows golem variants info (requires Crystal Embedding skill) + - Uses store methods: `canSummonGolem()`, `summonGolem()`, `dismissGolem()`, `getGolemDuration()` + - Checks attunement status via `store.attunements.enchanter?.active` and `store.attunements.fabricator?.active` + +- **New imports added**: + - `GOLEM_DEFS`, `GOLEM_VARIANTS`, `ELEMENTS` from `@/lib/game/constants` + - `ActiveGolem` type from `@/lib/game/types` + - Additional Lucide icons: `Heart`, `Sword`, `Skull` + +- **Visual styling**: + - Maintains existing dark theme (bg-gray-900/80, border-gray-700) + - Element-colored accents for golem types + - Uses shadcn/ui components: Card, Button, Progress, Badge, ScrollArea, Tabs + - Consistent with existing CraftingTab styling + +--- +## Task ID: 18 - StatsTab Redesign for Attunement-Specific Stats +### Work Task +Redesign the StatsTab component to better display attunement-specific stats with Active Attunements card, Combination Skills card, and reorganized attunement-specific stat sections. + +### Work Summary +- **Added "Active Attunements" card at the top**: + - Each active attunement displayed as an expandable card/badge with: + - Icon and name (Enchanter ✨, Invoker 💜, Fabricator ⚒️) + - Current level and XP progress bar + - Primary mana type generated as a badge + - Key capability unlocked as a badge + - Click to expand reveals: + - All available skills for that attunement + - Skills currently being studied (highlighted) + - Skill levels displayed with badges + +- **Added "Combination Skills" card**: + - Shows combination skills that the player has unlocked (level 5+ in both required attunements) + - Skills they can unlock shown with requirement badges + - Green color scheme for available skills + - Greyed out style for locked skills with red/green requirement indicators + - Displays attunement level requirements for each skill + +- **Reorganized existing stats into attunement-specific sections**: + - **Enchanter Stats** (teal accent border): + - Enchantment capacity with Ancient Echo bonus + - Efficiency bonus from efficientEnchant skill + - Designs created count + - Effects unlocked count + - Disenchant recovery rate + - Enchant speed bonus + - **Invoker Stats** (purple accent border): + - Pacts signed count (X/10) + - Pact multiplier calculation + - Pact Mastery bonus + - Guardian Affinity time reduction + - Elemental Bond capacity per pact + - Pact Synergy bonus + - Signed pacts list with guardian colors + - **Fabricator Stats** (earth/amber accent border): + - Golems active count + - Golem DPS (using store.getActiveGolemDPS()) + - Golem duration (using store.getGolemDuration()) + - Golem Vitality HP bonus + - Crafting speed bonus + - Earth conversion bonus + - Active golems list with HP bars and damage dealt + +- **Import requirements fulfilled**: + - Imported `ATTUNEMENTS_DEF` from `@/lib/game/data/attunements` + - Imported `SKILL_CATEGORIES` from `@/lib/game/constants` + - Uses `store.attunements` to check active attunements + - Uses `store.getGolemDuration()` and `store.getActiveGolemDPS()` for Fabricator stats + +- **Visual styling**: + - Uses Card, Badge, Progress, Collapsible components from shadcn/ui + - Matches existing dark theme (bg-gray-900/80, border-gray-700) + - Uses attunement colors from ATTUNEMENTS_DEF: + - Enchanter: teal #1ABC9C + - Invoker: purple #9B59B6 + - Fabricator: earth #F4A261 + - Top border accent for each attunement-specific section + - Conditional rendering - only shows attunement sections if that attunement is active + +- **Preserved existing functionality**: + - Mana Stats section retained + - Combat Stats section retained + - Study Stats section retained + - Element Stats section retained + - Active Skill Upgrades section retained + - Loop Stats section retained