Refactor large files into modular components
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m9s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m9s
- Refactored page.tsx (613→252 lines) with GameOverScreen and LeftPanel extracted - Refactored StatsTab.tsx (584→92 lines) with section components - Refactored SkillsTab.tsx (434→54 lines) with sub-components - Created modular structure for GameContext, LootInventory, and other components - All extracted components organized into feature directories
This commit is contained in:
+250
-9
@@ -12,14 +12,34 @@ Mana-Loop/
|
|||||||
│ └── custom.db
|
│ └── custom.db
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── task5/
|
│ ├── task5/
|
||||||
|
│ │ ├── subtask_11_context.md
|
||||||
│ │ ├── subtask_12_context.md
|
│ │ ├── subtask_12_context.md
|
||||||
│ │ ├── subtask_13_context.md
|
│ │ ├── subtask_13_context.md
|
||||||
│ │ ├── subtask_14_context.md
|
│ │ ├── subtask_14_context.md
|
||||||
│ │ └── subtask_17_context.md
|
│ │ ├── subtask_15_context.md
|
||||||
|
│ │ ├── subtask_16_context.md
|
||||||
|
│ │ ├── subtask_17_context.md
|
||||||
|
│ │ ├── subtask_18_context.md
|
||||||
|
│ │ ├── subtask_19_context.md
|
||||||
|
│ │ ├── subtask_5_context.md
|
||||||
|
│ │ ├── subtask_6_context.md
|
||||||
|
│ │ └── subtask_9_context.md
|
||||||
|
│ ├── task6/
|
||||||
|
│ │ └── subtask_1_context.md
|
||||||
|
│ ├── task7/
|
||||||
|
│ │ ├── ctx_page.md
|
||||||
|
│ │ ├── ctx_skillstab.md
|
||||||
|
│ │ ├── ctx_upgrade_effects.md
|
||||||
|
│ │ ├── plan_page.md
|
||||||
|
│ │ ├── plan_skillstab.md
|
||||||
|
│ │ └── plan_upgrade_effects.md
|
||||||
│ ├── GAME_BRIEFING.md
|
│ ├── GAME_BRIEFING.md
|
||||||
│ ├── project-structure.txt
|
│ ├── project-structure.txt
|
||||||
│ ├── skills.md
|
│ ├── skills.md
|
||||||
│ └── task5.md
|
│ ├── task5.md
|
||||||
|
│ ├── task5_insight_proposals.md
|
||||||
|
│ ├── task6.md
|
||||||
|
│ └── task7.md
|
||||||
├── download/
|
├── download/
|
||||||
│ └── README.md
|
│ └── README.md
|
||||||
├── examples/
|
├── examples/
|
||||||
@@ -37,12 +57,50 @@ Mana-Loop/
|
|||||||
│ ├── app/
|
│ ├── app/
|
||||||
│ │ ├── api/
|
│ │ ├── api/
|
||||||
│ │ │ └── route.ts
|
│ │ │ └── route.ts
|
||||||
|
│ │ ├── components/
|
||||||
|
│ │ │ ├── GameOverScreen.tsx
|
||||||
|
│ │ │ └── LeftPanel.tsx
|
||||||
│ │ ├── globals.css
|
│ │ ├── globals.css
|
||||||
│ │ ├── layout.tsx
|
│ │ ├── layout.tsx
|
||||||
│ │ └── page.tsx
|
│ │ └── page.tsx
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── game/
|
│ │ ├── game/
|
||||||
|
│ │ │ ├── GameContext/
|
||||||
|
│ │ │ │ ├── Provider.tsx
|
||||||
|
│ │ │ │ ├── context-create.ts
|
||||||
|
│ │ │ │ ├── hooks.ts
|
||||||
|
│ │ │ │ └── types.ts
|
||||||
|
│ │ │ ├── LootInventory/
|
||||||
|
│ │ │ │ ├── BlueprintsSection.tsx
|
||||||
|
│ │ │ │ ├── EquipmentItem.tsx
|
||||||
|
│ │ │ │ ├── EssenceItem.tsx
|
||||||
|
│ │ │ │ ├── LootInventoryDisplay.tsx
|
||||||
|
│ │ │ │ ├── MaterialItem.tsx
|
||||||
|
│ │ │ │ ├── icons.ts
|
||||||
|
│ │ │ │ ├── index.tsx
|
||||||
|
│ │ │ │ └── types.ts
|
||||||
|
│ │ │ ├── SkillsTab/
|
||||||
|
│ │ │ │ ├── SkillCategory.tsx
|
||||||
|
│ │ │ │ ├── SkillRow.tsx
|
||||||
|
│ │ │ │ ├── SkillStudyProgress.tsx
|
||||||
|
│ │ │ │ ├── SkillUpgradeDialog.tsx
|
||||||
|
│ │ │ │ └── skills-utils.ts
|
||||||
|
│ │ │ ├── StatsTab/
|
||||||
|
│ │ │ │ ├── ActiveUpgradesSection.tsx
|
||||||
|
│ │ │ │ ├── CombatStatsSection.tsx
|
||||||
|
│ │ │ │ ├── ElementStatsSection.tsx
|
||||||
|
│ │ │ │ ├── LoopStatsSection.tsx
|
||||||
|
│ │ │ │ ├── ManaStatsSection.tsx
|
||||||
|
│ │ │ │ ├── PactStatusSection.tsx
|
||||||
|
│ │ │ │ └── StudyStatsSection.tsx
|
||||||
│ │ │ ├── crafting/
|
│ │ │ ├── crafting/
|
||||||
|
│ │ │ │ ├── EnchantmentDesigner/
|
||||||
|
│ │ │ │ │ ├── DesignForm.tsx
|
||||||
|
│ │ │ │ │ ├── EffectSelector.tsx
|
||||||
|
│ │ │ │ │ ├── EquipmentTypeSelector.tsx
|
||||||
|
│ │ │ │ │ ├── SavedDesigns.tsx
|
||||||
|
│ │ │ │ │ ├── types.ts
|
||||||
|
│ │ │ │ │ └── utils.ts
|
||||||
│ │ │ │ ├── EnchantmentApplier.tsx
|
│ │ │ │ ├── EnchantmentApplier.tsx
|
||||||
│ │ │ │ ├── EnchantmentDesigner.tsx
|
│ │ │ │ ├── EnchantmentDesigner.tsx
|
||||||
│ │ │ │ ├── EnchantmentPreparer.tsx
|
│ │ │ │ ├── EnchantmentPreparer.tsx
|
||||||
@@ -72,16 +130,32 @@ Mana-Loop/
|
|||||||
│ │ │ │ └── index.tsx
|
│ │ │ │ └── index.tsx
|
||||||
│ │ │ ├── tabs/
|
│ │ │ ├── tabs/
|
||||||
│ │ │ │ ├── AchievementsTab.tsx
|
│ │ │ │ ├── AchievementsTab.tsx
|
||||||
|
│ │ │ │ ├── ActivityLog.tsx
|
||||||
│ │ │ │ ├── AttunementsTab.tsx
|
│ │ │ │ ├── AttunementsTab.tsx
|
||||||
│ │ │ │ ├── AttunementsTab.tsx.backup
|
│ │ │ │ ├── AttunementsTab.tsx.backup
|
||||||
|
│ │ │ │ ├── CategorySkillsList.tsx
|
||||||
|
│ │ │ │ ├── CombatStatsPanel.tsx
|
||||||
│ │ │ │ ├── CraftingTab.tsx
|
│ │ │ │ ├── CraftingTab.tsx
|
||||||
│ │ │ │ ├── DebugTab.tsx
|
│ │ │ │ ├── DebugTab.tsx
|
||||||
|
│ │ │ │ ├── EnchantmentsPanel.tsx
|
||||||
|
│ │ │ │ ├── EquipmentControls.tsx
|
||||||
|
│ │ │ │ ├── EquipmentInventory.tsx
|
||||||
|
│ │ │ │ ├── EquipmentSlotGrid.tsx
|
||||||
│ │ │ │ ├── EquipmentTab.tsx
|
│ │ │ │ ├── EquipmentTab.tsx
|
||||||
|
│ │ │ │ ├── FloorControls.tsx
|
||||||
│ │ │ │ ├── GolemancyTab.tsx
|
│ │ │ │ ├── GolemancyTab.tsx
|
||||||
|
│ │ │ │ ├── GuardianPanel.tsx
|
||||||
│ │ │ │ ├── LabTab.tsx
|
│ │ │ │ ├── LabTab.tsx
|
||||||
│ │ │ │ ├── LootTab.tsx
|
│ │ │ │ ├── LootTab.tsx
|
||||||
|
│ │ │ │ ├── MilestoneProgress.tsx
|
||||||
|
│ │ │ │ ├── PrestigeTab.tsx
|
||||||
|
│ │ │ │ ├── RoomDisplay.tsx
|
||||||
|
│ │ │ │ ├── SkillCategoryHeader.tsx
|
||||||
|
│ │ │ │ ├── SkillMultipliers.tsx
|
||||||
|
│ │ │ │ ├── SkillRow.tsx
|
||||||
│ │ │ │ ├── SkillsTab.tsx
|
│ │ │ │ ├── SkillsTab.tsx
|
||||||
│ │ │ │ ├── SpellsTab.tsx
|
│ │ │ │ ├── SpellsTab.tsx
|
||||||
|
│ │ │ │ ├── SpireHeader.tsx
|
||||||
│ │ │ │ ├── SpireTab.tsx
|
│ │ │ │ ├── SpireTab.tsx
|
||||||
│ │ │ │ ├── StatsTab.tsx
|
│ │ │ │ ├── StatsTab.tsx
|
||||||
│ │ │ │ ├── StudyProgress.tsx
|
│ │ │ │ ├── StudyProgress.tsx
|
||||||
@@ -96,7 +170,6 @@ Mana-Loop/
|
|||||||
│ │ │ ├── GameContext.tsx
|
│ │ │ ├── GameContext.tsx
|
||||||
│ │ │ ├── GameToast.tsx
|
│ │ │ ├── GameToast.tsx
|
||||||
│ │ │ ├── LabTab.tsx
|
│ │ │ ├── LabTab.tsx
|
||||||
│ │ │ ├── LootInventory.tsx
|
|
||||||
│ │ │ ├── ManaDisplay.tsx
|
│ │ │ ├── ManaDisplay.tsx
|
||||||
│ │ │ ├── SkillsTab.tsx
|
│ │ │ ├── SkillsTab.tsx
|
||||||
│ │ │ ├── SpellsTab.tsx
|
│ │ │ ├── SpellsTab.tsx
|
||||||
@@ -143,11 +216,37 @@ Mana-Loop/
|
|||||||
│ └── lib/
|
│ └── lib/
|
||||||
│ ├── game/
|
│ ├── game/
|
||||||
│ │ ├── __tests__/
|
│ │ ├── __tests__/
|
||||||
|
│ │ │ ├── skills-tests/
|
||||||
|
│ │ │ │ ├── ascension-skills.test.ts
|
||||||
|
│ │ │ │ ├── integration-and-evolution.test.ts
|
||||||
|
│ │ │ │ ├── mana-skills.test.ts
|
||||||
|
│ │ │ │ ├── prestige-upgrades.test.ts
|
||||||
|
│ │ │ │ ├── skill-prerequisites.test.ts
|
||||||
|
│ │ │ │ ├── specialized-skills.test.ts
|
||||||
|
│ │ │ │ ├── study-skills.test.ts
|
||||||
|
│ │ │ │ └── study-times.test.ts
|
||||||
|
│ │ │ ├── store-method-tests/
|
||||||
│ │ │ ├── bug-fixes.test.ts
|
│ │ │ ├── bug-fixes.test.ts
|
||||||
│ │ │ ├── computed-stats.test.ts
|
│ │ │ ├── computed-stats.test.ts
|
||||||
│ │ │ ├── skill-system.test.ts
|
│ │ │ ├── skill-system.test.ts
|
||||||
│ │ │ └── skills.test.ts
|
│ │ │ └── skills.test.ts
|
||||||
|
│ │ ├── attunements/
|
||||||
|
│ │ │ ├── data.ts
|
||||||
|
│ │ │ ├── index.ts
|
||||||
|
│ │ │ ├── types.ts
|
||||||
|
│ │ │ └── utils.ts
|
||||||
│ │ ├── constants/
|
│ │ ├── constants/
|
||||||
|
│ │ │ ├── spells-modules/
|
||||||
|
│ │ │ │ ├── advanced-spells.ts
|
||||||
|
│ │ │ │ ├── aoe-spells.ts
|
||||||
|
│ │ │ │ ├── basic-elemental-spells.ts
|
||||||
|
│ │ │ │ ├── compound-spells.ts
|
||||||
|
│ │ │ │ ├── enchantment-spells.ts
|
||||||
|
│ │ │ │ ├── legendary-spells.ts
|
||||||
|
│ │ │ │ ├── lightning-spells.ts
|
||||||
|
│ │ │ │ ├── master-spells.ts
|
||||||
|
│ │ │ │ ├── raw-spells.ts
|
||||||
|
│ │ │ │ └── utility-spells.ts
|
||||||
│ │ │ ├── core.ts
|
│ │ │ ├── core.ts
|
||||||
│ │ │ ├── elements.ts
|
│ │ │ ├── elements.ts
|
||||||
│ │ │ ├── guardians.ts
|
│ │ │ ├── guardians.ts
|
||||||
@@ -156,27 +255,93 @@ Mana-Loop/
|
|||||||
│ │ │ ├── rooms.ts
|
│ │ │ ├── rooms.ts
|
||||||
│ │ │ ├── skills.ts
|
│ │ │ ├── skills.ts
|
||||||
│ │ │ └── spells.ts
|
│ │ │ └── spells.ts
|
||||||
|
│ │ ├── crafting-actions/
|
||||||
|
│ │ │ ├── application-actions.ts
|
||||||
|
│ │ │ ├── computed-getters.ts
|
||||||
|
│ │ │ ├── crafting-equipment-actions.ts
|
||||||
|
│ │ │ ├── design-actions.ts
|
||||||
|
│ │ │ ├── disenchant-actions.ts
|
||||||
|
│ │ │ ├── equipment-actions.ts
|
||||||
|
│ │ │ ├── index.ts
|
||||||
|
│ │ │ └── preparation-actions.ts
|
||||||
│ │ ├── data/
|
│ │ ├── data/
|
||||||
│ │ │ ├── enchantments/
|
│ │ │ ├── enchantments/
|
||||||
|
│ │ │ │ ├── spell-effects/
|
||||||
|
│ │ │ │ │ ├── basic-spells.ts
|
||||||
|
│ │ │ │ │ ├── index.ts
|
||||||
|
│ │ │ │ │ ├── lightning-spells.ts
|
||||||
|
│ │ │ │ │ ├── metal-spells.ts
|
||||||
|
│ │ │ │ │ ├── sand-spells.ts
|
||||||
|
│ │ │ │ │ ├── tier2-spells.ts
|
||||||
|
│ │ │ │ │ ├── tier3-spells.ts
|
||||||
|
│ │ │ │ │ └── types.ts
|
||||||
│ │ │ │ ├── combat-effects.ts
|
│ │ │ │ ├── combat-effects.ts
|
||||||
│ │ │ │ ├── defense-effects.ts
|
│ │ │ │ ├── defense-effects.ts
|
||||||
│ │ │ │ ├── elemental-effects.ts
|
│ │ │ │ ├── elemental-effects.ts
|
||||||
│ │ │ │ ├── index.ts
|
│ │ │ │ ├── index.ts
|
||||||
│ │ │ │ ├── mana-effects.ts
|
│ │ │ │ ├── mana-effects.ts
|
||||||
│ │ │ │ ├── special-effects.ts
|
│ │ │ │ ├── special-effects.ts
|
||||||
│ │ │ │ ├── spell-effects.ts
|
|
||||||
│ │ │ │ └── utility-effects.ts
|
│ │ │ │ └── utility-effects.ts
|
||||||
|
│ │ │ ├── equipment/
|
||||||
|
│ │ │ │ ├── accessories.ts
|
||||||
|
│ │ │ │ ├── body.ts
|
||||||
|
│ │ │ │ ├── casters.ts
|
||||||
|
│ │ │ │ ├── catalysts.ts
|
||||||
|
│ │ │ │ ├── feet.ts
|
||||||
|
│ │ │ │ ├── hands.ts
|
||||||
|
│ │ │ │ ├── head.ts
|
||||||
|
│ │ │ │ ├── index.ts
|
||||||
|
│ │ │ │ ├── shields.ts
|
||||||
|
│ │ │ │ ├── swords.ts
|
||||||
|
│ │ │ │ ├── types.ts
|
||||||
|
│ │ │ │ └── utils.ts
|
||||||
|
│ │ │ ├── golems/
|
||||||
|
│ │ │ │ ├── base-golems.ts
|
||||||
|
│ │ │ │ ├── elemental-golems.ts
|
||||||
|
│ │ │ │ ├── hybrid-golems.ts
|
||||||
|
│ │ │ │ ├── index.ts
|
||||||
|
│ │ │ │ ├── types.ts
|
||||||
|
│ │ │ │ └── utils.ts
|
||||||
│ │ │ ├── achievements.ts
|
│ │ │ ├── achievements.ts
|
||||||
│ │ │ ├── attunements.ts
|
│ │ │ ├── attunements.ts
|
||||||
│ │ │ ├── crafting-recipes.ts
|
│ │ │ ├── crafting-recipes.ts
|
||||||
│ │ │ ├── enchantment-effects.ts
|
│ │ │ ├── enchantment-effects.ts
|
||||||
│ │ │ ├── enchantment-types.ts
|
│ │ │ ├── enchantment-types.ts
|
||||||
│ │ │ ├── equipment.ts
|
|
||||||
│ │ │ ├── golems.ts
|
|
||||||
│ │ │ └── loot-drops.ts
|
│ │ │ └── loot-drops.ts
|
||||||
│ │ ├── hooks/
|
│ │ ├── hooks/
|
||||||
│ │ │ └── useGameDerived.ts
|
│ │ │ ├── useGameDerived.ts
|
||||||
|
│ │ │ └── useSkillUpgradeSelection.ts
|
||||||
|
│ │ ├── skill-evolution-modules/
|
||||||
|
│ │ │ ├── elemental-attunement.ts
|
||||||
|
│ │ │ ├── enchanting-skills.ts
|
||||||
|
│ │ │ ├── focused-mind.ts
|
||||||
|
│ │ │ ├── guardian-skills.ts
|
||||||
|
│ │ │ ├── hybrid-skills.ts
|
||||||
|
│ │ │ ├── index.ts
|
||||||
|
│ │ │ ├── insight-harvest.ts
|
||||||
|
│ │ │ ├── invocation-skills.ts
|
||||||
|
│ │ │ ├── knowledge-retention.ts
|
||||||
|
│ │ │ ├── learning-skills.ts
|
||||||
|
│ │ │ ├── magic-skills.ts
|
||||||
|
│ │ │ ├── mana-utility-skills.ts
|
||||||
|
│ │ │ ├── mana-well-flow.ts
|
||||||
|
│ │ │ ├── quick-learner.ts
|
||||||
|
│ │ │ ├── types.ts
|
||||||
|
│ │ │ └── utils.ts
|
||||||
|
│ │ ├── skills-split-tests/
|
||||||
|
│ │ │ ├── ascension-specialized-skills.test.ts
|
||||||
|
│ │ │ ├── mana-skills.test.ts
|
||||||
|
│ │ │ ├── prerequisites-studytimes-prestige-integration.test.ts
|
||||||
|
│ │ │ └── study-skills.test.ts
|
||||||
│ │ ├── store/
|
│ │ ├── store/
|
||||||
|
│ │ │ ├── crafting-modules/
|
||||||
|
│ │ │ │ ├── initial-state.ts
|
||||||
|
│ │ │ │ ├── selectors.ts
|
||||||
|
│ │ │ │ ├── slice-logic.ts
|
||||||
|
│ │ │ │ ├── starting-equipment.ts
|
||||||
|
│ │ │ │ ├── tick-processors.ts
|
||||||
|
│ │ │ │ ├── types.ts
|
||||||
|
│ │ │ │ └── utils.ts
|
||||||
│ │ │ ├── combatSlice.ts
|
│ │ │ ├── combatSlice.ts
|
||||||
│ │ │ ├── computed.ts
|
│ │ │ ├── computed.ts
|
||||||
│ │ │ ├── craftingSlice.ts
|
│ │ │ ├── craftingSlice.ts
|
||||||
@@ -186,11 +351,71 @@ Mana-Loop/
|
|||||||
│ │ │ ├── prestigeSlice.ts
|
│ │ │ ├── prestigeSlice.ts
|
||||||
│ │ │ ├── skillSlice.ts
|
│ │ │ ├── skillSlice.ts
|
||||||
│ │ │ └── timeSlice.ts
|
│ │ │ └── timeSlice.ts
|
||||||
|
│ │ ├── store-modules/
|
||||||
|
│ │ │ ├── {room-utils,enemy-utils,initial-state,activity-log,store-actions}/
|
||||||
|
│ │ │ ├── activity-log.ts
|
||||||
|
│ │ │ ├── computed-stats.ts
|
||||||
|
│ │ │ ├── enemy-utils.ts
|
||||||
|
│ │ │ ├── initial-state.ts
|
||||||
|
│ │ │ ├── room-utils.ts
|
||||||
|
│ │ │ ├── store-actions.ts
|
||||||
|
│ │ │ └── tick-logic.ts
|
||||||
|
│ │ ├── store-tests/
|
||||||
|
│ │ │ ├── damage-calculation.test.ts
|
||||||
|
│ │ │ ├── element-recipes.test.ts
|
||||||
|
│ │ │ ├── floor.test.ts
|
||||||
|
│ │ │ ├── formatting.test.ts
|
||||||
|
│ │ │ ├── game-constants.test.ts
|
||||||
|
│ │ │ ├── individual-skills.test.ts
|
||||||
|
│ │ │ ├── insight-meditation-incursion.test.ts
|
||||||
|
│ │ │ ├── integration.test.ts
|
||||||
|
│ │ │ ├── mana-calculation.test.ts
|
||||||
|
│ │ │ ├── skill-evolution.test.ts
|
||||||
|
│ │ │ ├── skill-requirements.test.ts
|
||||||
|
│ │ │ ├── spell-cost.test.ts
|
||||||
|
│ │ │ ├── study-speed.test.ts
|
||||||
|
│ │ │ └── test-utils.ts
|
||||||
│ │ ├── stores/
|
│ │ ├── stores/
|
||||||
│ │ │ ├── __tests__/
|
│ │ │ ├── __tests__/
|
||||||
|
│ │ │ │ ├── combat-store-tests/
|
||||||
|
│ │ │ │ ├── index-tests/
|
||||||
|
│ │ │ │ │ ├── combat-calculations.test.ts
|
||||||
|
│ │ │ │ │ ├── definitions.test.ts
|
||||||
|
│ │ │ │ │ ├── mana-calculations.test.ts
|
||||||
|
│ │ │ │ │ ├── meditation-insight-incursion.test.ts
|
||||||
|
│ │ │ │ │ ├── spell-cost.test.ts
|
||||||
|
│ │ │ │ │ ├── study-speed.test.ts
|
||||||
|
│ │ │ │ │ └── utility-functions.test.ts
|
||||||
|
│ │ │ │ ├── mana-store-tests/
|
||||||
|
│ │ │ │ ├── prestige-store-tests/
|
||||||
|
│ │ │ │ ├── store-method-tests/
|
||||||
|
│ │ │ │ │ ├── combat-store.test.ts
|
||||||
|
│ │ │ │ │ ├── mana-store.test.ts
|
||||||
|
│ │ │ │ │ ├── prestige-store.test.ts
|
||||||
|
│ │ │ │ │ ├── skill-store.test.ts
|
||||||
|
│ │ │ │ │ └── ui-store.test.ts
|
||||||
|
│ │ │ │ ├── stores-split-tests/
|
||||||
|
│ │ │ │ ├── stores-tests/
|
||||||
|
│ │ │ │ │ ├── damage-calculation.test.ts
|
||||||
|
│ │ │ │ │ ├── floor.test.ts
|
||||||
|
│ │ │ │ │ ├── formatting.test.ts
|
||||||
|
│ │ │ │ │ ├── guardians.test.ts
|
||||||
|
│ │ │ │ │ ├── incursion.test.ts
|
||||||
|
│ │ │ │ │ ├── insight-calculation.test.ts
|
||||||
|
│ │ │ │ │ ├── mana-calculation.test.ts
|
||||||
|
│ │ │ │ │ ├── meditation.test.ts
|
||||||
|
│ │ │ │ │ ├── prestige-upgrades.test.ts
|
||||||
|
│ │ │ │ │ ├── skill-definitions.test.ts
|
||||||
|
│ │ │ │ │ ├── spell-cost.test.ts
|
||||||
|
│ │ │ │ │ ├── spell-definitions.test.ts
|
||||||
|
│ │ │ │ │ └── study-speed.test.ts
|
||||||
|
│ │ │ │ ├── ui-store-tests/
|
||||||
│ │ │ │ ├── store-methods.test.ts
|
│ │ │ │ ├── store-methods.test.ts
|
||||||
│ │ │ │ └── stores.test.ts
|
│ │ │ │ └── stores.test.ts
|
||||||
│ │ │ ├── combatStore.ts
|
│ │ │ ├── combatStore.ts
|
||||||
|
│ │ │ ├── gameActions.ts
|
||||||
|
│ │ │ ├── gameHooks.ts
|
||||||
|
│ │ │ ├── gameLoopActions.ts
|
||||||
│ │ │ ├── gameStore.ts
|
│ │ │ ├── gameStore.ts
|
||||||
│ │ │ ├── index.test.ts
|
│ │ │ ├── index.test.ts
|
||||||
│ │ │ ├── index.ts
|
│ │ │ ├── index.ts
|
||||||
@@ -198,6 +423,13 @@ Mana-Loop/
|
|||||||
│ │ │ ├── prestigeStore.ts
|
│ │ │ ├── prestigeStore.ts
|
||||||
│ │ │ ├── skillStore.ts
|
│ │ │ ├── skillStore.ts
|
||||||
│ │ │ └── uiStore.ts
|
│ │ │ └── uiStore.ts
|
||||||
|
│ │ ├── stores-split-tests/
|
||||||
|
│ │ │ ├── combat-store.test.ts
|
||||||
|
│ │ │ ├── integration.test.ts
|
||||||
|
│ │ │ ├── mana-store.test.ts
|
||||||
|
│ │ │ ├── prestige-store.test.ts
|
||||||
|
│ │ │ ├── skill-store.test.ts
|
||||||
|
│ │ │ └── ui-store.test.ts
|
||||||
│ │ ├── types/
|
│ │ ├── types/
|
||||||
│ │ │ ├── attunements.ts
|
│ │ │ ├── attunements.ts
|
||||||
│ │ │ ├── elements.ts
|
│ │ │ ├── elements.ts
|
||||||
@@ -212,22 +444,31 @@ Mana-Loop/
|
|||||||
│ │ │ ├── formatting.ts
|
│ │ │ ├── formatting.ts
|
||||||
│ │ │ ├── index.ts
|
│ │ │ ├── index.ts
|
||||||
│ │ │ └── mana-utils.ts
|
│ │ │ └── mana-utils.ts
|
||||||
│ │ ├── attunements.ts
|
|
||||||
│ │ ├── computed-stats.ts
|
│ │ ├── computed-stats.ts
|
||||||
│ │ ├── constants.ts
|
│ │ ├── constants.ts
|
||||||
|
│ │ ├── crafting-apply.ts
|
||||||
|
│ │ ├── crafting-attunements.ts
|
||||||
|
│ │ ├── crafting-design.ts
|
||||||
|
│ │ ├── crafting-equipment.ts
|
||||||
|
│ │ ├── crafting-loot.ts
|
||||||
|
│ │ ├── crafting-prep.ts
|
||||||
│ │ ├── crafting-slice.ts
|
│ │ ├── crafting-slice.ts
|
||||||
|
│ │ ├── crafting-utils.ts
|
||||||
│ │ ├── debug-context.tsx
|
│ │ ├── debug-context.tsx
|
||||||
|
│ │ ├── dynamic-compute.ts
|
||||||
│ │ ├── effects.ts
|
│ │ ├── effects.ts
|
||||||
│ │ ├── formatting.ts
|
│ │ ├── formatting.ts
|
||||||
│ │ ├── navigation-slice.ts
|
│ │ ├── navigation-slice.ts
|
||||||
│ │ ├── skill-evolution.ts
|
│ │ ├── skill-evolution.ts
|
||||||
│ │ ├── skills.test.ts
|
│ │ ├── skills.test.ts
|
||||||
|
│ │ ├── special-effects.ts
|
||||||
│ │ ├── store.test.ts
|
│ │ ├── store.test.ts
|
||||||
│ │ ├── store.ts
|
│ │ ├── store.ts
|
||||||
│ │ ├── stores.test.ts
|
│ │ ├── stores.test.ts
|
||||||
│ │ ├── study-slice.ts
|
│ │ ├── study-slice.ts
|
||||||
│ │ ├── types.ts
|
│ │ ├── types.ts
|
||||||
│ │ └── upgrade-effects.ts
|
│ │ ├── upgrade-effects.ts
|
||||||
|
│ │ └── upgrade-effects.types.ts
|
||||||
│ ├── db.ts
|
│ ├── db.ts
|
||||||
│ └── utils.ts
|
│ └── utils.ts
|
||||||
├── .accesslog
|
├── .accesslog
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import type { GameStore } from '@/lib/game/store';
|
||||||
|
|
||||||
|
interface GameOverScreenProps {
|
||||||
|
store: GameStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GameOverScreen({ store }: GameOverScreenProps) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 game-overlay flex items-center justify-center z-50">
|
||||||
|
<Card className="bg-gray-900 border-gray-600 max-w-md w-full mx-4 shadow-2xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className={`text-3xl text-center game-title ${store.victory ? 'text-amber-400' : 'text-red-400'}`}>
|
||||||
|
{store.victory ? 'VICTORY!' : 'LOOP ENDS'}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-center text-gray-400">
|
||||||
|
{store.victory
|
||||||
|
? 'The Awakened One falls! Your power echoes through eternity.'
|
||||||
|
: 'The time loop resets... but you remember.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
|
<div className="text-xl font-bold text-amber-400 game-mono">{store.fmt(store.loopInsight)}</div>
|
||||||
|
<div className="text-xs text-gray-400">Insight Gained</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
|
<div className="text-xl font-bold text-blue-400 game-mono">{store.maxFloorReached}</div>
|
||||||
|
<div className="text-xs text-gray-400">Best Floor</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
|
<div className="text-xl font-bold text-purple-400 game-mono">{store.signedPacts.length}</div>
|
||||||
|
<div className="text-xs text-gray-400">Pacts Signed</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
|
<div className="text-xl font-bold text-green-400 game-mono">{store.loopCount + 1}</div>
|
||||||
|
<div className="text-xs text-gray-400">Total Loops</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
||||||
|
size="lg"
|
||||||
|
onClick={() => store.startNewLoop()}
|
||||||
|
>
|
||||||
|
Begin New Loop
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Mountain } from 'lucide-react';
|
||||||
|
import { ManaDisplay } from '@/components/game';
|
||||||
|
import { ActionButtons } from '@/components/game';
|
||||||
|
import { CalendarDisplay } from '@/components/game';
|
||||||
|
import { DebugName } from '@/lib/game/debug-context';
|
||||||
|
import type { GameStore } from '@/lib/game/store';
|
||||||
|
import { computeMaxMana, computeClickMana, getMeditationBonus, getUnifiedEffects } from '@/lib/game/store';
|
||||||
|
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
||||||
|
|
||||||
|
interface LeftPanelProps {
|
||||||
|
store: GameStore;
|
||||||
|
effectiveRegen: number;
|
||||||
|
incursionStrength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LeftPanel({ store, effectiveRegen, incursionStrength }: LeftPanelProps) {
|
||||||
|
const [isGathering, setIsGathering] = useState(false);
|
||||||
|
|
||||||
|
const handleGatherStart = () => {
|
||||||
|
setIsGathering(true);
|
||||||
|
store.gatherMana();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGatherEnd = () => {
|
||||||
|
setIsGathering(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isGathering) return;
|
||||||
|
|
||||||
|
let lastGatherTime = 0;
|
||||||
|
const minGatherInterval = 100;
|
||||||
|
let animationFrameId: number;
|
||||||
|
|
||||||
|
const gatherLoop = (timestamp: number) => {
|
||||||
|
if (timestamp - lastGatherTime >= minGatherInterval) {
|
||||||
|
store.gatherMana();
|
||||||
|
lastGatherTime = timestamp;
|
||||||
|
}
|
||||||
|
animationFrameId = requestAnimationFrame(gatherLoop);
|
||||||
|
};
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(gatherLoop);
|
||||||
|
return () => cancelAnimationFrame(animationFrameId);
|
||||||
|
}, [isGathering, store]);
|
||||||
|
|
||||||
|
const maxMana = computeMaxMana(store, getUnifiedEffects(store));
|
||||||
|
const clickMana = computeClickMana(store);
|
||||||
|
const meditationMultiplier = getMeditationBonus(store.meditateTicks, store.skills, getUnifiedEffects(store).meditationEfficiency);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="md:w-80 space-y-4 flex-shrink-0">
|
||||||
|
<DebugName name="ManaDisplay">
|
||||||
|
<ManaDisplay
|
||||||
|
rawMana={store.rawMana}
|
||||||
|
maxMana={maxMana}
|
||||||
|
effectiveRegen={effectiveRegen}
|
||||||
|
meditationMultiplier={meditationMultiplier}
|
||||||
|
clickMana={clickMana}
|
||||||
|
isGathering={isGathering}
|
||||||
|
onGatherStart={handleGatherStart}
|
||||||
|
onGatherEnd={handleGatherEnd}
|
||||||
|
elements={store.elements}
|
||||||
|
/>
|
||||||
|
</DebugName>
|
||||||
|
|
||||||
|
{!store.spireMode && (
|
||||||
|
<DebugName name="ClimbSpireButton">
|
||||||
|
<Button
|
||||||
|
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700"
|
||||||
|
size="lg"
|
||||||
|
onClick={() => store.enterSpireMode()}
|
||||||
|
>
|
||||||
|
<Mountain className="w-5 h-5 mr-2" />
|
||||||
|
Climb the Spire
|
||||||
|
</Button>
|
||||||
|
</DebugName>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!store.spireMode && (
|
||||||
|
<DebugName name="ActionButtons">
|
||||||
|
<ActionButtons
|
||||||
|
currentAction={store.currentAction}
|
||||||
|
currentStudyTarget={store.currentStudyTarget}
|
||||||
|
designProgress={store.designProgress}
|
||||||
|
designProgress2={store.designProgress2}
|
||||||
|
preparationProgress={store.preparationProgress}
|
||||||
|
applicationProgress={store.applicationProgress}
|
||||||
|
equipmentCraftingProgress={store.equipmentCraftingProgress}
|
||||||
|
/>
|
||||||
|
</DebugName>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DebugName name="CalendarDisplay">
|
||||||
|
<CalendarDisplay
|
||||||
|
day={store.day}
|
||||||
|
hour={store.hour}
|
||||||
|
incursionStrength={incursionStrength}
|
||||||
|
/>
|
||||||
|
</DebugName>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+139
-497
@@ -1,25 +1,27 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, lazy, Suspense } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
import { useGameStore, useGameLoop, fmt, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store';
|
import { useGameStore, fmt, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost, getUnifiedEffects } from '@/lib/game/store';
|
||||||
import { ActivityLogEntry } from '@/lib/game/types';
|
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
||||||
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
|
||||||
|
|
||||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
import { ELEMENTS, GUARDIANS, SPELLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||||
|
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
||||||
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
import { TimeDisplay } from '@/components/game';
|
||||||
|
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { RotateCcw, Mountain, ChevronDown } from 'lucide-react';
|
import { RotateCcw, Mountain } from 'lucide-react';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
import { DebugName } from '@/lib/game/debug-context';
|
||||||
// Non-tab component imports
|
|
||||||
import { ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
|
// Import extracted components
|
||||||
|
import { GameOverScreen } from './components/GameOverScreen';
|
||||||
|
import { LeftPanel } from './components/LeftPanel';
|
||||||
|
|
||||||
// Lazy load tab components
|
// Lazy load tab components
|
||||||
const SpireTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireTab })));
|
const SpireTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireTab })));
|
||||||
const SkillsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SkillsTab })));
|
const SkillsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SkillsTab })));
|
||||||
@@ -34,381 +36,57 @@ const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module
|
|||||||
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
|
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
|
||||||
const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab })));
|
const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab })));
|
||||||
|
|
||||||
// Loading fallback component
|
|
||||||
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
|
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Extracted Components
|
// Grimoire Tab Component
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
interface GameOverScreenProps {
|
function GrimoireTab() {
|
||||||
store: any;
|
const grimoireSpells = Object.values(SPELLS_DEF).filter((s: any) => s.grimoire);
|
||||||
}
|
const availablePages = Math.ceil(grimoireSpells.length / 12);
|
||||||
|
|
||||||
function GameOverScreen({ store }: GameOverScreenProps) {
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 game-overlay flex items-center justify-center z-50">
|
|
||||||
<Card className="bg-gray-900 border-gray-600 max-w-md w-full mx-4 shadow-2xl">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className={`text-3xl text-center game-title ${store.victory ? 'text-amber-400' : 'text-red-400'}`}>
|
|
||||||
{store.victory ? 'VICTORY!' : 'LOOP ENDS'}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<p className="text-center text-gray-400">
|
|
||||||
{store.victory
|
|
||||||
? 'The Awakened One falls! Your power echoes through eternity.'
|
|
||||||
: 'The time loop resets... but you remember.'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
|
||||||
<div className="text-xl font-bold text-amber-400 game-mono">{fmt(store.loopInsight)}</div>
|
|
||||||
<div className="text-xs text-gray-400">Insight Gained</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
|
||||||
<div className="text-xl font-bold text-blue-400 game-mono">{store.maxFloorReached}</div>
|
|
||||||
<div className="text-xs text-gray-400">Best Floor</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
|
||||||
<div className="text-xl font-bold text-purple-400 game-mono">{store.signedPacts.length}</div>
|
|
||||||
<div className="text-xs text-gray-400">Pacts Signed</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
|
||||||
<div className="text-xl font-bold text-green-400 game-mono">{store.loopCount + 1}</div>
|
|
||||||
<div className="text-xs text-gray-400">Total Loops</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
|
||||||
size="lg"
|
|
||||||
onClick={() => store.startNewLoop()}
|
|
||||||
>
|
|
||||||
Begin New Loop
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LeftPanelProps {
|
|
||||||
store: any;
|
|
||||||
effectiveRegen: number;
|
|
||||||
incursionStrength: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LeftPanel({ store, effectiveRegen, incursionStrength }: LeftPanelProps) {
|
|
||||||
const [isGathering, setIsGathering] = useState(false);
|
|
||||||
|
|
||||||
const handleGatherStart = () => {
|
|
||||||
setIsGathering(true);
|
|
||||||
store.gatherMana();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGatherEnd = () => {
|
|
||||||
setIsGathering(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isGathering) return;
|
|
||||||
|
|
||||||
let lastGatherTime = 0;
|
|
||||||
const minGatherInterval = 100;
|
|
||||||
let animationFrameId: number;
|
|
||||||
|
|
||||||
const gatherLoop = (timestamp: number) => {
|
|
||||||
if (timestamp - lastGatherTime >= minGatherInterval) {
|
|
||||||
store.gatherMana();
|
|
||||||
lastGatherTime = timestamp;
|
|
||||||
}
|
|
||||||
animationFrameId = requestAnimationFrame(gatherLoop);
|
|
||||||
};
|
|
||||||
|
|
||||||
animationFrameId = requestAnimationFrame(gatherLoop);
|
|
||||||
return () => cancelAnimationFrame(animationFrameId);
|
|
||||||
}, [isGathering, store]);
|
|
||||||
|
|
||||||
const maxMana = computeMaxMana(store, getUnifiedEffects(store));
|
|
||||||
const clickMana = computeClickMana(store);
|
|
||||||
const meditationMultiplier = getMeditationBonus(store.meditateTicks, store.skills, getUnifiedEffects(store).meditationEfficiency);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:w-80 space-y-4 flex-shrink-0">
|
<div className="space-y-4">
|
||||||
<DebugName name="ManaDisplay">
|
<div className="text-sm text-gray-400">
|
||||||
<ManaDisplay
|
<p className="mb-2">A vast tome of arcane knowledge. Study carefully — each spell costs insight to transcribe into your repertoire.</p>
|
||||||
rawMana={store.rawMana}
|
<p>Available pages: {availablePages}. Spells in grimoire: {grimoireSpells.length}.</p>
|
||||||
maxMana={maxMana}
|
|
||||||
effectiveRegen={effectiveRegen}
|
|
||||||
meditationMultiplier={meditationMultiplier}
|
|
||||||
clickMana={clickMana}
|
|
||||||
isGathering={isGathering}
|
|
||||||
onGatherStart={handleGatherStart}
|
|
||||||
onGatherEnd={handleGatherEnd}
|
|
||||||
elements={store.elements}
|
|
||||||
/>
|
|
||||||
</DebugName>
|
|
||||||
|
|
||||||
{!store.spireMode && (
|
|
||||||
<DebugName name="ClimbSpireButton">
|
|
||||||
<Button
|
|
||||||
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700"
|
|
||||||
size="lg"
|
|
||||||
onClick={() => store.enterSpireMode()}
|
|
||||||
>
|
|
||||||
<Mountain className="w-5 h-5 mr-2" />
|
|
||||||
Climb the Spire
|
|
||||||
</Button>
|
|
||||||
</DebugName>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!store.spireMode && (
|
|
||||||
<DebugName name="ActionButtons">
|
|
||||||
<ActionButtons
|
|
||||||
currentAction={store.currentAction}
|
|
||||||
currentStudyTarget={store.currentStudyTarget}
|
|
||||||
designProgress={store.designProgress}
|
|
||||||
designProgress2={store.designProgress2}
|
|
||||||
preparationProgress={store.preparationProgress}
|
|
||||||
applicationProgress={store.applicationProgress}
|
|
||||||
equipmentCraftingProgress={store.equipmentCraftingProgress}
|
|
||||||
/>
|
|
||||||
</DebugName>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DebugName name="CalendarDisplay">
|
|
||||||
<CalendarDisplay
|
|
||||||
day={store.day}
|
|
||||||
hour={store.hour}
|
|
||||||
incursionStrength={incursionStrength}
|
|
||||||
/>
|
|
||||||
</DebugName>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MainTabsProps {
|
|
||||||
store: any;
|
|
||||||
upgradeEffects: any;
|
|
||||||
maxMana: number;
|
|
||||||
baseRegen: number;
|
|
||||||
clickMana: number;
|
|
||||||
meditationMultiplier: number;
|
|
||||||
effectiveRegen: number;
|
|
||||||
incursionStrength: number;
|
|
||||||
manaCascadeBonus: number;
|
|
||||||
studySpeedMult: number;
|
|
||||||
studyCostMult: number;
|
|
||||||
manaWaterfallBonus: number;
|
|
||||||
hasManaWaterfall: boolean;
|
|
||||||
hasFlowSurge: boolean;
|
|
||||||
hasManaOverflow: boolean;
|
|
||||||
hasEternalFlow: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MainTabs({
|
|
||||||
store,
|
|
||||||
upgradeEffects,
|
|
||||||
maxMana,
|
|
||||||
baseRegen,
|
|
||||||
clickMana,
|
|
||||||
meditationMultiplier,
|
|
||||||
effectiveRegen,
|
|
||||||
incursionStrength,
|
|
||||||
manaCascadeBonus,
|
|
||||||
studySpeedMult,
|
|
||||||
studyCostMult,
|
|
||||||
manaWaterfallBonus,
|
|
||||||
hasManaWaterfall,
|
|
||||||
hasFlowSurge,
|
|
||||||
hasManaOverflow,
|
|
||||||
hasEternalFlow,
|
|
||||||
}: MainTabsProps) {
|
|
||||||
const [activeTab, setActiveTab] = useState('spire');
|
|
||||||
|
|
||||||
const renderGrimoireTab = (): JSX.Element => {
|
|
||||||
const grimoireSpells = Object.values(SPELLS_DEF).filter((s: any) => s.grimoire);
|
|
||||||
const availablePages = Math.ceil(grimoireSpells.length / 12);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
<p className="mb-2">A vast tome of arcane knowledge. Study carefully — each spell costs insight to transcribe into your repertoire.</p>
|
|
||||||
<p>Available pages: {availablePages}. Spells in grimoire: {grimoireSpells.length}.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScrollArea className="h-[600px] rounded border border-gray-700 p-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{grimoireSpells.map((spell: any) => (
|
|
||||||
<div
|
|
||||||
key={spell.id}
|
|
||||||
className="p-4 bg-gray-800/50 rounded border border-gray-600 hover:border-gray-500 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between mb-2">
|
|
||||||
<span className="font-bold text-gray-100">{spell.name}</span>
|
|
||||||
<Badge variant="outline" className="border-gray-600">
|
|
||||||
{spell.element}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-400 mb-3">{spell.desc}</p>
|
|
||||||
<div className="text-xs text-gray-500 space-y-1">
|
|
||||||
<div>Cost: {(spell.cost as any[]).map((c: any) => `${c.amount} ${c.type}`).join(', ')}</div>
|
|
||||||
<div>Power: {spell.power}</div>
|
|
||||||
{spell.effect && <div>Effect: {spell.effect}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
<ScrollArea className="h-[600px] rounded border border-gray-700 p-4">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
{grimoireSpells.map((spell: any) => (
|
||||||
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
<div
|
||||||
<TabsTrigger value="spire" className="text-xs px-2 py-1">⚔️ Spire</TabsTrigger>
|
key={spell.id}
|
||||||
<TabsTrigger value="attunements" className="text-xs px-2 py-1">✨ Attune</TabsTrigger>
|
className="p-4 bg-gray-800/50 rounded border border-gray-600 hover:border-gray-500 transition-colors"
|
||||||
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golems</TabsTrigger>
|
>
|
||||||
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
|
<div className="flex items-start justify-between mb-2">
|
||||||
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
|
<span className="font-bold text-gray-100">{spell.name}</span>
|
||||||
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡️ Gear</TabsTrigger>
|
<Badge variant="outline" className="border-gray-600">
|
||||||
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
{spell.element}
|
||||||
<TabsTrigger value="loot" className="text-xs px-2 py-1">💎 Loot</TabsTrigger>
|
</Badge>
|
||||||
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achieve</TabsTrigger>
|
</div>
|
||||||
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
<p className="text-sm text-gray-400 mb-3">{spell.desc}</p>
|
||||||
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
<div className="text-xs text-gray-500 space-y-1">
|
||||||
<TabsTrigger value="debug" className="text-xs px-2 py-1">🔧 Debug</TabsTrigger>
|
<div>Cost: {(spell.cost as any[]).map((c: any) => `${c.amount} ${c.type}`).join(', ')}</div>
|
||||||
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
<div>Power: {spell.power}</div>
|
||||||
</TabsList>
|
{spell.effect && <div>Effect: {spell.effect}</div>}
|
||||||
|
</div>
|
||||||
<TabsContent value="spire">
|
</div>
|
||||||
<DebugName name="SpireTab">
|
))}
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
</div>
|
||||||
<SpireTab store={store} />
|
</ScrollArea>
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="attunements">
|
|
||||||
<DebugName name="AttunementsTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<AttunementsTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="golemancy">
|
|
||||||
<DebugName name="GolemancyTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<GolemancyTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="skills">
|
|
||||||
<DebugName name="SkillsTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<SkillsTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="spells">
|
|
||||||
<DebugName name="SpellsTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<SpellsTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="equipment">
|
|
||||||
<DebugName name="EquipmentTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<EquipmentTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="crafting">
|
|
||||||
<DebugName name="CraftingTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<CraftingTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="loot">
|
|
||||||
<DebugName name="LootTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<LootTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="achievements">
|
|
||||||
<DebugName name="AchievementsTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<AchievementsTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="lab">
|
|
||||||
<DebugName name="LabTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<LabTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="stats">
|
|
||||||
<DebugName name="StatsTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<StatsTab
|
|
||||||
store={store}
|
|
||||||
upgradeEffects={upgradeEffects}
|
|
||||||
maxMana={maxMana}
|
|
||||||
baseRegen={baseRegen}
|
|
||||||
clickMana={clickMana}
|
|
||||||
meditationMultiplier={meditationMultiplier}
|
|
||||||
effectiveRegen={effectiveRegen}
|
|
||||||
incursionStrength={incursionStrength}
|
|
||||||
manaCascadeBonus={manaCascadeBonus}
|
|
||||||
manaWaterfallBonus={manaWaterfallBonus}
|
|
||||||
hasManaWaterfall={hasManaWaterfall}
|
|
||||||
hasFlowSurge={hasFlowSurge}
|
|
||||||
hasManaOverflow={hasManaOverflow}
|
|
||||||
hasEternalFlow={hasEternalFlow}
|
|
||||||
studySpeedMult={studySpeedMult}
|
|
||||||
studyCostMult={studyCostMult}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="grimoire">
|
|
||||||
<DebugName name="GrimoireTab">
|
|
||||||
{renderGrimoireTab()}
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="debug">
|
|
||||||
<DebugName name="DebugTab">
|
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
|
||||||
<DebugTab store={store} />
|
|
||||||
</Suspense>
|
|
||||||
</DebugName>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Main Game Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export default function ManaLoopGame() {
|
export default function ManaLoopGame() {
|
||||||
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
||||||
|
const [activeTab, setActiveTab] = useState('spire');
|
||||||
|
|
||||||
// Game store
|
// Game store
|
||||||
const store: any = useGameStore();
|
const store: any = useGameStore();
|
||||||
@@ -417,23 +95,12 @@ export default function ManaLoopGame() {
|
|||||||
// Computed effects from upgrades and equipment
|
// Computed effects from upgrades and equipment
|
||||||
const upgradeEffects = getUnifiedEffects(store);
|
const upgradeEffects = getUnifiedEffects(store);
|
||||||
|
|
||||||
// Get unlocked elements for mana type selector
|
|
||||||
Object.entries(ELEMENTS)
|
|
||||||
.filter(([id]) => store.elements[id]?.unlocked)
|
|
||||||
.map(([id, elem]) => ({ id, name: elem.name, sym: elem.sym, color: elem.color }));
|
|
||||||
|
|
||||||
// Derived stats
|
// Derived stats
|
||||||
const maxMana = computeMaxMana(store, upgradeEffects);
|
const maxMana = computeMaxMana(store, upgradeEffects);
|
||||||
const baseRegen = computeRegen(store, upgradeEffects);
|
const baseRegen = computeRegen(store, upgradeEffects);
|
||||||
const clickMana = computeClickMana(store);
|
const clickMana = computeClickMana(store);
|
||||||
const floorElem = getFloorElement(store.currentFloor);
|
|
||||||
const floorElemDef = ELEMENTS[floorElem];
|
|
||||||
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
|
|
||||||
const currentGuardian = GUARDIANS[store.currentFloor];
|
|
||||||
const meditationMultiplier = getMeditationBonus(store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency);
|
const meditationMultiplier = getMeditationBonus(store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency);
|
||||||
const incursionStrength = getIncursionStrength(store.day, store.hour);
|
const incursionStrength = getIncursionStrength(store.day, store.hour);
|
||||||
const studySpeedMult = getStudySpeedMultiplier(store.skills);
|
|
||||||
const studyCostMult = getStudyCostMultiplier(store.skills);
|
|
||||||
|
|
||||||
// Effective regen with incursion penalty
|
// Effective regen with incursion penalty
|
||||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||||||
@@ -448,12 +115,6 @@ export default function ManaLoopGame() {
|
|||||||
? Math.floor(maxMana / 100) * 0.25
|
? Math.floor(maxMana / 100) * 0.25
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Special effects flags for mana features
|
|
||||||
const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL);
|
|
||||||
const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE);
|
|
||||||
const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW);
|
|
||||||
const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW);
|
|
||||||
|
|
||||||
// Effective regen
|
// Effective regen
|
||||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
||||||
|
|
||||||
@@ -461,14 +122,7 @@ export default function ManaLoopGame() {
|
|||||||
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
|
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
|
||||||
|
|
||||||
// Compute total DPS
|
// Compute total DPS
|
||||||
const totalDPS = getTotalDPS(store, upgradeEffects as any, floorElem);
|
const totalDPS = getTotalDPS(store, upgradeEffects as any);
|
||||||
|
|
||||||
// Check if spell can be cast
|
|
||||||
const canCastSpell = (spellId: string): boolean => {
|
|
||||||
const spell = SPELLS_DEF[spellId];
|
|
||||||
if (!spell) return false;
|
|
||||||
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Game Over Screen
|
// Game Over Screen
|
||||||
if (store.gameOver) {
|
if (store.gameOver) {
|
||||||
@@ -498,113 +152,101 @@ export default function ManaLoopGame() {
|
|||||||
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
|
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
|
||||||
<LeftPanel store={store} effectiveRegen={effectiveRegen} incursionStrength={incursionStrength} />
|
<LeftPanel store={store} effectiveRegen={effectiveRegen} incursionStrength={incursionStrength} />
|
||||||
|
|
||||||
{!store.spireMode ? (
|
<div className="flex-1 min-w-0">
|
||||||
<MainTabs
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
store={store}
|
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
||||||
upgradeEffects={upgradeEffects}
|
<TabsTrigger value="spire" className="text-xs px-2 py-1">⚔️ Spire</TabsTrigger>
|
||||||
maxMana={maxMana}
|
<TabsTrigger value="attunements" className="text-xs px-2 py-1">✨ Attune</TabsTrigger>
|
||||||
baseRegen={baseRegen}
|
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golems</TabsTrigger>
|
||||||
clickMana={clickMana}
|
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
|
||||||
meditationMultiplier={meditationMultiplier}
|
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
|
||||||
effectiveRegen={effectiveRegen}
|
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡️ Gear</TabsTrigger>
|
||||||
incursionStrength={incursionStrength}
|
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
||||||
manaCascadeBonus={manaCascadeBonus}
|
<TabsTrigger value="loot" className="text-xs px-2 py-1">💎 Loot</TabsTrigger>
|
||||||
manaWaterfallBonus={manaWaterfallBonus}
|
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achieve</TabsTrigger>
|
||||||
hasManaWaterfall={hasManaWaterfall}
|
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
||||||
hasFlowSurge={hasFlowSurge}
|
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
||||||
hasManaOverflow={hasManaOverflow}
|
<TabsTrigger value="debug" className="text-xs px-2 py-1">🐛 Debug</TabsTrigger>
|
||||||
hasEternalFlow={hasEternalFlow}
|
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
||||||
studySpeedMult={studySpeedMult}
|
</TabsList>
|
||||||
studyCostMult={studyCostMult}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
/* Spire Mode - Simplified UI */
|
|
||||||
<div className="flex-1 min-w-0 space-y-4">
|
|
||||||
<DebugName name="SpireModeUI">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h2 className="text-2xl font-bold game-title text-amber-400">
|
|
||||||
🏔️ Spire Mode - Floor {store.currentFloor}
|
|
||||||
</h2>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
{store.currentAction === 'climb' && !store.isDescending && (
|
|
||||||
<Badge className="bg-green-900/50 text-green-300 border-green-600">Climbing</Badge>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="border-blue-600/50 text-blue-400 hover:bg-blue-900/20"
|
|
||||||
onClick={() => store.climbDownFloor()}
|
|
||||||
disabled={store.isDescending}
|
|
||||||
>
|
|
||||||
<ChevronDown className="w-4 h-4 mr-2" />
|
|
||||||
{store.isDescending ? 'Descending…' : 'Begin Descent'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
onClick={() => store.exitSpireMode()}
|
|
||||||
>
|
|
||||||
Exit Spire
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<TabsContent value="spire">
|
||||||
<Suspense fallback={<TabLoadingFallback />}>
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
<SpireTab store={store} simpleMode={true} />
|
<SpireTab store={store} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
<TabsContent value="attunements">
|
||||||
<CardHeader className="pb-2">
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
|
<AttunementsTab store={store} />
|
||||||
</CardHeader>
|
</Suspense>
|
||||||
<CardContent>
|
</TabsContent>
|
||||||
<ScrollArea className="h-48">
|
|
||||||
<div className="space-y-1">
|
|
||||||
{(store.activityLog || []).slice(0, 50).map((entry: ActivityLogEntry, i) => {
|
|
||||||
const getEventStyle = (eventType: string) => {
|
|
||||||
switch (eventType) {
|
|
||||||
case 'enemy_defeated':
|
|
||||||
case 'floor_cleared':
|
|
||||||
return 'text-green-400';
|
|
||||||
case 'damage_dealt':
|
|
||||||
return 'text-red-400';
|
|
||||||
case 'dodge':
|
|
||||||
return 'text-yellow-400';
|
|
||||||
case 'armor_proc':
|
|
||||||
return 'text-blue-400';
|
|
||||||
case 'special_effect':
|
|
||||||
return 'text-purple-400';
|
|
||||||
case 'floor_transition':
|
|
||||||
return 'text-cyan-400';
|
|
||||||
case 'spell_cast':
|
|
||||||
return 'text-amber-400';
|
|
||||||
case 'golem_attack':
|
|
||||||
return 'text-orange-400';
|
|
||||||
case 'puzzle_solved':
|
|
||||||
return 'text-pink-400';
|
|
||||||
default:
|
|
||||||
return 'text-gray-300';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
<TabsContent value="golemancy">
|
||||||
<div
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
key={entry.id}
|
<GolemancyTab store={store} />
|
||||||
className={`text-xs ${i === 0 ? 'text-gray-200 font-semibold' : getEventStyle(entry.eventType)}`}
|
</Suspense>
|
||||||
>
|
</TabsContent>
|
||||||
{entry.message}
|
|
||||||
</div>
|
<TabsContent value="skills">
|
||||||
);
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
})}
|
<SkillsTab store={store} />
|
||||||
{(store.activityLog || []).length === 0 && (
|
</Suspense>
|
||||||
<div className="text-xs text-gray-500 italic">No activity yet...</div>
|
</TabsContent>
|
||||||
)}
|
|
||||||
</div>
|
<TabsContent value="spells">
|
||||||
</ScrollArea>
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
</CardContent>
|
<SpellsTab store={store} />
|
||||||
</Card>
|
</Suspense>
|
||||||
</DebugName>
|
</TabsContent>
|
||||||
</div>
|
|
||||||
)}
|
<TabsContent value="equipment">
|
||||||
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
|
<EquipmentTab store={store} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="crafting">
|
||||||
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
|
<CraftingTab store={store} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="loot">
|
||||||
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
|
<LootTab store={store} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="achievements">
|
||||||
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
|
<AchievementsTab store={store} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="lab">
|
||||||
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
|
<LabTab store={store} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="stats">
|
||||||
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
|
<StatsTab store={store} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="debug">
|
||||||
|
<Suspense fallback={<TabLoadingFallback />}>
|
||||||
|
<DebugTab store={store} />
|
||||||
|
</Suspense>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="grimoire">
|
||||||
|
<GrimoireTab />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -1,428 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, useMemo, type ReactNode } from 'react';
|
// Re-export everything from the modular GameContext files
|
||||||
import { useSkillStore } from '@/lib/game/stores/skillStore';
|
export { GameProvider, GameProvider as default } from './GameContext/Provider';
|
||||||
import { useManaStore } from '@/lib/game/stores/manaStore';
|
export { useGameContext } from './GameContext/hooks';
|
||||||
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
export { GameContext } from './GameContext/context-create';
|
||||||
import { useUIStore } from '@/lib/game/stores/uiStore';
|
export type { GameContextValue, UnifiedStore } from './GameContext/types';
|
||||||
import { useCombatStore } from '@/lib/game/stores/combatStore';
|
|
||||||
import { useGameStore, useGameLoop } from '@/lib/game/stores/gameStore';
|
|
||||||
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/upgrade-effects';
|
|
||||||
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
|
||||||
import {
|
|
||||||
computeMaxMana,
|
|
||||||
computeRegen,
|
|
||||||
computeClickMana,
|
|
||||||
getMeditationBonus,
|
|
||||||
canAffordSpellCost,
|
|
||||||
calcDamage,
|
|
||||||
getFloorElement,
|
|
||||||
getBoonBonuses,
|
|
||||||
getIncursionStrength,
|
|
||||||
} from '@/lib/game/utils';
|
|
||||||
import {
|
|
||||||
ELEMENTS,
|
|
||||||
GUARDIANS,
|
|
||||||
SPELLS_DEF,
|
|
||||||
HOURS_PER_TICK,
|
|
||||||
TICK_MS,
|
|
||||||
} from '@/lib/game/constants';
|
|
||||||
import type { ElementDef, GuardianDef, SpellDef, GameAction } from '@/lib/game/types';
|
|
||||||
|
|
||||||
// Define a unified store type that combines all stores
|
|
||||||
interface UnifiedStore {
|
|
||||||
// From gameStore (coordinator)
|
|
||||||
day: number;
|
|
||||||
hour: number;
|
|
||||||
incursionStrength: number;
|
|
||||||
containmentWards: number;
|
|
||||||
initialized: boolean;
|
|
||||||
tick: () => void;
|
|
||||||
resetGame: () => void;
|
|
||||||
gatherMana: () => void;
|
|
||||||
startNewLoop: () => void;
|
|
||||||
|
|
||||||
// From manaStore
|
|
||||||
rawMana: number;
|
|
||||||
meditateTicks: number;
|
|
||||||
totalManaGathered: number;
|
|
||||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
|
||||||
setRawMana: (amount: number) => void;
|
|
||||||
addRawMana: (amount: number, max: number) => void;
|
|
||||||
spendRawMana: (amount: number) => boolean;
|
|
||||||
convertMana: (element: string, amount: number) => boolean;
|
|
||||||
unlockElement: (element: string, cost: number) => boolean;
|
|
||||||
craftComposite: (target: string, recipe: string[]) => boolean;
|
|
||||||
|
|
||||||
// From skillStore
|
|
||||||
skills: Record<string, number>;
|
|
||||||
skillProgress: Record<string, number>;
|
|
||||||
skillUpgrades: Record<string, string[]>;
|
|
||||||
skillTiers: Record<string, number>;
|
|
||||||
paidStudySkills: Record<string, number>;
|
|
||||||
currentStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
|
|
||||||
parallelStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
|
|
||||||
setSkillLevel: (skillId: string, level: number) => void;
|
|
||||||
startStudyingSkill: (skillId: string, rawMana: number) => { started: boolean; cost: number };
|
|
||||||
startStudyingSpell: (spellId: string, rawMana: number, studyTime: number) => { started: boolean; cost: number };
|
|
||||||
cancelStudy: (retentionBonus: number) => void;
|
|
||||||
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
|
||||||
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
|
||||||
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void;
|
|
||||||
tierUpSkill: (skillId: string) => void;
|
|
||||||
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: Array<{ id: string; name: string; desc: string; milestone: 5 | 10; effect: { type: string; stat?: string; value?: number; specialId?: string } }>; selected: string[] };
|
|
||||||
|
|
||||||
// From prestigeStore
|
|
||||||
loopCount: number;
|
|
||||||
insight: number;
|
|
||||||
totalInsight: number;
|
|
||||||
loopInsight: number;
|
|
||||||
prestigeUpgrades: Record<string, number>;
|
|
||||||
memorySlots: number;
|
|
||||||
pactSlots: number;
|
|
||||||
memories: Array<{ skillId: string; level: number; tier: number; upgrades: string[] }>;
|
|
||||||
defeatedGuardians: number[];
|
|
||||||
signedPacts: number[];
|
|
||||||
pactRitualFloor: number | null;
|
|
||||||
pactRitualProgress: number;
|
|
||||||
doPrestige: (id: string) => void;
|
|
||||||
addMemory: (memory: { skillId: string; level: number; tier: number; upgrades: string[] }) => void;
|
|
||||||
removeMemory: (skillId: string) => void;
|
|
||||||
clearMemories: () => void;
|
|
||||||
startPactRitual: (floor: number, rawMana: number) => boolean;
|
|
||||||
cancelPactRitual: () => void;
|
|
||||||
removePact: (floor: number) => void;
|
|
||||||
defeatGuardian: (floor: number) => void;
|
|
||||||
|
|
||||||
// From combatStore
|
|
||||||
currentFloor: number;
|
|
||||||
floorHP: number;
|
|
||||||
floorMaxHP: number;
|
|
||||||
maxFloorReached: number;
|
|
||||||
activeSpell: string;
|
|
||||||
currentAction: GameAction;
|
|
||||||
castProgress: number;
|
|
||||||
spells: Record<string, { learned: boolean; level: number; studyProgress?: number }>;
|
|
||||||
setAction: (action: GameAction) => void;
|
|
||||||
setSpell: (spellId: string) => void;
|
|
||||||
learnSpell: (spellId: string) => void;
|
|
||||||
advanceFloor: () => void;
|
|
||||||
|
|
||||||
// From uiStore
|
|
||||||
log: string[];
|
|
||||||
paused: boolean;
|
|
||||||
gameOver: boolean;
|
|
||||||
victory: boolean;
|
|
||||||
addLog: (message: string) => void;
|
|
||||||
togglePause: () => void;
|
|
||||||
setPaused: (paused: boolean) => void;
|
|
||||||
setGameOver: (gameOver: boolean, victory?: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GameContextValue {
|
|
||||||
// Unified store for backward compatibility
|
|
||||||
store: UnifiedStore;
|
|
||||||
|
|
||||||
// Individual stores for direct access if needed
|
|
||||||
skillStore: ReturnType<typeof useSkillStore.getState>;
|
|
||||||
manaStore: ReturnType<typeof useManaStore.getState>;
|
|
||||||
prestigeStore: ReturnType<typeof usePrestigeStore.getState>;
|
|
||||||
uiStore: ReturnType<typeof useUIStore.getState>;
|
|
||||||
combatStore: ReturnType<typeof useCombatStore.getState>;
|
|
||||||
|
|
||||||
// Computed effects from upgrades
|
|
||||||
upgradeEffects: ReturnType<typeof computeEffects>;
|
|
||||||
|
|
||||||
// Derived stats
|
|
||||||
maxMana: number;
|
|
||||||
baseRegen: number;
|
|
||||||
clickMana: number;
|
|
||||||
floorElem: string;
|
|
||||||
floorElemDef: ElementDef | undefined;
|
|
||||||
isGuardianFloor: boolean;
|
|
||||||
currentGuardian: GuardianDef | undefined;
|
|
||||||
activeSpellDef: SpellDef | undefined;
|
|
||||||
meditationMultiplier: number;
|
|
||||||
incursionStrength: number;
|
|
||||||
studySpeedMult: number;
|
|
||||||
studyCostMult: number;
|
|
||||||
|
|
||||||
// Effective regen calculations
|
|
||||||
effectiveRegenWithSpecials: number;
|
|
||||||
manaCascadeBonus: number;
|
|
||||||
manaWaterfallBonus: number;
|
|
||||||
effectiveRegen: number;
|
|
||||||
|
|
||||||
// Has special flags
|
|
||||||
hasManaWaterfall: boolean;
|
|
||||||
hasFlowSurge: boolean;
|
|
||||||
hasManaOverflow: boolean;
|
|
||||||
hasEternalFlow: boolean;
|
|
||||||
|
|
||||||
// DPS calculation
|
|
||||||
dps: number;
|
|
||||||
|
|
||||||
// Boons
|
|
||||||
activeBoons: ReturnType<typeof getBoonBonuses>;
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
canCastSpell: (spellId: string) => boolean;
|
|
||||||
hasSpecial: (effects: ReturnType<typeof computeEffects>, specialId: string) => boolean;
|
|
||||||
SPECIAL_EFFECTS: typeof SPECIAL_EFFECTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GameContext = createContext<GameContextValue | null>(null);
|
|
||||||
|
|
||||||
export function GameProvider({ children }: { children: ReactNode }) {
|
|
||||||
// Get all individual stores
|
|
||||||
const gameStore = useGameStore();
|
|
||||||
const skillState = useSkillStore();
|
|
||||||
const manaState = useManaStore();
|
|
||||||
const prestigeState = usePrestigeStore();
|
|
||||||
const uiState = useUIStore();
|
|
||||||
const combatState = useCombatStore();
|
|
||||||
|
|
||||||
// Create unified store object for backward compatibility
|
|
||||||
const unifiedStore = useMemo<UnifiedStore>(() => ({
|
|
||||||
// From gameStore
|
|
||||||
day: gameStore.day,
|
|
||||||
hour: gameStore.hour,
|
|
||||||
incursionStrength: gameStore.incursionStrength,
|
|
||||||
containmentWards: gameStore.containmentWards,
|
|
||||||
initialized: gameStore.initialized,
|
|
||||||
tick: gameStore.tick,
|
|
||||||
resetGame: gameStore.resetGame,
|
|
||||||
gatherMana: gameStore.gatherMana,
|
|
||||||
startNewLoop: gameStore.startNewLoop,
|
|
||||||
|
|
||||||
// From manaStore
|
|
||||||
rawMana: manaState.rawMana,
|
|
||||||
meditateTicks: manaState.meditateTicks,
|
|
||||||
totalManaGathered: manaState.totalManaGathered,
|
|
||||||
elements: manaState.elements,
|
|
||||||
setRawMana: manaState.setRawMana,
|
|
||||||
addRawMana: manaState.addRawMana,
|
|
||||||
spendRawMana: manaState.spendRawMana,
|
|
||||||
convertMana: manaState.convertMana,
|
|
||||||
unlockElement: manaState.unlockElement,
|
|
||||||
craftComposite: manaState.craftComposite,
|
|
||||||
|
|
||||||
// From skillStore
|
|
||||||
skills: skillState.skills,
|
|
||||||
skillProgress: skillState.skillProgress,
|
|
||||||
skillUpgrades: skillState.skillUpgrades,
|
|
||||||
skillTiers: skillState.skillTiers,
|
|
||||||
paidStudySkills: skillState.paidStudySkills,
|
|
||||||
currentStudyTarget: skillState.currentStudyTarget,
|
|
||||||
parallelStudyTarget: skillState.parallelStudyTarget,
|
|
||||||
setSkillLevel: skillState.setSkillLevel,
|
|
||||||
startStudyingSkill: skillState.startStudyingSkill,
|
|
||||||
startStudyingSpell: skillState.startStudyingSpell,
|
|
||||||
cancelStudy: skillState.cancelStudy,
|
|
||||||
selectSkillUpgrade: skillState.selectSkillUpgrade,
|
|
||||||
deselectSkillUpgrade: skillState.deselectSkillUpgrade,
|
|
||||||
commitSkillUpgrades: skillState.commitSkillUpgrades,
|
|
||||||
tierUpSkill: skillState.tierUpSkill,
|
|
||||||
getSkillUpgradeChoices: skillState.getSkillUpgradeChoices,
|
|
||||||
|
|
||||||
// From prestigeStore
|
|
||||||
loopCount: prestigeState.loopCount,
|
|
||||||
insight: prestigeState.insight,
|
|
||||||
totalInsight: prestigeState.totalInsight,
|
|
||||||
loopInsight: prestigeState.loopInsight,
|
|
||||||
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
|
||||||
memorySlots: prestigeState.memorySlots,
|
|
||||||
pactSlots: prestigeState.pactSlots,
|
|
||||||
memories: prestigeState.memories,
|
|
||||||
defeatedGuardians: prestigeState.defeatedGuardians,
|
|
||||||
signedPacts: prestigeState.signedPacts,
|
|
||||||
pactRitualFloor: prestigeState.pactRitualFloor,
|
|
||||||
pactRitualProgress: prestigeState.pactRitualProgress,
|
|
||||||
doPrestige: prestigeState.doPrestige,
|
|
||||||
addMemory: prestigeState.addMemory,
|
|
||||||
removeMemory: prestigeState.removeMemory,
|
|
||||||
clearMemories: prestigeState.clearMemories,
|
|
||||||
startPactRitual: prestigeState.startPactRitual,
|
|
||||||
cancelPactRitual: prestigeState.cancelPactRitual,
|
|
||||||
removePact: prestigeState.removePact,
|
|
||||||
defeatGuardian: prestigeState.defeatGuardian,
|
|
||||||
|
|
||||||
// From combatStore
|
|
||||||
currentFloor: combatState.currentFloor,
|
|
||||||
floorHP: combatState.floorHP,
|
|
||||||
floorMaxHP: combatState.floorMaxHP,
|
|
||||||
maxFloorReached: combatState.maxFloorReached,
|
|
||||||
activeSpell: combatState.activeSpell,
|
|
||||||
currentAction: combatState.currentAction,
|
|
||||||
castProgress: combatState.castProgress,
|
|
||||||
spells: combatState.spells,
|
|
||||||
setAction: combatState.setAction,
|
|
||||||
setSpell: combatState.setSpell,
|
|
||||||
learnSpell: combatState.learnSpell,
|
|
||||||
advanceFloor: combatState.advanceFloor,
|
|
||||||
|
|
||||||
// From uiStore
|
|
||||||
log: uiState.logs,
|
|
||||||
paused: uiState.paused,
|
|
||||||
gameOver: uiState.gameOver,
|
|
||||||
victory: uiState.victory,
|
|
||||||
addLog: uiState.addLog,
|
|
||||||
togglePause: uiState.togglePause,
|
|
||||||
setPaused: uiState.setPaused,
|
|
||||||
setGameOver: uiState.setGameOver,
|
|
||||||
}), [gameStore, skillState, manaState, prestigeState, uiState, combatState]);
|
|
||||||
|
|
||||||
// Computed effects from upgrades
|
|
||||||
const upgradeEffects = useMemo(
|
|
||||||
() => computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {}),
|
|
||||||
[skillState.skillUpgrades, skillState.skillTiers]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a minimal state object for compute functions
|
|
||||||
const stateForCompute = useMemo(() => ({
|
|
||||||
skills: skillState.skills,
|
|
||||||
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
|
||||||
skillUpgrades: skillState.skillUpgrades,
|
|
||||||
skillTiers: skillState.skillTiers,
|
|
||||||
signedPacts: prestigeState.signedPacts,
|
|
||||||
rawMana: manaState.rawMana,
|
|
||||||
meditateTicks: manaState.meditateTicks,
|
|
||||||
incursionStrength: gameStore.incursionStrength,
|
|
||||||
}), [skillState, prestigeState, manaState, gameStore.incursionStrength]);
|
|
||||||
|
|
||||||
// Derived stats
|
|
||||||
const maxMana = useMemo(
|
|
||||||
() => computeMaxMana(stateForCompute, upgradeEffects),
|
|
||||||
[stateForCompute, upgradeEffects]
|
|
||||||
);
|
|
||||||
|
|
||||||
const baseRegen = useMemo(
|
|
||||||
() => computeRegen(stateForCompute, upgradeEffects),
|
|
||||||
[stateForCompute, upgradeEffects]
|
|
||||||
);
|
|
||||||
|
|
||||||
const clickMana = useMemo(() => computeClickMana(stateForCompute), [stateForCompute]);
|
|
||||||
|
|
||||||
// Floor element from combat store
|
|
||||||
const floorElem = useMemo(() => getFloorElement(combatState.currentFloor), [combatState.currentFloor]);
|
|
||||||
const floorElemDef = ELEMENTS[floorElem];
|
|
||||||
const isGuardianFloor = !!GUARDIANS[combatState.currentFloor];
|
|
||||||
const currentGuardian = GUARDIANS[combatState.currentFloor];
|
|
||||||
const activeSpellDef = SPELLS_DEF[combatState.activeSpell];
|
|
||||||
|
|
||||||
const meditationMultiplier = useMemo(
|
|
||||||
() => getMeditationBonus(manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency),
|
|
||||||
[manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency]
|
|
||||||
);
|
|
||||||
|
|
||||||
const incursionStrength = useMemo(
|
|
||||||
() => getIncursionStrength(gameStore.day, gameStore.hour),
|
|
||||||
[gameStore.day, gameStore.hour]
|
|
||||||
);
|
|
||||||
|
|
||||||
const studySpeedMult = useMemo(
|
|
||||||
() => getStudySpeedMultiplier(skillState.skills),
|
|
||||||
[skillState.skills]
|
|
||||||
);
|
|
||||||
|
|
||||||
const studyCostMult = useMemo(
|
|
||||||
() => getStudyCostMultiplier(skillState.skills),
|
|
||||||
[skillState.skills]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Effective regen calculations
|
|
||||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
|
||||||
|
|
||||||
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
|
||||||
? Math.floor(maxMana / 100) * 0.1
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
|
||||||
? Math.floor(maxMana / 100) * 0.25
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
|
||||||
|
|
||||||
// Has special flags for UI
|
|
||||||
const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL);
|
|
||||||
const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE);
|
|
||||||
const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW);
|
|
||||||
const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW);
|
|
||||||
|
|
||||||
// Active boons
|
|
||||||
const activeBoons = useMemo(
|
|
||||||
() => getBoonBonuses(prestigeState.signedPacts),
|
|
||||||
[prestigeState.signedPacts]
|
|
||||||
);
|
|
||||||
|
|
||||||
// DPS calculation - based on active spell, attack speed, and damage
|
|
||||||
const dps = useMemo(() => {
|
|
||||||
if (!activeSpellDef) return 0;
|
|
||||||
const baseDmg = calcDamage(
|
|
||||||
{ skills: skillState.skills, signedPacts: prestigeState.signedPacts },
|
|
||||||
combatState.activeSpell,
|
|
||||||
floorElem
|
|
||||||
);
|
|
||||||
const dmgWithEffects = baseDmg * upgradeEffects.baseDamageMultiplier + upgradeEffects.baseDamageBonus;
|
|
||||||
const attackSpeed = (1 + (skillState.skills.quickCast || 0) * 0.05) * upgradeEffects.attackSpeedMultiplier;
|
|
||||||
const castSpeed = activeSpellDef.castSpeed || 1;
|
|
||||||
return dmgWithEffects * attackSpeed * castSpeed;
|
|
||||||
}, [activeSpellDef, skillState.skills, prestigeState.signedPacts, floorElem, upgradeEffects, combatState.activeSpell]);
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
const canCastSpell = (spellId: string): boolean => {
|
|
||||||
const spell = SPELLS_DEF[spellId];
|
|
||||||
if (!spell) return false;
|
|
||||||
return canAffordSpellCost(spell.cost, manaState.rawMana, manaState.elements);
|
|
||||||
};
|
|
||||||
|
|
||||||
const value: GameContextValue = {
|
|
||||||
store: unifiedStore,
|
|
||||||
skillStore: skillState,
|
|
||||||
manaStore: manaState,
|
|
||||||
prestigeStore: prestigeState,
|
|
||||||
uiStore: uiState,
|
|
||||||
combatStore: combatState,
|
|
||||||
upgradeEffects,
|
|
||||||
maxMana,
|
|
||||||
baseRegen,
|
|
||||||
clickMana,
|
|
||||||
floorElem,
|
|
||||||
floorElemDef,
|
|
||||||
isGuardianFloor,
|
|
||||||
currentGuardian,
|
|
||||||
activeSpellDef,
|
|
||||||
meditationMultiplier,
|
|
||||||
incursionStrength,
|
|
||||||
studySpeedMult,
|
|
||||||
studyCostMult,
|
|
||||||
effectiveRegenWithSpecials,
|
|
||||||
manaCascadeBonus,
|
|
||||||
manaWaterfallBonus,
|
|
||||||
effectiveRegen,
|
|
||||||
hasManaWaterfall,
|
|
||||||
hasFlowSurge,
|
|
||||||
hasManaOverflow,
|
|
||||||
hasEternalFlow,
|
|
||||||
dps,
|
|
||||||
activeBoons,
|
|
||||||
canCastSpell,
|
|
||||||
hasSpecial,
|
|
||||||
SPECIAL_EFFECTS,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <GameContext.Provider value={value}>{children}</GameContext.Provider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGameContext() {
|
|
||||||
const context = useContext(GameContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useGameContext must be used within a GameProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
GameProvider.displayName = "GameProvider";
|
|
||||||
|
|
||||||
// Re-export useGameLoop for convenience
|
// Re-export useGameLoop for convenience
|
||||||
export { useGameLoop };
|
export { useGameLoop } from '@/lib/game/stores/gameHooks';
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo, type ReactNode } from 'react';
|
||||||
|
import { useSkillStore } from '@/lib/game/stores/skillStore';
|
||||||
|
import { useManaStore } from '@/lib/game/stores/manaStore';
|
||||||
|
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
||||||
|
import { useUIStore } from '@/lib/game/stores/uiStore';
|
||||||
|
import { useCombatStore } from '@/lib/game/stores/combatStore';
|
||||||
|
import { useGameStore } from '@/lib/game/stores/gameStore';
|
||||||
|
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/upgrade-effects';
|
||||||
|
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||||
|
import {
|
||||||
|
computeMaxMana,
|
||||||
|
computeRegen,
|
||||||
|
computeClickMana,
|
||||||
|
getMeditationBonus,
|
||||||
|
canAffordSpellCost,
|
||||||
|
calcDamage,
|
||||||
|
getFloorElement,
|
||||||
|
getBoonBonuses,
|
||||||
|
getIncursionStrength,
|
||||||
|
} from '@/lib/game/utils';
|
||||||
|
import {
|
||||||
|
ELEMENTS,
|
||||||
|
GUARDIANS,
|
||||||
|
SPELLS_DEF,
|
||||||
|
} from '@/lib/game/constants';
|
||||||
|
import type { ElementDef, GuardianDef, SpellDef, GameAction } from '@/lib/game/types';
|
||||||
|
import type { UnifiedStore, GameContextValue } from './types';
|
||||||
|
import { GameContext } from './context-create';
|
||||||
|
|
||||||
|
function createUnifiedStore(
|
||||||
|
gameStore: ReturnType<typeof useGameStore.getState>,
|
||||||
|
skillState: ReturnType<typeof useSkillStore.getState>,
|
||||||
|
manaState: ReturnType<typeof useManaStore.getState>,
|
||||||
|
prestigeState: ReturnType<typeof usePrestigeStore.getState>,
|
||||||
|
uiState: ReturnType<typeof useUIStore.getState>,
|
||||||
|
combatState: ReturnType<typeof useCombatStore.getState>
|
||||||
|
): UnifiedStore {
|
||||||
|
return {
|
||||||
|
// From gameStore
|
||||||
|
day: gameStore.day,
|
||||||
|
hour: gameStore.hour,
|
||||||
|
incursionStrength: gameStore.incursionStrength,
|
||||||
|
containmentWards: gameStore.containmentWards,
|
||||||
|
initialized: gameStore.initialized,
|
||||||
|
tick: gameStore.tick,
|
||||||
|
resetGame: gameStore.resetGame,
|
||||||
|
gatherMana: gameStore.gatherMana,
|
||||||
|
startNewLoop: gameStore.startNewLoop,
|
||||||
|
|
||||||
|
// From manaStore
|
||||||
|
rawMana: manaState.rawMana,
|
||||||
|
meditateTicks: manaState.meditateTicks,
|
||||||
|
totalManaGathered: manaState.totalManaGathered,
|
||||||
|
elements: manaState.elements,
|
||||||
|
setRawMana: manaState.setRawMana,
|
||||||
|
addRawMana: manaState.addRawMana,
|
||||||
|
spendRawMana: manaState.spendRawMana,
|
||||||
|
convertMana: manaState.convertMana,
|
||||||
|
unlockElement: manaState.unlockElement,
|
||||||
|
craftComposite: manaState.craftComposite,
|
||||||
|
|
||||||
|
// From skillStore
|
||||||
|
skills: skillState.skills,
|
||||||
|
skillProgress: skillState.skillProgress,
|
||||||
|
skillUpgrades: skillState.skillUpgrades,
|
||||||
|
skillTiers: skillState.skillTiers,
|
||||||
|
paidStudySkills: skillState.paidStudySkills,
|
||||||
|
currentStudyTarget: skillState.currentStudyTarget,
|
||||||
|
parallelStudyTarget: skillState.parallelStudyTarget,
|
||||||
|
setSkillLevel: skillState.setSkillLevel,
|
||||||
|
startStudyingSkill: skillState.startStudyingSkill,
|
||||||
|
startStudyingSpell: skillState.startStudyingSpell,
|
||||||
|
cancelStudy: skillState.cancelStudy,
|
||||||
|
selectSkillUpgrade: skillState.selectSkillUpgrade,
|
||||||
|
deselectSkillUpgrade: skillState.deselectSkillUpgrade,
|
||||||
|
commitSkillUpgrades: skillState.commitSkillUpgrades,
|
||||||
|
tierUpSkill: skillState.tierUpSkill,
|
||||||
|
getSkillUpgradeChoices: skillState.getSkillUpgradeChoices,
|
||||||
|
|
||||||
|
// From prestigeStore
|
||||||
|
loopCount: prestigeState.loopCount,
|
||||||
|
insight: prestigeState.insight,
|
||||||
|
totalInsight: prestigeState.totalInsight,
|
||||||
|
loopInsight: prestigeState.loopInsight,
|
||||||
|
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
||||||
|
memorySlots: prestigeState.memorySlots,
|
||||||
|
pactSlots: prestigeState.pactSlots,
|
||||||
|
memories: prestigeState.memories,
|
||||||
|
defeatedGuardians: prestigeState.defeatedGuardians,
|
||||||
|
signedPacts: prestigeState.signedPacts,
|
||||||
|
pactRitualFloor: prestigeState.pactRitualFloor,
|
||||||
|
pactRitualProgress: prestigeState.pactRitualProgress,
|
||||||
|
doPrestige: prestigeState.doPrestige,
|
||||||
|
addMemory: prestigeState.addMemory,
|
||||||
|
removeMemory: prestigeState.removeMemory,
|
||||||
|
clearMemories: prestigeState.clearMemories,
|
||||||
|
startPactRitual: prestigeState.startPactRitual,
|
||||||
|
cancelPactRitual: prestigeState.cancelPactRitual,
|
||||||
|
removePact: prestigeState.removePact,
|
||||||
|
defeatGuardian: prestigeState.defeatGuardian,
|
||||||
|
|
||||||
|
// From combatStore
|
||||||
|
currentFloor: combatState.currentFloor,
|
||||||
|
floorHP: combatState.floorHP,
|
||||||
|
floorMaxHP: combatState.floorMaxHP,
|
||||||
|
maxFloorReached: combatState.maxFloorReached,
|
||||||
|
activeSpell: combatState.activeSpell,
|
||||||
|
currentAction: combatState.currentAction,
|
||||||
|
castProgress: combatState.castProgress,
|
||||||
|
spells: combatState.spells,
|
||||||
|
setAction: combatState.setAction,
|
||||||
|
setSpell: combatState.setSpell,
|
||||||
|
learnSpell: combatState.learnSpell,
|
||||||
|
advanceFloor: combatState.advanceFloor,
|
||||||
|
|
||||||
|
// From uiStore
|
||||||
|
log: uiState.logs,
|
||||||
|
paused: uiState.paused,
|
||||||
|
gameOver: uiState.gameOver,
|
||||||
|
victory: uiState.victory,
|
||||||
|
addLog: uiState.addLog,
|
||||||
|
togglePause: uiState.togglePause,
|
||||||
|
setPaused: uiState.setPaused,
|
||||||
|
setGameOver: uiState.setGameOver,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GameProvider({ children }: { children: ReactNode }) {
|
||||||
|
// Get all individual stores
|
||||||
|
const gameStore = useGameStore();
|
||||||
|
const skillState = useSkillStore();
|
||||||
|
const manaState = useManaStore();
|
||||||
|
const prestigeState = usePrestigeStore();
|
||||||
|
const uiState = useUIStore();
|
||||||
|
const combatState = useCombatStore();
|
||||||
|
|
||||||
|
// Create unified store object for backward compatibility
|
||||||
|
const unifiedStore = useMemo(
|
||||||
|
() => createUnifiedStore(gameStore, skillState, manaState, prestigeState, uiState, combatState),
|
||||||
|
[gameStore, skillState, manaState, prestigeState, uiState, combatState]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Computed effects from upgrades
|
||||||
|
const upgradeEffects = useMemo(
|
||||||
|
() => computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {}),
|
||||||
|
[skillState.skillUpgrades, skillState.skillTiers]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a minimal state object for compute functions
|
||||||
|
const stateForCompute = useMemo(() => ({
|
||||||
|
skills: skillState.skills,
|
||||||
|
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
||||||
|
skillUpgrades: skillState.skillUpgrades,
|
||||||
|
skillTiers: skillState.skillTiers,
|
||||||
|
signedPacts: prestigeState.signedPacts,
|
||||||
|
rawMana: manaState.rawMana,
|
||||||
|
meditateTicks: manaState.meditateTicks,
|
||||||
|
incursionStrength: gameStore.incursionStrength,
|
||||||
|
}), [skillState, prestigeState, manaState, gameStore.incursionStrength]);
|
||||||
|
|
||||||
|
// Derived stats
|
||||||
|
const maxMana = useMemo(
|
||||||
|
() => computeMaxMana(stateForCompute, upgradeEffects),
|
||||||
|
[stateForCompute, upgradeEffects]
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseRegen = useMemo(
|
||||||
|
() => computeRegen(stateForCompute, upgradeEffects),
|
||||||
|
[stateForCompute, upgradeEffects]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clickMana = useMemo(() => computeClickMana(stateForCompute), [stateForCompute]);
|
||||||
|
|
||||||
|
// Floor element from combat store
|
||||||
|
const floorElem = useMemo(() => getFloorElement(combatState.currentFloor), [combatState.currentFloor]);
|
||||||
|
const floorElemDef = ELEMENTS[floorElem];
|
||||||
|
const isGuardianFloor = !!GUARDIANS[combatState.currentFloor];
|
||||||
|
const currentGuardian = GUARDIANS[combatState.currentFloor];
|
||||||
|
const activeSpellDef = SPELLS_DEF[combatState.activeSpell];
|
||||||
|
|
||||||
|
const meditationMultiplier = useMemo(
|
||||||
|
() => getMeditationBonus(manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency),
|
||||||
|
[manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency]
|
||||||
|
);
|
||||||
|
|
||||||
|
const incursionStrength = useMemo(
|
||||||
|
() => getIncursionStrength(gameStore.day, gameStore.hour),
|
||||||
|
[gameStore.day, gameStore.hour]
|
||||||
|
);
|
||||||
|
|
||||||
|
const studySpeedMult = useMemo(
|
||||||
|
() => getStudySpeedMultiplier(skillState.skills),
|
||||||
|
[skillState.skills]
|
||||||
|
);
|
||||||
|
|
||||||
|
const studyCostMult = useMemo(
|
||||||
|
() => getStudyCostMultiplier(skillState.skills),
|
||||||
|
[skillState.skills]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Effective regen calculations
|
||||||
|
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||||||
|
|
||||||
|
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
||||||
|
? Math.floor(maxMana / 100) * 0.1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
||||||
|
? Math.floor(maxMana / 100) * 0.25
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
||||||
|
|
||||||
|
// Has special flags for UI
|
||||||
|
const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL);
|
||||||
|
const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE);
|
||||||
|
const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW);
|
||||||
|
const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW);
|
||||||
|
|
||||||
|
// Active boons
|
||||||
|
const activeBoons = useMemo(
|
||||||
|
() => getBoonBonuses(prestigeState.signedPacts),
|
||||||
|
[prestigeState.signedPacts]
|
||||||
|
);
|
||||||
|
|
||||||
|
// DPS calculation - based on active spell, attack speed, and damage
|
||||||
|
const dps = useMemo(() => {
|
||||||
|
if (!activeSpellDef) return 0;
|
||||||
|
const baseDmg = calcDamage(
|
||||||
|
{ skills: skillState.skills, signedPacts: prestigeState.signedPacts },
|
||||||
|
combatState.activeSpell,
|
||||||
|
floorElem
|
||||||
|
);
|
||||||
|
const dmgWithEffects = baseDmg * upgradeEffects.baseDamageMultiplier + upgradeEffects.baseDamageBonus;
|
||||||
|
const attackSpeed = (1 + (skillState.skills.quickCast || 0) * 0.05) * upgradeEffects.attackSpeedMultiplier;
|
||||||
|
const castSpeed = activeSpellDef.castSpeed || 1;
|
||||||
|
return dmgWithEffects * attackSpeed * castSpeed;
|
||||||
|
}, [activeSpellDef, skillState.skills, prestigeState.signedPacts, floorElem, upgradeEffects, combatState.activeSpell]);
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
const canCastSpell = (spellId: string): boolean => {
|
||||||
|
const spell = SPELLS_DEF[spellId];
|
||||||
|
if (!spell) return false;
|
||||||
|
return canAffordSpellCost(spell.cost, manaState.rawMana, manaState.elements);
|
||||||
|
};
|
||||||
|
|
||||||
|
const value: GameContextValue = {
|
||||||
|
store: unifiedStore,
|
||||||
|
skillStore: skillState,
|
||||||
|
manaStore: manaState,
|
||||||
|
prestigeStore: prestigeState,
|
||||||
|
uiStore: uiState,
|
||||||
|
combatStore: combatState,
|
||||||
|
upgradeEffects,
|
||||||
|
maxMana,
|
||||||
|
baseRegen,
|
||||||
|
clickMana,
|
||||||
|
floorElem,
|
||||||
|
floorElemDef,
|
||||||
|
isGuardianFloor,
|
||||||
|
currentGuardian,
|
||||||
|
activeSpellDef,
|
||||||
|
meditationMultiplier,
|
||||||
|
incursionStrength,
|
||||||
|
studySpeedMult,
|
||||||
|
studyCostMult,
|
||||||
|
effectiveRegenWithSpecials,
|
||||||
|
manaCascadeBonus,
|
||||||
|
manaWaterfallBonus,
|
||||||
|
effectiveRegen,
|
||||||
|
hasManaWaterfall,
|
||||||
|
hasFlowSurge,
|
||||||
|
hasManaOverflow,
|
||||||
|
hasEternalFlow,
|
||||||
|
dps,
|
||||||
|
activeBoons,
|
||||||
|
canCastSpell,
|
||||||
|
hasSpecial,
|
||||||
|
SPECIAL_EFFECTS,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <GameContext.Provider value={value}>{children}</GameContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameProvider.displayName = "GameProvider";
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import type { GameContextValue } from './types';
|
||||||
|
|
||||||
|
export const GameContext = createContext<GameContextValue | null>(null);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { GameContext } from './context-create';
|
||||||
|
import type { GameContextValue } from './types';
|
||||||
|
|
||||||
|
export function useGameContext(): GameContextValue {
|
||||||
|
const context = useContext(GameContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useGameContext must be used within a GameProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import type { ElementDef, GuardianDef, SpellDef, GameAction } from '@/lib/game/types';
|
||||||
|
import { useSkillStore } from '@/lib/game/stores/skillStore';
|
||||||
|
import { useManaStore } from '@/lib/game/stores/manaStore';
|
||||||
|
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
||||||
|
import { useUIStore } from '@/lib/game/stores/uiStore';
|
||||||
|
import { useCombatStore } from '@/lib/game/stores/combatStore';
|
||||||
|
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/upgrade-effects';
|
||||||
|
import { getBoonBonuses } from '@/lib/game/utils';
|
||||||
|
|
||||||
|
// Define a unified store type that combines all stores
|
||||||
|
export interface UnifiedStore {
|
||||||
|
// From gameStore (coordinator)
|
||||||
|
day: number;
|
||||||
|
hour: number;
|
||||||
|
incursionStrength: number;
|
||||||
|
containmentWards: number;
|
||||||
|
initialized: boolean;
|
||||||
|
tick: () => void;
|
||||||
|
resetGame: () => void;
|
||||||
|
gatherMana: () => void;
|
||||||
|
startNewLoop: () => void;
|
||||||
|
|
||||||
|
// From manaStore
|
||||||
|
rawMana: number;
|
||||||
|
meditateTicks: number;
|
||||||
|
totalManaGathered: number;
|
||||||
|
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||||||
|
setRawMana: (amount: number) => void;
|
||||||
|
addRawMana: (amount: number, max: number) => void;
|
||||||
|
spendRawMana: (amount: number) => boolean;
|
||||||
|
convertMana: (element: string, amount: number) => boolean;
|
||||||
|
unlockElement: (element: string, cost: number) => boolean;
|
||||||
|
craftComposite: (target: string, recipe: string[]) => boolean;
|
||||||
|
|
||||||
|
// From skillStore
|
||||||
|
skills: Record<string, number>;
|
||||||
|
skillProgress: Record<string, number>;
|
||||||
|
skillUpgrades: Record<string, string[]>;
|
||||||
|
skillTiers: Record<string, number>;
|
||||||
|
paidStudySkills: Record<string, number>;
|
||||||
|
currentStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
|
||||||
|
parallelStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
|
||||||
|
setSkillLevel: (skillId: string, level: number) => void;
|
||||||
|
startStudyingSkill: (skillId: string, rawMana: number) => { started: boolean; cost: number };
|
||||||
|
startStudyingSpell: (spellId: string, rawMana: number, studyTime: number) => { started: boolean; cost: number };
|
||||||
|
cancelStudy: (retentionBonus: number) => void;
|
||||||
|
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
||||||
|
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
||||||
|
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void;
|
||||||
|
tierUpSkill: (skillId: string) => void;
|
||||||
|
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => {
|
||||||
|
available: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
milestone: 5 | 10;
|
||||||
|
effect: { type: string; stat?: string; value?: number; specialId?: string }
|
||||||
|
}>;
|
||||||
|
selected: string[]
|
||||||
|
};
|
||||||
|
|
||||||
|
// From prestigeStore
|
||||||
|
loopCount: number;
|
||||||
|
insight: number;
|
||||||
|
totalInsight: number;
|
||||||
|
loopInsight: number;
|
||||||
|
prestigeUpgrades: Record<string, number>;
|
||||||
|
memorySlots: number;
|
||||||
|
pactSlots: number;
|
||||||
|
memories: Array<{ skillId: string; level: number; tier: number; upgrades: string[] }>;
|
||||||
|
defeatedGuardians: number[];
|
||||||
|
signedPacts: number[];
|
||||||
|
pactRitualFloor: number | null;
|
||||||
|
pactRitualProgress: number;
|
||||||
|
doPrestige: (id: string) => void;
|
||||||
|
addMemory: (memory: { skillId: string; level: number; tier: number; upgrades: string[] }) => void;
|
||||||
|
removeMemory: (skillId: string) => void;
|
||||||
|
clearMemories: () => void;
|
||||||
|
startPactRitual: (floor: number, rawMana: number) => boolean;
|
||||||
|
cancelPactRitual: () => void;
|
||||||
|
removePact: (floor: number) => void;
|
||||||
|
defeatGuardian: (floor: number) => void;
|
||||||
|
|
||||||
|
// From combatStore
|
||||||
|
currentFloor: number;
|
||||||
|
floorHP: number;
|
||||||
|
floorMaxHP: number;
|
||||||
|
maxFloorReached: number;
|
||||||
|
activeSpell: string;
|
||||||
|
currentAction: GameAction;
|
||||||
|
castProgress: number;
|
||||||
|
spells: Record<string, { learned: boolean; level: number; studyProgress?: number }>;
|
||||||
|
setAction: (action: GameAction) => void;
|
||||||
|
setSpell: (spellId: string) => void;
|
||||||
|
learnSpell: (spellId: string) => void;
|
||||||
|
advanceFloor: () => void;
|
||||||
|
|
||||||
|
// From uiStore
|
||||||
|
log: string[];
|
||||||
|
paused: boolean;
|
||||||
|
gameOver: boolean;
|
||||||
|
victory: boolean;
|
||||||
|
addLog: (message: string) => void;
|
||||||
|
togglePause: () => void;
|
||||||
|
setPaused: (paused: boolean) => void;
|
||||||
|
setGameOver: (gameOver: boolean, victory?: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameContextValue {
|
||||||
|
// Unified store for backward compatibility
|
||||||
|
store: UnifiedStore;
|
||||||
|
|
||||||
|
// Individual stores for direct access if needed
|
||||||
|
skillStore: ReturnType<typeof useSkillStore.getState>;
|
||||||
|
manaStore: ReturnType<typeof useManaStore.getState>;
|
||||||
|
prestigeStore: ReturnType<typeof usePrestigeStore.getState>;
|
||||||
|
uiStore: ReturnType<typeof useUIStore.getState>;
|
||||||
|
combatStore: ReturnType<typeof useCombatStore.getState>;
|
||||||
|
|
||||||
|
// Computed effects from upgrades
|
||||||
|
upgradeEffects: ReturnType<typeof computeEffects>;
|
||||||
|
|
||||||
|
// Derived stats
|
||||||
|
maxMana: number;
|
||||||
|
baseRegen: number;
|
||||||
|
clickMana: number;
|
||||||
|
floorElem: string;
|
||||||
|
floorElemDef: ElementDef | undefined;
|
||||||
|
isGuardianFloor: boolean;
|
||||||
|
currentGuardian: GuardianDef | undefined;
|
||||||
|
activeSpellDef: SpellDef | undefined;
|
||||||
|
meditationMultiplier: number;
|
||||||
|
incursionStrength: number;
|
||||||
|
studySpeedMult: number;
|
||||||
|
studyCostMult: number;
|
||||||
|
|
||||||
|
// Effective regen calculations
|
||||||
|
effectiveRegenWithSpecials: number;
|
||||||
|
manaCascadeBonus: number;
|
||||||
|
manaWaterfallBonus: number;
|
||||||
|
effectiveRegen: number;
|
||||||
|
|
||||||
|
// Has special flags
|
||||||
|
hasManaWaterfall: boolean;
|
||||||
|
hasFlowSurge: boolean;
|
||||||
|
hasManaOverflow: boolean;
|
||||||
|
hasEternalFlow: boolean;
|
||||||
|
|
||||||
|
// DPS calculation
|
||||||
|
dps: number;
|
||||||
|
|
||||||
|
// Boons
|
||||||
|
activeBoons: ReturnType<typeof getBoonBonuses>;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
canCastSpell: (spellId: string) => boolean;
|
||||||
|
hasSpecial: (effects: ReturnType<typeof computeEffects>, specialId: string) => boolean;
|
||||||
|
SPECIAL_EFFECTS: typeof SPECIAL_EFFECTS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Scroll } from 'lucide-react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { LOOT_DROPS } from '@/lib/game/data/loot-drops';
|
||||||
|
import { RARITY_CSS_VAR, RARITY_GLOW_CSS_VAR } from './types';
|
||||||
|
|
||||||
|
interface BlueprintsSectionProps {
|
||||||
|
blueprints: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlueprintsSection({ blueprints }: BlueprintsSectionProps) {
|
||||||
|
if (blueprints.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
||||||
|
<Scroll className="w-3 h-3" />
|
||||||
|
Blueprints (permanent)
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{blueprints.map((id) => {
|
||||||
|
const drop = LOOT_DROPS[id];
|
||||||
|
if (!drop) return null;
|
||||||
|
const rarityColor = RARITY_CSS_VAR[drop.rarity] || 'var(--rarity-common)';
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={id}
|
||||||
|
className="text-xs"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${RARITY_GLOW_CSS_VAR[drop.rarity] || 'var(--rarity-common-glow)'}`,
|
||||||
|
color: rarityColor,
|
||||||
|
borderColor: rarityColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drop.name}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)] mt-1 italic">
|
||||||
|
Blueprints are permanent unlocks - use them to craft equipment
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Package, Trash2 } from 'lucide-react';
|
||||||
|
import type { EquipmentInstance } from '@/lib/game/types';
|
||||||
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||||
|
import { CATEGORY_ICONS } from './icons';
|
||||||
|
import { RARITY_CSS_VAR, RARITY_GLOW_CSS_VAR } from './types';
|
||||||
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
|
|
||||||
|
interface EquipmentItemProps {
|
||||||
|
instanceId: string;
|
||||||
|
instance: EquipmentInstance;
|
||||||
|
onDelete?: (instanceId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EquipmentItem({ instanceId, instance, onDelete }: EquipmentItemProps) {
|
||||||
|
const type = EQUIPMENT_TYPES[instance.typeId];
|
||||||
|
const Icon = type ? CATEGORY_ICONS[type.category] || Package : Package;
|
||||||
|
const rarityColor = RARITY_CSS_VAR[instance.rarity] || 'var(--rarity-common)';
|
||||||
|
const rarityGlow = RARITY_GLOW_CSS_VAR[instance.rarity] || 'var(--rarity-common-glow)';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="p-2 rounded border bg-[var(--bg-sunken)] group"
|
||||||
|
style={{
|
||||||
|
borderColor: rarityColor,
|
||||||
|
backgroundColor: rarityGlow,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Icon className="w-4 h-4 mt-0.5" style={{ color: rarityColor }} />
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-semibold" style={{ color: rarityColor }}>
|
||||||
|
{instance.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--text-secondary)]">
|
||||||
|
{type?.name} • {instance.usedCapacity}/{instance.totalCapacity} cap
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)] capitalize">
|
||||||
|
{instance.rarity} • {instance.enchantments.length} enchants
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{onDelete && (
|
||||||
|
<ActionButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
||||||
|
onClick={() => onDelete(instanceId)}
|
||||||
|
aria-label={`Delete ${instance.name}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EquipmentSectionProps {
|
||||||
|
equipment: [string, EquipmentInstance][];
|
||||||
|
onDeleteEquipment?: (instanceId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EquipmentSection({ equipment, onDeleteEquipment }: EquipmentSectionProps) {
|
||||||
|
if (equipment.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
||||||
|
<Package className="w-3 h-3" />
|
||||||
|
Equipment
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{equipment.map(([id, instance]) => (
|
||||||
|
<EquipmentItem
|
||||||
|
key={id}
|
||||||
|
instanceId={id}
|
||||||
|
instance={instance}
|
||||||
|
onDelete={onDeleteEquipment}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Droplet } from 'lucide-react';
|
||||||
|
import { ElementBadge } from '@/components/ui/element-badge';
|
||||||
|
import type { ElementState } from '@/lib/game/types';
|
||||||
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
|
|
||||||
|
interface EssenceItemProps {
|
||||||
|
elementId: string;
|
||||||
|
state: ElementState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EssenceItem({ elementId, state }: EssenceItemProps) {
|
||||||
|
const elem = ELEMENTS[elementId];
|
||||||
|
if (!elem) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="p-2 rounded border bg-[var(--bg-sunken)]"
|
||||||
|
style={{
|
||||||
|
borderColor: `var(--mana-${elementId})`,
|
||||||
|
backgroundColor: `var(--mana-${elementId})20`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ElementBadge element={elementId} showIcon={true} size="sm" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--text-secondary)] mt-1">
|
||||||
|
{state.current} / {state.max}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EssenceSectionProps {
|
||||||
|
essence: [string, ElementState][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EssenceSection({ essence }: EssenceSectionProps) {
|
||||||
|
if (essence.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
||||||
|
<Droplet className="w-3 h-3" />
|
||||||
|
Elemental Essence
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{essence.map(([id, state]) => (
|
||||||
|
<EssenceItem key={id} elementId={id} state={state} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+39
-228
@@ -8,13 +8,11 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
import {
|
||||||
Gem, Sparkles, Scroll, Droplet, Trash2, Search,
|
Gem, Search, ArrowUpDown, AlertTriangle
|
||||||
Package, Sword, Shield, Shirt, Crown, ArrowUpDown,
|
|
||||||
Wrench, AlertTriangle
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ElementBadge } from '@/components/ui/element-badge';
|
import { ElementBadge } from '@/components/ui/element-badge';
|
||||||
import type { LootInventory as LootInventoryType, EquipmentInstance, ElementState } from '@/lib/game/types';
|
import type { LootInventory as LootInventoryType, EquipmentInstance, ElementState } from '@/lib/game/types';
|
||||||
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
import { LOOT_DROPS } from '@/lib/game/data/loot-drops';
|
||||||
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
import { useGameToast } from '@/components/game/GameToast';
|
import { useGameToast } from '@/components/game/GameToast';
|
||||||
@@ -29,6 +27,12 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
|
|
||||||
|
import { type SortMode, type FilterMode, RARITY_ORDER } from './types';
|
||||||
|
import { MaterialsSection } from './MaterialItem';
|
||||||
|
import { EssenceSection } from './EssenceItem';
|
||||||
|
import { BlueprintsSection } from './BlueprintsSection';
|
||||||
|
import { EquipmentSection } from './EquipmentItem';
|
||||||
|
|
||||||
interface LootInventoryProps {
|
interface LootInventoryProps {
|
||||||
inventory: LootInventoryType;
|
inventory: LootInventoryType;
|
||||||
elements?: Record<string, ElementState>;
|
elements?: Record<string, ElementState>;
|
||||||
@@ -37,49 +41,6 @@ interface LootInventoryProps {
|
|||||||
onDeleteEquipment?: (instanceId: string) => void;
|
onDeleteEquipment?: (instanceId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortMode = 'name' | 'rarity' | 'count';
|
|
||||||
type FilterMode = 'all' | 'materials' | 'essence' | 'blueprints' | 'equipment';
|
|
||||||
|
|
||||||
const RARITY_ORDER = {
|
|
||||||
common: 0,
|
|
||||||
uncommon: 1,
|
|
||||||
rare: 2,
|
|
||||||
epic: 3,
|
|
||||||
legendary: 4,
|
|
||||||
mythic: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Map rarity to CSS variable for colors
|
|
||||||
const RARITY_CSS_VAR: Record<string, string> = {
|
|
||||||
common: 'var(--rarity-common)',
|
|
||||||
uncommon: 'var(--rarity-uncommon)',
|
|
||||||
rare: 'var(--rarity-rare)',
|
|
||||||
epic: 'var(--rarity-epic)',
|
|
||||||
legendary: 'var(--rarity-legendary)',
|
|
||||||
mythic: 'var(--rarity-mythic)',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Map rarity to CSS variable for glow/background
|
|
||||||
const RARITY_GLOW_CSS_VAR: Record<string, string> = {
|
|
||||||
common: 'var(--rarity-common-glow)',
|
|
||||||
uncommon: 'var(--rarity-uncommon-glow)',
|
|
||||||
rare: 'var(--rarity-rare-glow)',
|
|
||||||
epic: 'var(--rarity-epic-glow)',
|
|
||||||
legendary: 'var(--rarity-legendary-glow)',
|
|
||||||
mythic: 'var(--rarity-mythic-glow)',
|
|
||||||
};
|
|
||||||
|
|
||||||
const CATEGORY_ICONS: Record<string, typeof Sword> = {
|
|
||||||
caster: Sword,
|
|
||||||
shield: Shield,
|
|
||||||
catalyst: Sparkles,
|
|
||||||
head: Crown,
|
|
||||||
body: Shirt,
|
|
||||||
hands: Wrench,
|
|
||||||
feet: Package,
|
|
||||||
accessory: Gem,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LootInventoryDisplay({
|
export function LootInventoryDisplay({
|
||||||
inventory,
|
inventory,
|
||||||
elements,
|
elements,
|
||||||
@@ -131,7 +92,7 @@ export function LootInventoryDisplay({
|
|||||||
? Object.entries(elements)
|
? Object.entries(elements)
|
||||||
.filter(([id, state]) => {
|
.filter(([id, state]) => {
|
||||||
if (!state.unlocked || state.current <= 0) return false;
|
if (!state.unlocked || state.current <= 0) return false;
|
||||||
if (id === 'transference') return false; // Transference is not loot
|
if (id === 'transference') return false;
|
||||||
if (searchTerm && !ELEMENTS[id]?.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
if (searchTerm && !ELEMENTS[id]?.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
@@ -167,22 +128,6 @@ export function LootInventoryDisplay({
|
|||||||
// Check if we have anything to show
|
// Check if we have anything to show
|
||||||
const hasItems = totalItems > 0 || essenceCount > 0;
|
const hasItems = totalItems > 0 || essenceCount > 0;
|
||||||
|
|
||||||
if (!hasItems) {
|
|
||||||
return (
|
|
||||||
<GameCard variant="default" className="w-full">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Gem className="w-4 h-4 text-[var(--mana-light)]" />
|
|
||||||
<h3 className="text-[var(--mana-light)] game-panel-title text-xs uppercase tracking-wider">
|
|
||||||
Inventory
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="text-[var(--text-muted)] text-sm text-center py-4">
|
|
||||||
No items collected yet. Defeat floors and guardians to find loot!
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteMaterial = (materialId: string) => {
|
const handleDeleteMaterial = (materialId: string) => {
|
||||||
const drop = LOOT_DROPS[materialId];
|
const drop = LOOT_DROPS[materialId];
|
||||||
if (drop) {
|
if (drop) {
|
||||||
@@ -212,6 +157,22 @@ export function LootInventoryDisplay({
|
|||||||
setDeleteConfirm(null);
|
setDeleteConfirm(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!hasItems) {
|
||||||
|
return (
|
||||||
|
<GameCard variant="default" className="w-full">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Gem className="w-4 h-4 text-[var(--mana-light)]" />
|
||||||
|
<h3 className="text-[var(--mana-light)] game-panel-title text-xs uppercase tracking-wider">
|
||||||
|
Inventory
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-[var(--text-muted)] text-sm text-center py-4">
|
||||||
|
No items collected yet. Defeat floors and guardians to find loot!
|
||||||
|
</div>
|
||||||
|
</GameCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GameCard variant="default" className="w-full">
|
<GameCard variant="default" className="w-full">
|
||||||
@@ -279,179 +240,29 @@ export function LootInventoryDisplay({
|
|||||||
<ScrollArea className="h-64 w-full">
|
<ScrollArea className="h-64 w-full">
|
||||||
<div className="space-y-3 pr-2">
|
<div className="space-y-3 pr-2">
|
||||||
{/* Materials */}
|
{/* Materials */}
|
||||||
{(filterMode === 'all' || filterMode === 'materials') && filteredMaterials.length > 0 && (
|
{(filterMode === 'all' || filterMode === 'materials') && (
|
||||||
<div>
|
<MaterialsSection
|
||||||
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
materials={filteredMaterials}
|
||||||
<Sparkles className="w-3 h-3" />
|
onDeleteMaterial={handleDeleteMaterial}
|
||||||
Materials
|
/>
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{filteredMaterials.map(([id, count]) => {
|
|
||||||
const drop = LOOT_DROPS[id];
|
|
||||||
if (!drop) return null;
|
|
||||||
const rarityColor = RARITY_CSS_VAR[drop.rarity] || 'var(--rarity-common)';
|
|
||||||
const rarityGlow = RARITY_GLOW_CSS_VAR[drop.rarity] || 'var(--rarity-common-glow)';
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={id}
|
|
||||||
className="p-2 rounded border bg-[var(--bg-sunken)] group relative"
|
|
||||||
style={{
|
|
||||||
borderColor: rarityColor,
|
|
||||||
backgroundColor: rarityGlow,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-semibold" style={{ color: rarityColor }}>
|
|
||||||
{drop.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-secondary)]">
|
|
||||||
x{count}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)] capitalize">
|
|
||||||
{drop.rarity}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{onDeleteMaterial && (
|
|
||||||
<ActionButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
|
||||||
onClick={() => handleDeleteMaterial(id)}
|
|
||||||
aria-label={`Delete ${drop.name}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</ActionButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Essence */}
|
{/* Essence */}
|
||||||
{(filterMode === 'all' || filterMode === 'essence') && filteredEssence.length > 0 && (
|
{(filterMode === 'all' || filterMode === 'essence') && (
|
||||||
<div>
|
<EssenceSection essence={filteredEssence} />
|
||||||
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
|
||||||
<Droplet className="w-3 h-3" />
|
|
||||||
Elemental Essence
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{filteredEssence.map(([id, state]) => {
|
|
||||||
const elem = ELEMENTS[id];
|
|
||||||
if (!elem) return null;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={id}
|
|
||||||
className="p-2 rounded border bg-[var(--bg-sunken)]"
|
|
||||||
style={{
|
|
||||||
borderColor: `var(--mana-${id})`,
|
|
||||||
backgroundColor: `var(--mana-${id})20`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<ElementBadge element={id} showIcon={true} size="sm" />
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-secondary)] mt-1">
|
|
||||||
{state.current} / {state.max}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Blueprints */}
|
{/* Blueprints */}
|
||||||
{(filterMode === 'all' || filterMode === 'blueprints') && inventory.blueprints.length > 0 && (
|
{(filterMode === 'all' || filterMode === 'blueprints') && (
|
||||||
<div>
|
<BlueprintsSection blueprints={inventory.blueprints} />
|
||||||
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
|
||||||
<Scroll className="w-3 h-3" />
|
|
||||||
Blueprints (permanent)
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{inventory.blueprints.map((id) => {
|
|
||||||
const drop = LOOT_DROPS[id];
|
|
||||||
if (!drop) return null;
|
|
||||||
const rarityColor = RARITY_CSS_VAR[drop.rarity] || 'var(--rarity-common)';
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
key={id}
|
|
||||||
className="text-xs"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${RARITY_GLOW_CSS_VAR[drop.rarity] || 'var(--rarity-common-glow)'}`,
|
|
||||||
color: rarityColor,
|
|
||||||
borderColor: rarityColor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{drop.name}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)] mt-1 italic">
|
|
||||||
Blueprints are permanent unlocks - use them to craft equipment
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Equipment */}
|
{/* Equipment */}
|
||||||
{(filterMode === 'all' || filterMode === 'equipment') && filteredEquipment.length > 0 && (
|
{(filterMode === 'all' || filterMode === 'equipment') && (
|
||||||
<div>
|
<EquipmentSection
|
||||||
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
equipment={filteredEquipment}
|
||||||
<Package className="w-3 h-3" />
|
onDeleteEquipment={handleDeleteEquipment}
|
||||||
Equipment
|
/>
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{filteredEquipment.map(([id, instance]) => {
|
|
||||||
const type = EQUIPMENT_TYPES[instance.typeId];
|
|
||||||
const Icon = type ? CATEGORY_ICONS[type.category] || Package : Package;
|
|
||||||
const rarityColor = RARITY_CSS_VAR[instance.rarity] || 'var(--rarity-common)';
|
|
||||||
const rarityGlow = RARITY_GLOW_CSS_VAR[instance.rarity] || 'var(--rarity-common-glow)';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={id}
|
|
||||||
className="p-2 rounded border bg-[var(--bg-sunken)] group"
|
|
||||||
style={{
|
|
||||||
borderColor: rarityColor,
|
|
||||||
backgroundColor: rarityGlow,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<Icon className="w-4 h-4 mt-0.5" style={{ color: rarityColor }} />
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-semibold" style={{ color: rarityColor }}>
|
|
||||||
{instance.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-secondary)]">
|
|
||||||
{type?.name} • {instance.usedCapacity}/{instance.totalCapacity} cap
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)] capitalize">
|
|
||||||
{instance.rarity} • {instance.enchantments.length} enchants
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{onDeleteEquipment && (
|
|
||||||
<ActionButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
|
||||||
onClick={() => handleDeleteEquipment(id)}
|
|
||||||
aria-label={`Delete ${instance.name}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
</ActionButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { LootInventory } from '@/lib/game/types';
|
||||||
|
// For backward compatibility
|
||||||
|
type LootInventoryType = LootInventory;
|
||||||
|
import { LOOT_DROPS } from '@/lib/game/data/loot-drops';
|
||||||
|
import { RARITY_CSS_VAR, RARITY_GLOW_CSS_VAR } from './types';
|
||||||
|
import { Sparkles, Trash2 } from 'lucide-react';
|
||||||
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
|
|
||||||
|
interface MaterialItemProps {
|
||||||
|
materialId: string;
|
||||||
|
count: number;
|
||||||
|
onDelete?: (materialId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MaterialItem({ materialId, count, onDelete }: MaterialItemProps) {
|
||||||
|
const drop = LOOT_DROPS[materialId];
|
||||||
|
if (!drop) return null;
|
||||||
|
|
||||||
|
const rarityColor = RARITY_CSS_VAR[drop.rarity] || 'var(--rarity-common)';
|
||||||
|
const rarityGlow = RARITY_GLOW_CSS_VAR[drop.rarity] || 'var(--rarity-common-glow)';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="p-2 rounded border bg-[var(--bg-sunken)] group relative"
|
||||||
|
style={{
|
||||||
|
borderColor: rarityColor,
|
||||||
|
backgroundColor: rarityGlow,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-semibold" style={{ color: rarityColor }}>
|
||||||
|
{drop.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--text-secondary)]">
|
||||||
|
x{count}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)] capitalize">
|
||||||
|
{drop.rarity}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{onDelete && (
|
||||||
|
<ActionButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
||||||
|
onClick={() => onDelete(materialId)}
|
||||||
|
aria-label={`Delete ${drop.name}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MaterialsSectionProps {
|
||||||
|
materials: [string, number][];
|
||||||
|
onDeleteMaterial?: (materialId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MaterialsSection({ materials, onDeleteMaterial }: MaterialsSectionProps) {
|
||||||
|
if (materials.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)] mb-2 flex items-center gap-1">
|
||||||
|
<Sparkles className="w-3 h-3" />
|
||||||
|
Materials
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{materials.map(([id, count]) => (
|
||||||
|
<MaterialItem
|
||||||
|
key={id}
|
||||||
|
materialId={id}
|
||||||
|
count={count}
|
||||||
|
onDelete={onDeleteMaterial}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Gem, Sparkles, Scroll, Droplet, Trash2, Search,
|
||||||
|
Package, Sword, Shield, Shirt, Crown, ArrowUpDown,
|
||||||
|
Wrench, AlertTriangle } from 'lucide-react';
|
||||||
|
import type { EquipmentCategory } from '@/lib/game/data/equipment';
|
||||||
|
|
||||||
|
export const CATEGORY_ICONS: Record<string, typeof Sword> = {
|
||||||
|
caster: Sword,
|
||||||
|
shield: Shield,
|
||||||
|
catalyst: Sparkles,
|
||||||
|
head: Crown,
|
||||||
|
body: Shirt,
|
||||||
|
hands: Wrench,
|
||||||
|
feet: Package,
|
||||||
|
accessory: Gem,
|
||||||
|
};
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { GameCard } from '@/components/ui/game-card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Gem, Search, ArrowUpDown, AlertTriangle
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { ElementBadge } from '@/components/ui/element-badge';
|
||||||
|
import type { LootInventory as LootInventoryType, EquipmentInstance, ElementState } from '@/lib/game/types';
|
||||||
|
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
||||||
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||||
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
|
import { useGameToast } from '@/components/game/GameToast';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
|
||||||
|
import { type SortMode, type FilterMode, RARITY_ORDER, RARITY_CSS_VAR, RARITY_GLOW_CSS_VAR } from './types';
|
||||||
|
import { MaterialsSection } from './MaterialItem';
|
||||||
|
import { EssenceSection } from './EssenceItem';
|
||||||
|
import { BlueprintsSection } from './BlueprintsSection';
|
||||||
|
import { EquipmentSection } from './EquipmentItem';
|
||||||
|
|
||||||
|
interface LootInventoryProps {
|
||||||
|
inventory: LootInventoryType;
|
||||||
|
elements?: Record<string, ElementState>;
|
||||||
|
equipmentInstances?: Record<string, EquipmentInstance>;
|
||||||
|
onDeleteMaterial?: (materialId: string, amount: number) => void;
|
||||||
|
onDeleteEquipment?: (instanceId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LootInventoryDisplay({
|
||||||
|
inventory,
|
||||||
|
elements,
|
||||||
|
equipmentInstances = {},
|
||||||
|
onDeleteMaterial,
|
||||||
|
onDeleteEquipment,
|
||||||
|
}: LootInventoryProps) {
|
||||||
|
const showToast = useGameToast();
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [sortMode, setSortMode] = useState<SortMode>('rarity');
|
||||||
|
const [filterMode, setFilterMode] = useState<FilterMode>('all');
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'material' | 'equipment'; id: string; name: string } | null>(null);
|
||||||
|
|
||||||
|
// Count items
|
||||||
|
const materialCount = Object.values(inventory.materials).reduce((a: number, b: number) => a + b, 0);
|
||||||
|
|
||||||
|
// Calculate essence count
|
||||||
|
let essenceCount = 0;
|
||||||
|
if (elements) {
|
||||||
|
essenceCount = Object.entries(elements).reduce((acc: number, [id, state]) => {
|
||||||
|
if (id === 'transference') return acc;
|
||||||
|
return acc + (state.current || 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blueprintCount = inventory.blueprints.length;
|
||||||
|
const equipmentCount = Object.keys(equipmentInstances).length;
|
||||||
|
const totalItems = materialCount + blueprintCount + equipmentCount;
|
||||||
|
|
||||||
|
// Filter and sort materials
|
||||||
|
const filteredMaterials = Object.entries(inventory.materials)
|
||||||
|
.filter(([id, count]) => {
|
||||||
|
if (count <= 0) return false;
|
||||||
|
const drop = LOOT_DROPS[id];
|
||||||
|
if (!drop) return false;
|
||||||
|
if (searchTerm && !drop.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sort(([aId, aCount], [bId, bCount]) => {
|
||||||
|
const aDrop = LOOT_DROPS[aId];
|
||||||
|
const bDrop = LOOT_DROPS[bId];
|
||||||
|
if (!aDrop || !bDrop) return 0;
|
||||||
|
|
||||||
|
switch (sortMode) {
|
||||||
|
case 'name':
|
||||||
|
return aDrop.name.localeCompare(bDrop.name);
|
||||||
|
case 'rarity':
|
||||||
|
return RARITY_ORDER[bDrop.rarity] - RARITY_ORDER[aDrop.rarity];
|
||||||
|
case 'count':
|
||||||
|
return bCount - aCount;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter and sort essence
|
||||||
|
const filteredEssence = elements
|
||||||
|
? Object.entries(elements)
|
||||||
|
.filter(([id, state]) => {
|
||||||
|
if (!state.unlocked || state.current <= 0) return false;
|
||||||
|
if (id === 'transference') return false;
|
||||||
|
if (searchTerm && !ELEMENTS[id]?.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sort(([aId, aState], [bId, bState]) => {
|
||||||
|
switch (sortMode) {
|
||||||
|
case 'name':
|
||||||
|
return (ELEMENTS[aId]?.name || aId).localeCompare(ELEMENTS[bId]?.name || bId);
|
||||||
|
case 'count':
|
||||||
|
return bState.current - aState.current;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Filter and sort equipment
|
||||||
|
const filteredEquipment = Object.entries(equipmentInstances)
|
||||||
|
.filter(([id, instance]) => {
|
||||||
|
if (searchTerm && !instance.name.toLowerCase().includes(searchTerm.toLowerCase())) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sort(([aId, aInst], [bId, bInst]) => {
|
||||||
|
switch (sortMode) {
|
||||||
|
case 'name':
|
||||||
|
return aInst.name.localeCompare(bInst.name);
|
||||||
|
case 'rarity':
|
||||||
|
return RARITY_ORDER[bInst.rarity] - RARITY_ORDER[aInst.rarity];
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasItems = totalItems > 0 || essenceCount > 0;
|
||||||
|
|
||||||
|
const handleDeleteMaterial = (materialId: string) => {
|
||||||
|
const drop = LOOT_DROPS[materialId];
|
||||||
|
if (drop) {
|
||||||
|
setDeleteConfirm({ type: 'material', id: materialId, name: drop.name });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteEquipment = (instanceId: string) => {
|
||||||
|
const instance = equipmentInstances[instanceId];
|
||||||
|
if (instance) {
|
||||||
|
setDeleteConfirm({ type: 'equipment', id: instanceId, name: instance.name });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (!deleteConfirm) return;
|
||||||
|
|
||||||
|
if (deleteConfirm.type === 'material' && onDeleteMaterial) {
|
||||||
|
const amount = inventory.materials[deleteConfirm.id] || 0;
|
||||||
|
onDeleteMaterial(deleteConfirm.id, amount);
|
||||||
|
showToast('success', 'Material Deleted', `${deleteConfirm.name} removed from inventory`);
|
||||||
|
} else if (deleteConfirm.type === 'equipment' && onDeleteEquipment) {
|
||||||
|
onDeleteEquipment(deleteConfirm.id);
|
||||||
|
showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasItems) {
|
||||||
|
return (
|
||||||
|
<GameCard variant="default" className="w-full">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Gem className="w-4 h-4 text-[var(--mana-light)]" />
|
||||||
|
<h3 className="text-[var(--mana-light)] game-panel-title text-xs uppercase tracking-wider">
|
||||||
|
Inventory
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-[var(--text-muted)] text-sm text-center py-4">
|
||||||
|
No items collected yet. Defeat floors and guardians to find loot!
|
||||||
|
</div>
|
||||||
|
</GameCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GameCard variant="default" className="w-full">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Gem className="w-4 h-4 text-[var(--mana-light)]" />
|
||||||
|
<h3 className="text-[var(--mana-light)] game-panel-title text-xs uppercase tracking-wider">
|
||||||
|
Inventory
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
className="ml-auto bg-[var(--bg-sunken)] text-[var(--text-secondary)] text-xs border-[var(--border-subtle)]"
|
||||||
|
aria-label={`${totalItems} items in inventory`}
|
||||||
|
>
|
||||||
|
{totalItems} items
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filter Controls */}
|
||||||
|
<div className="flex gap-2 mb-3">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--text-muted)]" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="h-7 pl-7 bg-[var(--bg-sunken)] border-[var(--border-subtle)] text-xs text-[var(--text-primary)] placeholder:text-[var(--text-disabled)]"
|
||||||
|
aria-label="Search inventory"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ActionButton
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2"
|
||||||
|
onClick={() => setSortMode(sortMode === 'rarity' ? 'name' : sortMode === 'name' ? 'count' : 'rarity')}
|
||||||
|
aria-label={`Sort by ${sortMode === 'rarity' ? 'name' : sortMode === 'name' ? 'count' : 'rarity'}`}
|
||||||
|
>
|
||||||
|
<ArrowUpDown className="w-3 h-3" />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Tabs */}
|
||||||
|
<div className="flex gap-1 flex-wrap mb-3">
|
||||||
|
{[
|
||||||
|
{ mode: 'all' as FilterMode, label: 'All' },
|
||||||
|
{ mode: 'materials' as FilterMode, label: `Materials (${materialCount})` },
|
||||||
|
{ mode: 'essence' as FilterMode, label: `Essence (${essenceCount})` },
|
||||||
|
{ mode: 'blueprints' as FilterMode, label: `Blueprints (${blueprintCount})` },
|
||||||
|
{ mode: 'equipment' as FilterMode, label: `Equipment (${equipmentCount})` },
|
||||||
|
].map(({ mode, label }) => (
|
||||||
|
<ActionButton
|
||||||
|
key={mode}
|
||||||
|
variant={filterMode === mode ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
className={`h-6 px-2 text-xs ${filterMode === mode ? '' : 'bg-[var(--bg-sunken)]'}`}
|
||||||
|
onClick={() => setFilterMode(mode)}
|
||||||
|
aria-pressed={filterMode === mode}
|
||||||
|
aria-label={`Filter by ${label}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</ActionButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="bg-[var(--border-subtle)] mb-3" />
|
||||||
|
|
||||||
|
<ScrollArea className="h-64 w-full">
|
||||||
|
<div className="space-y-3 pr-2">
|
||||||
|
{/* Materials */}
|
||||||
|
{(filterMode === 'all' || filterMode === 'materials') && (
|
||||||
|
<MaterialsSection
|
||||||
|
materials={filteredMaterials}
|
||||||
|
onDeleteMaterial={handleDeleteMaterial}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Essence */}
|
||||||
|
{(filterMode === 'all' || filterMode === 'essence') && (
|
||||||
|
<EssenceSection essence={filteredEssence} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Blueprints */}
|
||||||
|
{(filterMode === 'all' || filterMode === 'blueprints') && (
|
||||||
|
<BlueprintsSection blueprints={inventory.blueprints} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Equipment */}
|
||||||
|
{(filterMode === 'all' || filterMode === 'equipment') && (
|
||||||
|
<EquipmentSection
|
||||||
|
equipment={filteredEquipment}
|
||||||
|
onDeleteEquipment={handleDeleteEquipment}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</GameCard>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<AlertDialog open={!!deleteConfirm} onOpenChange={() => setDeleteConfirm(null)}>
|
||||||
|
<AlertDialogContent className="bg-[var(--bg-surface)] border-[var(--border-default)]">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="text-[var(--mana-light)] flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-5 h-5" />
|
||||||
|
Delete Item
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="text-[var(--text-secondary)]">
|
||||||
|
Are you sure you want to delete <strong className="text-[var(--text-primary)]">{deleteConfirm?.name}</strong>?
|
||||||
|
{deleteConfirm?.type === 'material' && (
|
||||||
|
<span className="block mt-2 text-[var(--color-danger)]">
|
||||||
|
This will delete ALL {inventory.materials[deleteConfirm?.id || ''] || 0} of this material!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{deleteConfirm?.type === 'equipment' && (
|
||||||
|
<span className="block mt-2 text-[var(--color-danger)]">
|
||||||
|
This equipment and all its enchantments will be permanently lost!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel className="bg-[var(--bg-sunken)] border-[var(--border-default)] text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]">
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-[var(--interactive-danger)] hover:bg-[var(--interactive-danger-hover)] text-white"
|
||||||
|
onClick={confirmDelete}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LootInventoryDisplay.displayName = "LootInventoryDisplay";
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import type { LootInventory as LootInventoryType, EquipmentInstance, ElementState } from '@/lib/game/types';
|
||||||
|
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
||||||
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||||
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
|
|
||||||
|
export type SortMode = 'name' | 'rarity' | 'count';
|
||||||
|
export type FilterMode = 'all' | 'materials' | 'essence' | 'blueprints' | 'equipment';
|
||||||
|
|
||||||
|
export const RARITY_ORDER = {
|
||||||
|
common: 0,
|
||||||
|
uncommon: 1,
|
||||||
|
rare: 2,
|
||||||
|
epic: 3,
|
||||||
|
legendary: 4,
|
||||||
|
mythic: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map rarity to CSS variable for colors
|
||||||
|
export const RARITY_CSS_VAR: Record<string, string> = {
|
||||||
|
common: 'var(--rarity-common)',
|
||||||
|
uncommon: 'var(--rarity-uncommon)',
|
||||||
|
rare: 'var(--rarity-rare)',
|
||||||
|
epic: 'var(--rarity-epic)',
|
||||||
|
legendary: 'var(--rarity-legendary)',
|
||||||
|
mythic: 'var(--rarity-mythic)',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map rarity to CSS variable for glow/background
|
||||||
|
export const RARITY_GLOW_CSS_VAR: Record<string, string> = {
|
||||||
|
common: 'var(--rarity-common-glow)',
|
||||||
|
uncommon: 'var(--rarity-uncommon-glow)',
|
||||||
|
rare: 'var(--rarity-rare-glow)',
|
||||||
|
epic: 'var(--rarity-epic-glow)',
|
||||||
|
legendary: 'var(--rarity-legendary-glow)',
|
||||||
|
mythic: 'var(--rarity-mythic-glow)',
|
||||||
|
};
|
||||||
@@ -1,432 +1,52 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useGameStore, fmt, fmtDec } from '@/lib/game/store';
|
import { useGameStore } from '@/lib/game/store';
|
||||||
import { SKILLS_DEF, SKILL_CATEGORIES } from '@/lib/game/constants';
|
import { SKILL_CATEGORIES } from '@/lib/game/constants';
|
||||||
import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { computeEffects } from '@/lib/game/upgrade-effects';
|
import { SkillUpgradeDialog } from './SkillsTab/SkillUpgradeDialog';
|
||||||
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
import { SkillStudyProgress } from './SkillsTab/SkillStudyProgress';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { SkillCategory } from './SkillsTab/SkillCategory';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { BookOpen, X } from 'lucide-react';
|
|
||||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
|
||||||
|
|
||||||
// Format study time
|
|
||||||
function formatStudyTime(hours: number): string {
|
|
||||||
if (hours < 1) return `${Math.round(hours * 60)}m`;
|
|
||||||
return `${hours.toFixed(1)}h`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SkillsTab() {
|
export function SkillsTab() {
|
||||||
const store = useGameStore();
|
const store = useGameStore();
|
||||||
const { studySpeedMult, studyCostMult, hasParallelStudy } = useStudyStats();
|
|
||||||
|
|
||||||
const [upgradeDialogSkill, setUpgradeDialogSkill] = useState<string | null>(null);
|
const [upgradeDialogSkill, setUpgradeDialogSkill] = useState<string | null>(null);
|
||||||
const [upgradeDialogMilestone, setUpgradeDialogMilestone] = useState<5 | 10>(5);
|
const [upgradeDialogMilestone, setUpgradeDialogMilestone] = useState<5 | 10>(5);
|
||||||
const [pendingUpgradeSelections, setPendingUpgradeSelections] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const upgradeEffects = computeEffects(store.skillUpgrades || {}, store.skillTiers || {});
|
const handleUpgradeClick = (skillId: string, milestone: 5 | 10) => {
|
||||||
|
setUpgradeDialogSkill(skillId);
|
||||||
// Check if skill has milestone available
|
setUpgradeDialogMilestone(milestone);
|
||||||
const hasMilestoneUpgrade = (skillId: string, level: number): { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null => {
|
|
||||||
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
|
||||||
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
|
|
||||||
if (!path) return null;
|
|
||||||
|
|
||||||
if (level >= 5) {
|
|
||||||
const upgrades5 = getUpgradesForSkillAtMilestone(skillId, 5, store.skillTiers);
|
|
||||||
const selected5 = (store.skillUpgrades[skillId] || []).filter(id => id.includes('_l5'));
|
|
||||||
if (upgrades5.length > 0 && selected5.length < 2) {
|
|
||||||
return { milestone: 5, hasUpgrades: true, selectedCount: selected5.length };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (level >= 10) {
|
|
||||||
const upgrades10 = getUpgradesForSkillAtMilestone(skillId, 10, store.skillTiers);
|
|
||||||
const selected10 = (store.skillUpgrades[skillId] || []).filter(id => id.includes('_l10'));
|
|
||||||
if (upgrades10.length > 0 && selected10.length < 2) {
|
|
||||||
return { milestone: 10, hasUpgrades: true, selectedCount: selected10.length };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render upgrade selection dialog
|
const handleUpgradeClose = () => {
|
||||||
const renderUpgradeDialog = () => {
|
setUpgradeDialogSkill(null);
|
||||||
if (!upgradeDialogSkill) return null;
|
|
||||||
|
|
||||||
const skillDef = SKILLS_DEF[upgradeDialogSkill];
|
|
||||||
const level = store.skills[upgradeDialogSkill] || 0;
|
|
||||||
const { available, selected: alreadySelected } = store.getSkillUpgradeChoices(upgradeDialogSkill, upgradeDialogMilestone);
|
|
||||||
|
|
||||||
const currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected;
|
|
||||||
|
|
||||||
const toggleUpgrade = (upgradeId: string) => {
|
|
||||||
if (currentSelections.includes(upgradeId)) {
|
|
||||||
setPendingUpgradeSelections(currentSelections.filter(id => id !== upgradeId));
|
|
||||||
} else if (currentSelections.length < 2) {
|
|
||||||
setPendingUpgradeSelections([...currentSelections, upgradeId]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDone = () => {
|
|
||||||
if (currentSelections.length === 2 && upgradeDialogSkill) {
|
|
||||||
store.commitSkillUpgrades(upgradeDialogSkill, currentSelections, upgradeDialogMilestone);
|
|
||||||
}
|
|
||||||
setPendingUpgradeSelections([]);
|
|
||||||
setUpgradeDialogSkill(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setPendingUpgradeSelections([]);
|
|
||||||
setUpgradeDialogSkill(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={!!upgradeDialogSkill} onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setPendingUpgradeSelections([]);
|
|
||||||
setUpgradeDialogSkill(null);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-amber-400">
|
|
||||||
Choose Upgrade - {skillDef?.name || upgradeDialogSkill}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className="text-gray-400">
|
|
||||||
Level {upgradeDialogMilestone} Milestone - Select 2 upgrades ({currentSelections.length}/2 chosen)
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-2 mt-4">
|
|
||||||
{available.map((upgrade) => {
|
|
||||||
const isSelected = currentSelections.includes(upgrade.id);
|
|
||||||
const canToggle = currentSelections.length < 2 || isSelected;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={upgrade.id}
|
|
||||||
className={`p-3 rounded border cursor-pointer transition-all ${
|
|
||||||
isSelected
|
|
||||||
? 'border-amber-500 bg-amber-900/30'
|
|
||||||
: canToggle
|
|
||||||
? 'border-gray-600 bg-gray-800/50 hover:border-amber-500/50 hover:bg-gray-800'
|
|
||||||
: 'border-gray-700 bg-gray-800/30 opacity-50 cursor-not-allowed'
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (canToggle) {
|
|
||||||
toggleUpgrade(upgrade.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="font-semibold text-sm text-amber-300">{upgrade.name}</div>
|
|
||||||
{isSelected && <Badge className="bg-amber-600 text-amber-100">Selected</Badge>}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
|
||||||
{upgrade.effect.type === 'multiplier' && (
|
|
||||||
<div className="text-xs text-green-400 mt-1">
|
|
||||||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{upgrade.effect.type === 'bonus' && (
|
|
||||||
<div className="text-xs text-blue-400 mt-1">
|
|
||||||
+{upgrade.effect.value} {upgrade.effect.stat}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{upgrade.effect.type === 'special' && (
|
|
||||||
<div className="text-xs text-cyan-400 mt-1">
|
|
||||||
⚡ {upgrade.effect.specialDesc || 'Special effect'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-4">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
onClick={handleDone}
|
|
||||||
disabled={currentSelections.length !== 2}
|
|
||||||
>
|
|
||||||
{currentSelections.length < 2 ? `Select ${2 - currentSelections.length} more` : 'Confirm'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render study progress
|
|
||||||
const renderStudyProgress = () => {
|
|
||||||
if (!store.currentStudyTarget) return null;
|
|
||||||
|
|
||||||
const target = store.currentStudyTarget;
|
|
||||||
const progressPct = Math.min(100, (target.progress / target.required) * 100);
|
|
||||||
const def = SKILLS_DEF[target.id] || SKILLS_DEF[target.id.split('_t')[0]];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-3 rounded border border-purple-600/50 bg-purple-900/20">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<BookOpen className="w-4 h-4 text-purple-400" />
|
|
||||||
<span className="text-sm font-semibold text-purple-300">
|
|
||||||
{def?.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
|
||||||
onClick={() => store.cancelStudy()}
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
||||||
<span>{formatStudyTime(target.progress)} / {formatStudyTime(target.required)}</span>
|
|
||||||
<span>{studySpeedMult.toFixed(1)}x speed</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Upgrade Selection Dialog */}
|
{/* Upgrade Selection Dialog */}
|
||||||
{renderUpgradeDialog()}
|
<SkillUpgradeDialog
|
||||||
|
skillId={upgradeDialogSkill}
|
||||||
|
milestone={upgradeDialogMilestone}
|
||||||
|
onClose={handleUpgradeClose}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Current Study Progress */}
|
{/* Current Study Progress */}
|
||||||
{store.currentStudyTarget && store.currentStudyTarget.type === 'skill' && (
|
{store.currentStudyTarget && store.currentStudyTarget.type === 'skill' && (
|
||||||
<Card className="bg-gray-900/80 border-purple-600/50">
|
<Card className="bg-gray-900/80 border-purple-600/50">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
{renderStudyProgress()}
|
<SkillStudyProgress />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{SKILL_CATEGORIES.map((cat) => {
|
{SKILL_CATEGORIES.map((cat) => (
|
||||||
const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === cat.id);
|
<SkillCategory
|
||||||
if (skillsInCat.length === 0) return null;
|
key={cat.id}
|
||||||
|
categoryId={cat.id}
|
||||||
return (
|
onUpgradeClick={handleUpgradeClick}
|
||||||
<Card key={cat.id} className="bg-gray-900/80 border-gray-700">
|
/>
|
||||||
<CardHeader className="pb-2">
|
))}
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
|
||||||
{cat.icon} {cat.name}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{skillsInCat.map(([id, def]) => {
|
|
||||||
const currentTier = store.skillTiers?.[id] || 1;
|
|
||||||
const tieredSkillId = currentTier > 1 ? `${id}_t${currentTier}` : id;
|
|
||||||
const tierMultiplier = getTierMultiplier(tieredSkillId);
|
|
||||||
|
|
||||||
const level = store.skills[tieredSkillId] || store.skills[id] || 0;
|
|
||||||
const maxed = level >= def.max;
|
|
||||||
|
|
||||||
const isStudying = (store.currentStudyTarget?.id === id || store.currentStudyTarget?.id === tieredSkillId) && store.currentStudyTarget?.type === 'skill';
|
|
||||||
const savedProgress = store.skillProgress[tieredSkillId] || store.skillProgress[id] || 0;
|
|
||||||
|
|
||||||
const tierDef = SKILL_EVOLUTION_PATHS[id]?.tiers.find(t => t.tier === currentTier);
|
|
||||||
const skillDisplayName = tierDef?.name || def.name;
|
|
||||||
|
|
||||||
// Check prerequisites
|
|
||||||
let prereqMet = true;
|
|
||||||
if (def.req) {
|
|
||||||
for (const [r, rl] of Object.entries(def.req)) {
|
|
||||||
if ((store.skills[r] || 0) < rl) {
|
|
||||||
prereqMet = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply skill modifiers
|
|
||||||
const studyEffects = computeEffects(store.skillUpgrades || {}, store.skillTiers || {});
|
|
||||||
const effectiveSpeedMult = studySpeedMult * studyEffects.studySpeedMultiplier;
|
|
||||||
|
|
||||||
const tierStudyTime = def.studyTime * currentTier;
|
|
||||||
const effectiveStudyTime = tierStudyTime / effectiveSpeedMult;
|
|
||||||
|
|
||||||
const baseCost = def.base * (level + 1) * currentTier;
|
|
||||||
const cost = Math.floor(baseCost * studyCostMult);
|
|
||||||
|
|
||||||
// Check if any study is in progress (prevent switching topics)
|
|
||||||
const isAnyStudyInProgress = store.currentAction === 'study' && store.currentStudyTarget;
|
|
||||||
// Can only study if: not maxed, prereqs met, has mana, and either no study in progress or already studying this skill
|
|
||||||
const canStudy = !maxed && prereqMet && store.rawMana >= cost && (!isAnyStudyInProgress || isStudying);
|
|
||||||
|
|
||||||
const milestoneInfo = hasMilestoneUpgrade(tieredSkillId, level);
|
|
||||||
const nextTierSkill = getNextTierSkill(tieredSkillId);
|
|
||||||
const canTierUp = maxed && nextTierSkill;
|
|
||||||
|
|
||||||
const selectedUpgrades = store.skillUpgrades[tieredSkillId] || [];
|
|
||||||
const selectedL5 = selectedUpgrades.filter(u => u.includes('_l5'));
|
|
||||||
const selectedL10 = selectedUpgrades.filter(u => u.includes('_l10'));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={id}
|
|
||||||
className={`flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded border gap-2 ${
|
|
||||||
isStudying ? 'border-purple-500 bg-purple-900/20' :
|
|
||||||
milestoneInfo ? 'border-amber-500/50 bg-amber-900/10' :
|
|
||||||
'border-gray-700 bg-gray-800/30'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className="font-semibold text-sm">{skillDisplayName}</span>
|
|
||||||
{currentTier > 1 && (
|
|
||||||
<Badge className="bg-purple-600/50 text-purple-200 text-xs">Tier {currentTier} ({fmtDec(tierMultiplier, 0)}x)</Badge>
|
|
||||||
)}
|
|
||||||
{level > 0 && <span className="text-purple-400 text-sm">Lv.{level}</span>}
|
|
||||||
{selectedUpgrades.length > 0 && (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{selectedL5.length > 0 && (
|
|
||||||
<Badge className="bg-amber-700/50 text-amber-200 text-xs">L5: {selectedL5.length}</Badge>
|
|
||||||
)}
|
|
||||||
{selectedL10.length > 0 && (
|
|
||||||
<Badge className="bg-purple-700/50 text-purple-200 text-xs">L10: {selectedL10.length}</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 italic">{def.desc}{currentTier > 1 && ` (Tier ${currentTier}: ${fmtDec(tierMultiplier, 0)}x effect)`}</div>
|
|
||||||
{!prereqMet && def.req && (
|
|
||||||
<div className="text-xs text-red-400 mt-1">
|
|
||||||
Requires: {Object.entries(def.req).map(([r, rl]) => `${SKILLS_DEF[r]?.name} Lv.${rl}`).join(', ')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
|
||||||
<span className={effectiveSpeedMult > 1 ? 'text-green-400' : ''}>
|
|
||||||
Study: {formatStudyTime(effectiveStudyTime)}{effectiveSpeedMult > 1 && <span className="text-xs ml-1">({Math.round(effectiveSpeedMult * 100)}% speed)</span>}
|
|
||||||
</span>
|
|
||||||
{' • '}
|
|
||||||
<span className={studyCostMult < 1 ? 'text-green-400' : ''}>
|
|
||||||
Cost: {fmt(cost)} mana{studyCostMult < 1 && <span className="text-xs ml-1">({Math.round(studyCostMult * 100)}% cost)</span>}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{milestoneInfo && (
|
|
||||||
<div className="text-xs text-amber-400 mt-1 flex items-center gap-1">
|
|
||||||
⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 flex-wrap sm:flex-nowrap">
|
|
||||||
{/* Level dots */}
|
|
||||||
<div className="flex gap-1 shrink-0">
|
|
||||||
{Array.from({ length: def.max }).map((_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={`w-2 h-2 rounded-full border ${
|
|
||||||
i < level ? 'bg-purple-500 border-purple-400' :
|
|
||||||
i === 4 || i === 9 ? 'border-amber-500' :
|
|
||||||
'border-gray-600'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isStudying ? (
|
|
||||||
<div className="text-xs text-purple-400">
|
|
||||||
{formatStudyTime(store.currentStudyTarget?.progress || 0)}/{formatStudyTime(tierStudyTime)}
|
|
||||||
</div>
|
|
||||||
) : milestoneInfo ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="bg-amber-600 hover:bg-amber-700"
|
|
||||||
onClick={() => {
|
|
||||||
setUpgradeDialogSkill(tieredSkillId);
|
|
||||||
setUpgradeDialogMilestone(milestoneInfo.milestone);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Choose Upgrades
|
|
||||||
</Button>
|
|
||||||
) : canTierUp ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
|
||||||
onClick={() => store.tierUpSkill(tieredSkillId)}
|
|
||||||
>
|
|
||||||
⬆️ Tier Up
|
|
||||||
</Button>
|
|
||||||
) : maxed ? (
|
|
||||||
<Badge className="bg-green-900/50 text-green-300">Maxed</Badge>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant={canStudy ? 'default' : 'outline'}
|
|
||||||
disabled={!canStudy}
|
|
||||||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
|
||||||
onClick={() => store.startStudyingSkill(tieredSkillId)}
|
|
||||||
>
|
|
||||||
Study ({fmt(cost)})
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
{!canStudy && isAnyStudyInProgress && !isStudying && (
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Cannot switch topics while studying {SKILLS_DEF[store.currentStudyTarget?.id || '']?.name || 'another skill'}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
{/* Parallel Study button */}
|
|
||||||
{hasParallelStudy &&
|
|
||||||
store.currentStudyTarget &&
|
|
||||||
!store.parallelStudyTarget &&
|
|
||||||
store.currentStudyTarget.id !== tieredSkillId &&
|
|
||||||
canStudy && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
|
||||||
onClick={() => store.startParallelStudySkill(tieredSkillId)}
|
|
||||||
>
|
|
||||||
⚡
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Study in parallel (50% speed)</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { SKILLS_DEF, SKILL_CATEGORIES } from '@/lib/game/constants';
|
||||||
|
import { SkillRow } from './SkillRow';
|
||||||
|
|
||||||
|
interface SkillCategoryProps {
|
||||||
|
categoryId: string;
|
||||||
|
onUpgradeClick: (skillId: string, milestone: 5 | 10) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkillCategory({ categoryId, onUpgradeClick }: SkillCategoryProps) {
|
||||||
|
const cat = SKILL_CATEGORIES.find(c => c.id === categoryId);
|
||||||
|
if (!cat) return null;
|
||||||
|
|
||||||
|
const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === cat.id);
|
||||||
|
if (skillsInCat.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
||||||
|
{cat.icon} {cat.name}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{skillsInCat.map(([id, def]) => (
|
||||||
|
<SkillRow
|
||||||
|
key={id}
|
||||||
|
skillId={id}
|
||||||
|
onUpgradeClick={onUpgradeClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useGameStore, fmt, fmtDec } from '@/lib/game/store';
|
||||||
|
import { SKILLS_DEF, SKILL_CATEGORIES } from '@/lib/game/constants';
|
||||||
|
import { getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||||
|
import { computeEffects } from '@/lib/game/upgrade-effects';
|
||||||
|
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { formatStudyTime, hasMilestoneUpgrade, getSkillDisplayInfo } from './skills-utils';
|
||||||
|
|
||||||
|
interface SkillRowProps {
|
||||||
|
skillId: string;
|
||||||
|
onUpgradeClick: (skillId: string, milestone: 5 | 10) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||||
|
const store = useGameStore();
|
||||||
|
const { studySpeedMult, studyCostMult, hasParallelStudy } = useStudyStats();
|
||||||
|
|
||||||
|
const skillInfo = getSkillDisplayInfo(store, skillId);
|
||||||
|
const {
|
||||||
|
currentTier,
|
||||||
|
tieredSkillId,
|
||||||
|
tierMultiplier,
|
||||||
|
level,
|
||||||
|
maxed,
|
||||||
|
isStudying,
|
||||||
|
skillDisplayName,
|
||||||
|
prereqMet,
|
||||||
|
def
|
||||||
|
} = skillInfo;
|
||||||
|
|
||||||
|
// Apply skill modifiers
|
||||||
|
const studyEffects = computeEffects(store.skillUpgrades || {}, store.skillTiers || {});
|
||||||
|
const effectiveSpeedMult = studySpeedMult * studyEffects.studySpeedMultiplier;
|
||||||
|
|
||||||
|
const tierStudyTime = def.studyTime * currentTier;
|
||||||
|
const effectiveStudyTime = tierStudyTime / effectiveSpeedMult;
|
||||||
|
|
||||||
|
const baseCost = def.base * (level + 1) * currentTier;
|
||||||
|
const cost = Math.floor(baseCost * studyCostMult);
|
||||||
|
|
||||||
|
// Check if any study is in progress (prevent switching topics)
|
||||||
|
const isAnyStudyInProgress = store.currentAction === 'study' && store.currentStudyTarget;
|
||||||
|
// Can only study if: not maxed, prereqs met, has mana, and either no study in progress or already studying this skill
|
||||||
|
const canStudy = !maxed && prereqMet && store.rawMana >= cost && (!isAnyStudyInProgress || isStudying);
|
||||||
|
|
||||||
|
const milestoneInfo = hasMilestoneUpgrade(store, tieredSkillId, level);
|
||||||
|
const nextTierSkill = getNextTierSkill(tieredSkillId);
|
||||||
|
const canTierUp = maxed && nextTierSkill;
|
||||||
|
|
||||||
|
const selectedUpgrades = store.skillUpgrades[tieredSkillId] || [];
|
||||||
|
const selectedL5 = selectedUpgrades.filter(u => u.includes('_l5'));
|
||||||
|
const selectedL10 = selectedUpgrades.filter(u => u.includes('_l10'));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded border gap-2 ${
|
||||||
|
isStudying ? 'border-purple-500 bg-purple-900/20' :
|
||||||
|
milestoneInfo ? 'border-amber-500/50 bg-amber-900/10' :
|
||||||
|
'border-gray-700 bg-gray-800/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="font-semibold text-sm">{skillDisplayName}</span>
|
||||||
|
{currentTier > 1 && (
|
||||||
|
<Badge className="bg-purple-600/50 text-purple-200 text-xs">Tier {currentTier} ({fmtDec(tierMultiplier, 0)}x)</Badge>
|
||||||
|
)}
|
||||||
|
{level > 0 && <span className="text-purple-400 text-sm">Lv.{level}</span>}
|
||||||
|
{selectedUpgrades.length > 0 && (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{selectedL5.length > 0 && (
|
||||||
|
<Badge className="bg-amber-700/50 text-amber-200 text-xs">L5: {selectedL5.length}</Badge>
|
||||||
|
)}
|
||||||
|
{selectedL10.length > 0 && (
|
||||||
|
<Badge className="bg-purple-700/50 text-purple-200 text-xs">L10: {selectedL10.length}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 italic">{def.desc}{currentTier > 1 && ` (Tier ${currentTier}: ${fmtDec(tierMultiplier, 0)}x effect)`}</div>
|
||||||
|
{!prereqMet && def.req && (
|
||||||
|
<div className="text-xs text-red-400 mt-1">
|
||||||
|
Requires: {Object.entries(def.req).map(([r, rl]) => `${SKILLS_DEF[r]?.name} Lv.${rl}`).join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
<span className={effectiveSpeedMult > 1 ? 'text-green-400' : ''}>
|
||||||
|
Study: {formatStudyTime(effectiveStudyTime)}{effectiveSpeedMult > 1 && <span className="text-xs ml-1">({Math.round(effectiveSpeedMult * 100)}% speed)</span>}
|
||||||
|
</span>
|
||||||
|
{' • '}
|
||||||
|
<span className={studyCostMult < 1 ? 'text-green-400' : ''}>
|
||||||
|
Cost: {fmt(cost)} mana{studyCostMult < 1 && <span className="text-xs ml-1">({Math.round(studyCostMult * 100)}% cost)</span>}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{milestoneInfo && (
|
||||||
|
<div className="text-xs text-amber-400 mt-1 flex items-center gap-1">
|
||||||
|
⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 flex-wrap sm:flex-nowrap">
|
||||||
|
{/* Level dots */}
|
||||||
|
<div className="flex gap-1 shrink-0">
|
||||||
|
{Array.from({ length: def.max }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`w-2 h-2 rounded-full border ${
|
||||||
|
i < level ? 'bg-purple-500 border-purple-400' :
|
||||||
|
i === 4 || i === 9 ? 'border-amber-500' :
|
||||||
|
'border-gray-600'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isStudying ? (
|
||||||
|
<div className="text-xs text-purple-400">
|
||||||
|
{formatStudyTime(store.currentStudyTarget?.progress || 0)}/{formatStudyTime(tierStudyTime)}
|
||||||
|
</div>
|
||||||
|
) : milestoneInfo ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-amber-600 hover:bg-amber-700"
|
||||||
|
onClick={() => {
|
||||||
|
onUpgradeClick(tieredSkillId, milestoneInfo.milestone);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Choose Upgrades
|
||||||
|
</Button>
|
||||||
|
) : canTierUp ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-purple-600 hover:bg-purple-700"
|
||||||
|
onClick={() => store.tierUpSkill(tieredSkillId)}
|
||||||
|
>
|
||||||
|
⬆️ Tier Up
|
||||||
|
</Button>
|
||||||
|
) : maxed ? (
|
||||||
|
<Badge className="bg-green-900/50 text-green-300">Maxed</Badge>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={canStudy ? 'default' : 'outline'}
|
||||||
|
disabled={!canStudy}
|
||||||
|
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||||||
|
onClick={() => store.startStudyingSkill(tieredSkillId)}
|
||||||
|
>
|
||||||
|
Study ({fmt(cost)})
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{!canStudy && isAnyStudyInProgress && !isStudying && (
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Cannot switch topics while studying {SKILLS_DEF[store.currentStudyTarget?.id || '']?.name || 'another skill'}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
{/* Parallel Study button */}
|
||||||
|
{hasParallelStudy &&
|
||||||
|
store.currentStudyTarget &&
|
||||||
|
!store.parallelStudyTarget &&
|
||||||
|
store.currentStudyTarget.id !== tieredSkillId &&
|
||||||
|
canStudy && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
||||||
|
onClick={() => store.startParallelStudySkill(tieredSkillId)}
|
||||||
|
>
|
||||||
|
⚡
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Study in parallel (50% speed)</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useGameStore } from '@/lib/game/store';
|
||||||
|
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||||
|
import { formatStudyTime } from './skills-utils';
|
||||||
|
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||||
|
import { BookOpen, X } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
|
||||||
|
export function SkillStudyProgress() {
|
||||||
|
const store = useGameStore();
|
||||||
|
const { studySpeedMult } = useStudyStats();
|
||||||
|
|
||||||
|
if (!store.currentStudyTarget) return null;
|
||||||
|
|
||||||
|
const target = store.currentStudyTarget;
|
||||||
|
const progressPct = Math.min(100, (target.progress / target.required) * 100);
|
||||||
|
const def = SKILLS_DEF[target.id] || SKILLS_DEF[target.id.split('_t')[0]];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3 rounded border border-purple-600/50 bg-purple-900/20">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BookOpen className="w-4 h-4 text-purple-400" />
|
||||||
|
<span className="text-sm font-semibold text-purple-300">
|
||||||
|
{def?.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
||||||
|
onClick={() => store.cancelStudy()}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
||||||
|
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||||||
|
<span>{formatStudyTime(target.progress)} / {formatStudyTime(target.required)}</span>
|
||||||
|
<span>{studySpeedMult.toFixed(1)}x speed</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useGameStore, fmt } from '@/lib/game/store';
|
||||||
|
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||||
|
|
||||||
|
interface SkillUpgradeDialogProps {
|
||||||
|
skillId: string | null;
|
||||||
|
milestone: 5 | 10;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkillUpgradeDialog({ skillId, milestone, onClose }: SkillUpgradeDialogProps) {
|
||||||
|
const store = useGameStore();
|
||||||
|
const [pendingUpgradeSelections, setPendingUpgradeSelections] = useState<string[]>([]);
|
||||||
|
|
||||||
|
if (!skillId) return null;
|
||||||
|
|
||||||
|
const skillDef = SKILLS_DEF[skillId];
|
||||||
|
const { available, selected: alreadySelected } = store.getSkillUpgradeChoices(skillId, milestone);
|
||||||
|
|
||||||
|
const currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected;
|
||||||
|
|
||||||
|
const toggleUpgrade = (upgradeId: string) => {
|
||||||
|
if (currentSelections.includes(upgradeId)) {
|
||||||
|
setPendingUpgradeSelections(currentSelections.filter(id => id !== upgradeId));
|
||||||
|
} else if (currentSelections.length < 2) {
|
||||||
|
setPendingUpgradeSelections([...currentSelections, upgradeId]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDone = () => {
|
||||||
|
if (currentSelections.length === 2 && skillId) {
|
||||||
|
store.commitSkillUpgrades(skillId, currentSelections, milestone);
|
||||||
|
}
|
||||||
|
setPendingUpgradeSelections([]);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setPendingUpgradeSelections([]);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={!!skillId} onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setPendingUpgradeSelections([]);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-amber-400">
|
||||||
|
Choose Upgrade - {skillDef?.name || skillId}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-400">
|
||||||
|
Level {milestone} Milestone - Select 2 upgrades ({currentSelections.length}/2 chosen)
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-2 mt-4">
|
||||||
|
{available.map((upgrade) => {
|
||||||
|
const isSelected = currentSelections.includes(upgrade.id);
|
||||||
|
const canToggle = currentSelections.length < 2 || isSelected;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={upgrade.id}
|
||||||
|
className={`p-3 rounded border cursor-pointer transition-all ${
|
||||||
|
isSelected
|
||||||
|
? 'border-amber-500 bg-amber-900/30'
|
||||||
|
: canToggle
|
||||||
|
? 'border-gray-600 bg-gray-800/50 hover:border-amber-500/50 hover:bg-gray-800'
|
||||||
|
: 'border-gray-700 bg-gray-800/30 opacity-50 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (canToggle) {
|
||||||
|
toggleUpgrade(upgrade.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="font-semibold text-sm text-amber-300">{upgrade.name}</div>
|
||||||
|
{isSelected && <Badge className="bg-amber-600 text-amber-100">Selected</Badge>}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
||||||
|
{upgrade.effect.type === 'multiplier' && (
|
||||||
|
<div className="text-xs text-green-400 mt-1">
|
||||||
|
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgrade.effect.type === 'bonus' && (
|
||||||
|
<div className="text-xs text-blue-400 mt-1">
|
||||||
|
+{upgrade.effect.value} {upgrade.effect.stat}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgrade.effect.type === 'special' && (
|
||||||
|
<div className="text-xs text-cyan-400 mt-1">
|
||||||
|
⚡ {upgrade.effect.specialDesc || 'Special effect'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 mt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleDone}
|
||||||
|
disabled={currentSelections.length !== 2}
|
||||||
|
>
|
||||||
|
{currentSelections.length < 2 ? `Select ${2 - currentSelections.length} more` : 'Confirm'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||||
|
import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||||
|
import type { GameStore } from '@/lib/game/store';
|
||||||
|
|
||||||
|
// Format study time
|
||||||
|
export function formatStudyTime(hours: number): string {
|
||||||
|
if (hours < 1) return `${Math.round(hours * 60)}m`;
|
||||||
|
return `${hours.toFixed(1)}h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if skill has milestone available
|
||||||
|
export function hasMilestoneUpgrade(
|
||||||
|
store: GameStore,
|
||||||
|
skillId: string,
|
||||||
|
level: number
|
||||||
|
): { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null {
|
||||||
|
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
||||||
|
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
|
||||||
|
if (!path) return null;
|
||||||
|
|
||||||
|
if (level >= 5) {
|
||||||
|
const upgrades5 = getUpgradesForSkillAtMilestone(skillId, 5, store.skillTiers);
|
||||||
|
const selected5 = (store.skillUpgrades[skillId] || []).filter(id => id.includes('_l5'));
|
||||||
|
if (upgrades5.length > 0 && selected5.length < 2) {
|
||||||
|
return { milestone: 5, hasUpgrades: true, selectedCount: selected5.length };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level >= 10) {
|
||||||
|
const upgrades10 = getUpgradesForSkillAtMilestone(skillId, 10, store.skillTiers);
|
||||||
|
const selected10 = (store.skillUpgrades[skillId] || []).filter(id => id.includes('_l10'));
|
||||||
|
if (upgrades10.length > 0 && selected10.length < 2) {
|
||||||
|
return { milestone: 10, hasUpgrades: true, selectedCount: selected10.length };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get skill display info
|
||||||
|
export function getSkillDisplayInfo(
|
||||||
|
store: GameStore,
|
||||||
|
skillId: string
|
||||||
|
) {
|
||||||
|
const currentTier = store.skillTiers?.[skillId] || 1;
|
||||||
|
const tieredSkillId = currentTier > 1 ? `${skillId}_t${currentTier}` : skillId;
|
||||||
|
const def = SKILLS_DEF[skillId];
|
||||||
|
const tierMultiplier = getTierMultiplier(tieredSkillId);
|
||||||
|
|
||||||
|
const level = store.skills[tieredSkillId] || store.skills[skillId] || 0;
|
||||||
|
const maxed = level >= def.max;
|
||||||
|
|
||||||
|
const isStudying = (store.currentStudyTarget?.id === skillId || store.currentStudyTarget?.id === tieredSkillId) && store.currentStudyTarget?.type === 'skill';
|
||||||
|
const savedProgress = store.skillProgress[tieredSkillId] || store.skillProgress[skillId] || 0;
|
||||||
|
|
||||||
|
const tierDef = SKILL_EVOLUTION_PATHS[skillId]?.tiers.find(t => t.tier === currentTier);
|
||||||
|
const skillDisplayName = tierDef?.name || def.name;
|
||||||
|
|
||||||
|
// Check prerequisites
|
||||||
|
let prereqMet = true;
|
||||||
|
if (def.req) {
|
||||||
|
for (const [r, rl] of Object.entries(def.req)) {
|
||||||
|
if ((store.skills[r] || 0) < rl) {
|
||||||
|
prereqMet = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentTier,
|
||||||
|
tieredSkillId,
|
||||||
|
tierMultiplier,
|
||||||
|
level,
|
||||||
|
maxed,
|
||||||
|
isStudying,
|
||||||
|
savedProgress,
|
||||||
|
skillDisplayName,
|
||||||
|
prereqMet,
|
||||||
|
def,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,22 +4,20 @@ import { useGameStore, fmt, fmtDec } from '@/lib/game/store';
|
|||||||
import { ELEMENTS } from '@/lib/game/constants';
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||||
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { ManaStatsSection } from './StatsTab/ManaStatsSection';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { CombatStatsSection } from './StatsTab/CombatStatsSection';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { PactStatusSection } from './StatsTab/PactStatusSection';
|
||||||
import { Droplet, Swords, BookOpen, FlaskConical, RotateCcw, Trophy, Star } from 'lucide-react';
|
import { StudyStatsSection } from './StatsTab/StudyStatsSection';
|
||||||
|
import { ElementStatsSection } from './StatsTab/ElementStatsSection';
|
||||||
|
import { ActiveUpgradesSection } from './StatsTab/ActiveUpgradesSection';
|
||||||
|
import { LoopStatsSection } from './StatsTab/LoopStatsSection';
|
||||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||||
|
|
||||||
export function StatsTab() {
|
export function StatsTab() {
|
||||||
const store = useGameStore();
|
const store = useGameStore();
|
||||||
const {
|
const manaStats = useManaStats();
|
||||||
upgradeEffects, maxMana, baseRegen, clickMana,
|
const combatStats = useCombatStats();
|
||||||
meditationMultiplier, incursionStrength, manaCascadeBonus, manaWaterfallBonus, effectiveRegen,
|
const studyStats = useStudyStats();
|
||||||
hasSteadyStream, hasManaTorrent, hasDesperateWells,
|
|
||||||
hasManaWaterfall, hasFlowSurge, hasManaOverflow, hasEternalFlow
|
|
||||||
} = useManaStats();
|
|
||||||
const { activeSpellDef, pactMultiplier, pactInsightMultiplier } = useCombatStats();
|
|
||||||
const { studySpeedMult, studyCostMult } = useStudyStats();
|
|
||||||
|
|
||||||
// Compute element max
|
// Compute element max
|
||||||
const elemMax = (() => {
|
const elemMax = (() => {
|
||||||
@@ -31,7 +29,7 @@ export function StatsTab() {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
// Get all selected skill upgrades
|
// Get all selected skill upgrades
|
||||||
const getAllSelectedUpgrades = () => {
|
const getAllSelectedUpgrades = (): { skillId: string; upgrade: SkillUpgradeChoice }[] => {
|
||||||
const upgrades: { skillId: string; upgrade: SkillUpgradeChoice }[] = [];
|
const upgrades: { skillId: string; upgrade: SkillUpgradeChoice }[] = [];
|
||||||
for (const [skillId, selectedIds] of Object.entries(store.skillUpgrades)) {
|
for (const [skillId, selectedIds] of Object.entries(store.skillUpgrades)) {
|
||||||
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
||||||
@@ -40,7 +38,7 @@ export function StatsTab() {
|
|||||||
for (const tier of path.tiers) {
|
for (const tier of path.tiers) {
|
||||||
if (tier.skillId === skillId) {
|
if (tier.skillId === skillId) {
|
||||||
for (const upgradeId of selectedIds) {
|
for (const upgradeId of selectedIds) {
|
||||||
const upgrade = tier.upgrades.find(u => u.id === upgradeId);
|
const upgrade = (tier as any).upgrades?.find(u => u.id === upgradeId);
|
||||||
if (upgrade) {
|
if (upgrade) {
|
||||||
upgrades.push({ skillId, upgrade });
|
upgrades.push({ skillId, upgrade });
|
||||||
}
|
}
|
||||||
@@ -55,528 +53,38 @@ export function StatsTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Mana Stats */}
|
<ManaStatsSection
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
maxMana={manaStats.maxMana}
|
||||||
<CardHeader className="pb-2">
|
baseRegen={manaStats.baseRegen}
|
||||||
<CardTitle className="text-blue-400 game-panel-title text-xs flex items-center gap-2">
|
effectiveRegen={manaStats.effectiveRegen}
|
||||||
<Droplet className="w-4 h-4" />
|
clickMana={manaStats.clickMana}
|
||||||
Mana Stats
|
meditationMultiplier={manaStats.meditationMultiplier}
|
||||||
</CardTitle>
|
upgradeEffects={manaStats.upgradeEffects}
|
||||||
</CardHeader>
|
store={store}
|
||||||
<CardContent>
|
elemMax={elemMax}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
selectedUpgrades={selectedUpgrades}
|
||||||
<div className="space-y-2">
|
/>
|
||||||
<div className="flex justify-between text-sm">
|
<CombatStatsSection
|
||||||
<span className="text-gray-400">Base Max Mana:</span>
|
store={store}
|
||||||
<span className="text-gray-200">100</span>
|
activeSpellDef={combatStats.activeSpellDef}
|
||||||
</div>
|
pactMultiplier={combatStats.pactMultiplier}
|
||||||
<div className="flex justify-between text-sm">
|
/>
|
||||||
<span className="text-gray-400">Mana Well Bonus:</span>
|
<PactStatusSection
|
||||||
<span className="text-blue-300">
|
store={store}
|
||||||
{(() => {
|
pactMultiplier={combatStats.pactMultiplier}
|
||||||
const mw = store.skillTiers?.manaWell || 1;
|
pactInsightMultiplier={combatStats.pactInsightMultiplier}
|
||||||
const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell';
|
/>
|
||||||
const level = store.skills[tieredSkillId] || store.skills.manaWell || 0;
|
<StudyStatsSection
|
||||||
const tierMult = getTierMultiplier(tieredSkillId);
|
studySpeedMult={studyStats.studySpeedMult}
|
||||||
return `+${fmt(level * 100 * tierMult)} (${level} lvl × 100 × ${tierMult}x tier)`;
|
studyCostMult={studyStats.studyCostMult}
|
||||||
})()}
|
store={store}
|
||||||
</span>
|
/>
|
||||||
</div>
|
<ElementStatsSection
|
||||||
<div className="flex justify-between text-sm">
|
store={store}
|
||||||
<span className="text-gray-400">Prestige Mana Well:</span>
|
elemMax={elemMax}
|
||||||
<span className="text-blue-300">+{fmt((store.prestigeUpgrades.manaWell || 0) * 500)}</span>
|
/>
|
||||||
</div>
|
<ActiveUpgradesSection selectedUpgrades={selectedUpgrades} />
|
||||||
{upgradeEffects.maxManaBonus > 0 && (
|
<LoopStatsSection store={store} />
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-amber-400">Upgrade Mana Bonus:</span>
|
|
||||||
<span className="text-amber-300">+{fmt(upgradeEffects.maxManaBonus)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{upgradeEffects.maxManaMultiplier > 1 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-amber-400">Upgrade Mana Multiplier:</span>
|
|
||||||
<span className="text-amber-300">×{fmtDec(upgradeEffects.maxManaMultiplier, 2)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
|
||||||
<span className="text-gray-300">Total Max Mana:</span>
|
|
||||||
<span className="text-blue-400">{fmt(maxMana)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Base Regen:</span>
|
|
||||||
<span className="text-gray-200">2/hr</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Mana Flow Bonus:</span>
|
|
||||||
<span className="text-blue-300">
|
|
||||||
{(() => {
|
|
||||||
const mf = store.skillTiers?.manaFlow || 1;
|
|
||||||
const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow';
|
|
||||||
const level = store.skills[tieredSkillId] || store.skills.manaFlow || 0;
|
|
||||||
const tierMult = getTierMultiplier(tieredSkillId);
|
|
||||||
return `+${fmtDec(level * 1 * tierMult)}/hr (${level} lvl × 1 × ${tierMult}x tier)`;
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Mana Spring Bonus:</span>
|
|
||||||
<span className="text-blue-300">+{(store.skills.manaSpring || 0) * 2}/hr</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Prestige Mana Flow:</span>
|
|
||||||
<span className="text-blue-300">+{fmtDec((store.prestigeUpgrades.manaFlow || 0) * 0.5)}/hr</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Temporal Echo:</span>
|
|
||||||
<span className="text-blue-300">×{fmtDec(1 + (store.prestigeUpgrades.temporalEcho || 0) * 0.1, 2)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
|
||||||
<span className="text-gray-300">Base Regen:</span>
|
|
||||||
<span className="text-blue-400">{fmtDec(baseRegen, 2)}/hr</span>
|
|
||||||
</div>
|
|
||||||
{upgradeEffects.regenBonus > 0 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-amber-400">Upgrade Regen Bonus:</span>
|
|
||||||
<span className="text-amber-300">+{fmtDec(upgradeEffects.regenBonus, 2)}/hr</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{upgradeEffects.permanentRegenBonus > 0 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-amber-400">Permanent Regen Bonus:</span>
|
|
||||||
<span className="text-amber-300">+{fmtDec(upgradeEffects.permanentRegenBonus, 2)}/hr</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{upgradeEffects.regenMultiplier > 1 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-amber-400">Upgrade Regen Multiplier:</span>
|
|
||||||
<span className="text-amber-300">×{fmtDec(upgradeEffects.regenMultiplier, 2)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator className="bg-gray-700 my-3" />
|
|
||||||
{/* Skill Upgrade Effects Summary */}
|
|
||||||
{upgradeEffects.activeUpgrades.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className="mb-2">
|
|
||||||
<span className="text-xs text-amber-400 game-panel-title">Active Skill Upgrades</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mb-3">
|
|
||||||
{upgradeEffects.activeUpgrades.map((upgrade, idx) => (
|
|
||||||
<div key={idx} className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
|
||||||
<span className="text-gray-300">{upgrade.name}</span>
|
|
||||||
<span className="text-gray-400">{upgrade.desc}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Separator className="bg-gray-700 my-3" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Click Mana Value:</span>
|
|
||||||
<span className="text-purple-300">+{clickMana}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Mana Tap Bonus:</span>
|
|
||||||
<span className="text-purple-300">+{store.skills.manaTap || 0}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Mana Surge Bonus:</span>
|
|
||||||
<span className="text-purple-300">+{(store.skills.manaSurge || 0) * 3}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Mana Overflow:</span>
|
|
||||||
<span className="text-purple-300">×{fmtDec(1 + (store.skills.manaOverflow || 0) * 0.25, 2)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Meditation Multiplier:</span>
|
|
||||||
<span className={`font-semibold ${meditationMultiplier > 1.5 ? 'text-purple-400' : 'text-gray-300'}`}>
|
|
||||||
{fmtDec(meditationMultiplier, 2)}x
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Effective Regen:</span>
|
|
||||||
<span className="text-green-400 font-semibold">{fmtDec(effectiveRegen, 2)}/hr</span>
|
|
||||||
</div>
|
|
||||||
{incursionStrength > 0 && !hasSteadyStream && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-red-400">Incursion Penalty:</span>
|
|
||||||
<span className="text-red-400">-{Math.round(incursionStrength * 100)}%</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasSteadyStream && incursionStrength > 0 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-green-400">Steady Stream:</span>
|
|
||||||
<span className="text-green-400">Immune to incursion</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{manaCascadeBonus > 0 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-cyan-400">Mana Cascade Bonus:</span>
|
|
||||||
<span className="text-cyan-400">+{fmtDec(manaCascadeBonus, 2)}/hr</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{manaWaterfallBonus > 0 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-cyan-400">Mana Waterfall Bonus:</span>
|
|
||||||
<span className="text-cyan-400">+{fmtDec(manaWaterfallBonus, 2)}/hr</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasManaWaterfall && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-cyan-400">Mana Waterfall:</span>
|
|
||||||
<span className="text-cyan-400">+0.25 regen per 100 max mana</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasFlowSurge && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-cyan-400">Flow Surge:</span>
|
|
||||||
<span className="text-cyan-400">Clicks activate +100% regen for 1hr</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasManaOverflow && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-cyan-400">Mana Overflow:</span>
|
|
||||||
<span className="text-cyan-400">Raw mana can exceed max by 20%</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasEternalFlow && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-green-400">Eternal Flow:</span>
|
|
||||||
<span className="text-green-400">Regen immune to ALL penalties</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasManaTorrent && store.rawMana > maxMana * 0.75 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-cyan-400">Mana Torrent:</span>
|
|
||||||
<span className="text-cyan-400">+50% regen (high mana)</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasDesperateWells && store.rawMana < maxMana * 0.25 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-cyan-400">Desperate Wells:</span>
|
|
||||||
<span className="text-cyan-400">+50% regen (low mana)</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Combat Stats */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-red-400 game-panel-title text-xs flex items-center gap-2">
|
|
||||||
<Swords className="w-4 h-4" />
|
|
||||||
Combat Stats
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Active Spell Base Damage:</span>
|
|
||||||
<span className="text-gray-200">{activeSpellDef?.dmg || 5}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Combat Training Bonus:</span>
|
|
||||||
<span className="text-red-300">+{(store.skills.combatTrain || 0) * 5}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Arcane Fury Multiplier:</span>
|
|
||||||
<span className="text-red-300">×{fmtDec(1 + (store.skills.arcaneFury || 0) * 0.1, 2)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Elemental Mastery:</span>
|
|
||||||
<span className="text-red-300">×{fmtDec(1 + (store.skills.elementalMastery || 0) * 0.15, 2)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Guardian Bane:</span>
|
|
||||||
<span className="text-red-300">×{fmtDec(1 + (store.skills.guardianBane || 0) * 0.2, 2)} (vs guardians)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Critical Hit Chance:</span>
|
|
||||||
<span className="text-amber-300">{((store.skills.precision || 0) * 5)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Critical Multiplier:</span>
|
|
||||||
<span className="text-amber-300">1.5x</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Spell Echo Chance:</span>
|
|
||||||
<span className="text-amber-300">{((store.skills.spellEcho || 0) * 10)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Pact Multiplier:</span>
|
|
||||||
<span className="text-amber-300">×{fmtDec(pactMultiplier, 2)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
|
||||||
<span className="text-gray-300">Total Damage:</span>
|
|
||||||
<span className="text-red-400">{fmt(calcDamage(store, store.activeSpell))}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Pact Status */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
|
||||||
<Trophy className="w-4 h-4" />
|
|
||||||
Pact Status
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Pact Slots:</span>
|
|
||||||
<span className="text-amber-300">{store.signedPacts.length} / {1 + (store.prestigeUpgrades.pactCapacity || 0)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Damage Multiplier:</span>
|
|
||||||
<span className="text-amber-300">×{fmtDec(pactMultiplier, 2)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Insight Multiplier:</span>
|
|
||||||
<span className="text-purple-300">×{fmtDec(pactInsightMultiplier, 2)}</span>
|
|
||||||
</div>
|
|
||||||
{store.signedPacts.length > 1 && (
|
|
||||||
<>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Interference Mitigation:</span>
|
|
||||||
<span className="text-green-300">{Math.min(store.pactInterferenceMitigation || 0, 5) * 10}%</span>
|
|
||||||
</div>
|
|
||||||
{(store.pactInterferenceMitigation || 0) >= 5 && (
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-cyan-400">Synergy Bonus:</span>
|
|
||||||
<span className="text-cyan-300">+{((store.pactInterferenceMitigation || 0) - 5) * 10}%</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm text-gray-400 mb-2">Unlocked Mana Types:</div>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{Object.entries(store.elements)
|
|
||||||
.filter(([, state]) => state.unlocked)
|
|
||||||
.map(([id]) => {
|
|
||||||
const elem = ELEMENTS[id];
|
|
||||||
return (
|
|
||||||
<Badge key={id} variant="outline" className="text-xs" style={{ borderColor: elem?.color, color: elem?.color }}>
|
|
||||||
{elem?.sym} {elem?.name}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Study Stats */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
|
||||||
<BookOpen className="w-4 h-4" />
|
|
||||||
Study Stats
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Study Speed:</span>
|
|
||||||
<span className="text-purple-300">×{fmtDec(studySpeedMult, 2)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Quick Learner Bonus:</span>
|
|
||||||
<span className="text-purple-300">+{((store.skills.quickLearner || 0) * 10)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Study Cost:</span>
|
|
||||||
<span className="text-purple-300">{Math.round(studyCostMult * 100)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Focused Mind Bonus:</span>
|
|
||||||
<span className="text-purple-300">-{((store.skills.focusedMind || 0) * 5)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Progress Retention:</span>
|
|
||||||
<span className="text-purple-300">{Math.round((1 + (store.skills.knowledgeRetention || 0) * 0.2) * 100)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Element Stats */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-green-400 game-panel-title text-xs flex items-center gap-2">
|
|
||||||
<FlaskConical className="w-4 h-4" />
|
|
||||||
Element Stats
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Element Capacity:</span>
|
|
||||||
<span className="text-green-300">{elemMax}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Elem. Attunement Bonus:</span>
|
|
||||||
<span className="text-green-300">
|
|
||||||
{(() => {
|
|
||||||
const ea = store.skillTiers?.elemAttune || 1;
|
|
||||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
|
||||||
const level = store.skills[tieredSkillId] || store.skills.elemAttune || 0;
|
|
||||||
const tierMult = getTierMultiplier(tieredSkillId);
|
|
||||||
return `+${level * 50 * tierMult}`;
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Prestige Attunement:</span>
|
|
||||||
<span className="text-green-300">+{(store.prestigeUpgrades.elementalAttune || 0) * 25}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Unlocked Elements:</span>
|
|
||||||
<span className="text-green-300">{Object.values(store.elements).filter(e => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Elem. Crafting Bonus:</span>
|
|
||||||
<span className="text-green-300">×{fmtDec(1 + (store.skills.elemCrafting || 0) * 0.25, 2)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator className="bg-gray-700 my-3" />
|
|
||||||
<div className="text-xs text-gray-400 mb-2">Elemental Mana Pools:</div>
|
|
||||||
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
|
||||||
{Object.entries(store.elements)
|
|
||||||
.filter(([, state]) => state.unlocked)
|
|
||||||
.map(([id, state]) => {
|
|
||||||
const def = ELEMENTS[id];
|
|
||||||
return (
|
|
||||||
<div key={id} className="p-2 rounded border border-gray-700 bg-gray-800/50 text-center">
|
|
||||||
<div className="text-lg">{def?.sym}</div>
|
|
||||||
<div className="text-xs text-gray-400">{state.current}/{state.max}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Active Upgrades */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
|
||||||
<Star className="w-4 h-4" />
|
|
||||||
Active Skill Upgrades ({selectedUpgrades.length})
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{selectedUpgrades.length === 0 ? (
|
|
||||||
<div className="text-gray-500 text-sm">No skill upgrades selected yet. Level skills to 5 or 10 to choose upgrades.</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
||||||
{selectedUpgrades.map(({ skillId, upgrade }) => (
|
|
||||||
<div key={upgrade.id} className="p-2 rounded border border-amber-600/30 bg-amber-900/10">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-amber-300 text-sm font-semibold">{upgrade.name}</span>
|
|
||||||
<Badge variant="outline" className="text-xs text-gray-400">
|
|
||||||
{SKILLS_DEF[skillId]?.name || skillId}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
|
||||||
{upgrade.effect.type === 'multiplier' && (
|
|
||||||
<div className="text-xs text-green-400 mt-1">
|
|
||||||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{upgrade.effect.type === 'bonus' && (
|
|
||||||
<div className="text-xs text-blue-400 mt-1">
|
|
||||||
+{upgrade.effect.value} {upgrade.effect.stat}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{upgrade.effect.type === 'special' && (
|
|
||||||
<div className="text-xs text-cyan-400 mt-1">
|
|
||||||
⚡ {upgrade.effect.specialDesc || 'Special effect active'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Loop Stats */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
|
||||||
<RotateCcw className="w-4 h-4" />
|
|
||||||
Loop Stats
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
|
||||||
<div className="text-2xl font-bold text-amber-400 game-mono">{store.loopCount}</div>
|
|
||||||
<div className="text-xs text-gray-400">Loops Completed</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
|
||||||
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
|
|
||||||
<div className="text-xs text-gray-400">Current Insight</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
|
||||||
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
|
|
||||||
<div className="text-xs text-gray-400">Total Insight</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
|
||||||
<div className="text-2xl font-bold text-green-400 game-mono">{store.maxFloorReached}</div>
|
|
||||||
<div className="text-xs text-gray-400">Max Floor</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator className="bg-gray-700 my-3" />
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
|
||||||
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(store.spells).filter(s => s.learned).length}</div>
|
|
||||||
<div className="text-xs text-gray-400">Spells Learned</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
|
||||||
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(store.skills).reduce((a, b) => a + b, 0)}</div>
|
|
||||||
<div className="text-xs text-gray-400">Total Skill Levels</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
|
||||||
<div className="text-xl font-bold text-gray-300 game-mono">{fmt(store.totalManaGathered)}</div>
|
|
||||||
<div className="text-xs text-gray-400">Total Mana Gathered</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
|
||||||
<div className="text-xl font-bold text-gray-300 game-mono">{store.memorySlots}</div>
|
|
||||||
<div className="text-xs text-gray-400">Memory Slots</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Star } from 'lucide-react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||||
|
import { SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
|
||||||
|
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||||
|
|
||||||
|
interface ActiveUpgradesSectionProps {
|
||||||
|
selectedUpgrades: { skillId: string; upgrade: SkillUpgradeChoice }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActiveUpgradesSection({ selectedUpgrades }: ActiveUpgradesSectionProps) {
|
||||||
|
if (selectedUpgrades.length === 0) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||||||
|
<Star className="w-4 h-4" />
|
||||||
|
Active Skill Upgrades (0)
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-gray-500 text-sm">No skill upgrades selected yet. Level skills to 5 or 10 to choose upgrades.</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||||||
|
<Star className="w-4 h-4" />
|
||||||
|
Active Skill Upgrades ({selectedUpgrades.length})
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{selectedUpgrades.map(({ skillId, upgrade }) => (
|
||||||
|
<div key={upgrade.id} className="p-2 rounded border border-amber-600/30 bg-amber-900/10">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-amber-300 text-sm font-semibold">{upgrade.name}</span>
|
||||||
|
<Badge variant="outline" className="text-xs text-gray-400">
|
||||||
|
{SKILLS_DEF[skillId]?.name || skillId}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
||||||
|
{upgrade.effect.type === 'multiplier' && (
|
||||||
|
<div className="text-xs text-green-400 mt-1">
|
||||||
|
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgrade.effect.type === 'bonus' && (
|
||||||
|
<div className="text-xs text-blue-400 mt-1">
|
||||||
|
+{upgrade.effect.value} {upgrade.effect.stat}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgrade.effect.type === 'special' && (
|
||||||
|
<div className="text-xs text-cyan-400 mt-1">
|
||||||
|
⚡ {upgrade.effect.specialDesc || 'Special effect active'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Swords } from 'lucide-react';
|
||||||
|
import type { GameStore } from '@/lib/game/store';
|
||||||
|
import { fmt, fmtDec } from '@/lib/game/store';
|
||||||
|
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||||
|
|
||||||
|
interface CombatStatsSectionProps {
|
||||||
|
store: GameStore;
|
||||||
|
activeSpellDef: any;
|
||||||
|
pactMultiplier: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CombatStatsSection({ store, activeSpellDef, pactMultiplier }: CombatStatsSectionProps) {
|
||||||
|
const upgradeEffects = getUnifiedEffects(store);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-red-400 game-panel-title text-xs flex items-center gap-2">
|
||||||
|
<Swords className="w-4 h-4" />
|
||||||
|
Combat Stats
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Active Spell Base Damage:</span>
|
||||||
|
<span className="text-gray-200">{activeSpellDef?.dmg || 5}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Combat Training Bonus:</span>
|
||||||
|
<span className="text-red-300">+{(store.skills.combatTrain || 0) * 5}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Arcane Fury Multiplier:</span>
|
||||||
|
<span className="text-red-300">×{fmtDec(1 + (store.skills.arcaneFury || 0) * 0.1, 2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Elemental Mastery:</span>
|
||||||
|
<span className="text-red-300">×{fmtDec(1 + (store.skills.elementalMastery || 0) * 0.15, 2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Guardian Bane:</span>
|
||||||
|
<span className="text-red-300">×{fmtDec(1 + (store.skills.guardianBane || 0) * 0.2, 2)} (vs guardians)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Critical Hit Chance:</span>
|
||||||
|
<span className="text-amber-300">{((store.skills.precision || 0) * 5)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Critical Multiplier:</span>
|
||||||
|
<span className="text-amber-300">1.5x</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Spell Echo Chance:</span>
|
||||||
|
<span className="text-amber-300">{((store.skills.spellEcho || 0) * 10)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Pact Multiplier:</span>
|
||||||
|
<span className="text-amber-300">×{fmtDec(pactMultiplier, 2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||||||
|
<span className="text-gray-300">Total Damage:</span>
|
||||||
|
<span className="text-red-400">{fmt(store.activeSpell ? activeSpellDef?.dmg * pactMultiplier : 0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { FlaskConical } from 'lucide-react';
|
||||||
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
|
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||||
|
import { fmt, fmtDec } from '@/lib/game/store';
|
||||||
|
|
||||||
|
interface ElementStatsSectionProps {
|
||||||
|
store: any;
|
||||||
|
elemMax: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ElementStatsSection({ store, elemMax }: ElementStatsSectionProps) {
|
||||||
|
const getElemAttunementBonus = () => {
|
||||||
|
const ea = store.skillTiers?.elemAttune || 1;
|
||||||
|
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||||||
|
const level = store.skills[tieredSkillId] || store.skills.elemAttune || 0;
|
||||||
|
const tierMult = getTierMultiplier(tieredSkillId);
|
||||||
|
return level * 50 * tierMult;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-green-400 game-panel-title text-xs flex items-center gap-2">
|
||||||
|
<FlaskConical className="w-4 h-4" />
|
||||||
|
Element Stats
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Element Capacity:</span>
|
||||||
|
<span className="text-green-300">{elemMax}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Elem. Attunement Bonus:</span>
|
||||||
|
<span className="text-green-300">+{getElemAttunementBonus()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Prestige Attunement:</span>
|
||||||
|
<span className="text-green-300">+{(store.prestigeUpgrades.elementalAttune || 0) * 25}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Unlocked Elements:</span>
|
||||||
|
<span className="text-green-300">{Object.values(store.elements).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Elem. Crafting Bonus:</span>
|
||||||
|
<span className="text-green-300">×{fmtDec(1 + (store.skills.elemCrafting || 0) * 0.25, 2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-gray-700 my-3" />
|
||||||
|
<div className="text-xs text-gray-400 mb-2">Elemental Mana Pools:</div>
|
||||||
|
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
||||||
|
{Object.entries(store.elements)
|
||||||
|
.filter(([, state]: [string, any]) => state.unlocked)
|
||||||
|
.map(([id, state]: [string, any]) => {
|
||||||
|
const def = ELEMENTS[id];
|
||||||
|
return (
|
||||||
|
<div key={id} className="p-2 rounded border border-gray-700 bg-gray-800/50 text-center">
|
||||||
|
<div className="text-lg">{def?.sym}</div>
|
||||||
|
<div className="text-xs text-gray-400">{state.current}/{state.max}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { RotateCcw } from 'lucide-react';
|
||||||
|
import { fmt } from '@/lib/game/store';
|
||||||
|
|
||||||
|
interface LoopStatsSectionProps {
|
||||||
|
store: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoopStatsSection({ store }: LoopStatsSectionProps) {
|
||||||
|
const spellsLearned = Object.values(store.spells as Record<string, { learned: boolean }>).filter((s) => s.learned).length;
|
||||||
|
const totalSkillLevels = Object.values(store.skills as Record<string, number>).reduce((a: number, b: number) => a + b, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
Loop Stats
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||||
|
<div className="text-2xl font-bold text-amber-400 game-mono">{store.loopCount}</div>
|
||||||
|
<div className="text-xs text-gray-400">Loops Completed</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||||
|
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
|
||||||
|
<div className="text-xs text-gray-400">Current Insight</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
|
||||||
|
<div className="text-xs text-gray-400">Total Insight</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-400 game-mono">{store.maxFloorReached}</div>
|
||||||
|
<div className="text-xs text-gray-400">Max Floor</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="bg-gray-700 my-3" />
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||||
|
<div className="text-xl font-bold text-gray-300 game-mono">{spellsLearned}</div>
|
||||||
|
<div className="text-xs text-gray-400">Spells Learned</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||||
|
<div className="text-xl font-bold text-gray-300 game-mono">{totalSkillLevels}</div>
|
||||||
|
<div className="text-xs text-gray-400">Total Skill Levels</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||||
|
<div className="text-xl font-bold text-gray-300 game-mono">{fmt(store.totalManaGathered)}</div>
|
||||||
|
<div className="text-xs text-gray-400">Total Mana Gathered</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||||
|
<div className="text-xl font-bold text-gray-300 game-mono">{store.memorySlots}</div>
|
||||||
|
<div className="text-xs text-gray-400">Memory Slots</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { fmt, fmtDec } from '@/lib/game/store';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Droplet } from 'lucide-react';
|
||||||
|
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||||
|
|
||||||
|
interface ManaStatsSectionProps {
|
||||||
|
maxMana: number;
|
||||||
|
baseRegen: number;
|
||||||
|
effectiveRegen: number;
|
||||||
|
clickMana: number;
|
||||||
|
meditationMultiplier: number;
|
||||||
|
upgradeEffects: any;
|
||||||
|
store: any;
|
||||||
|
elemMax: number;
|
||||||
|
selectedUpgrades: { skillId: string; upgrade: SkillUpgradeChoice }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ManaStatsSection({
|
||||||
|
maxMana,
|
||||||
|
baseRegen,
|
||||||
|
effectiveRegen,
|
||||||
|
clickMana,
|
||||||
|
meditationMultiplier,
|
||||||
|
upgradeEffects,
|
||||||
|
store,
|
||||||
|
elemMax,
|
||||||
|
selectedUpgrades,
|
||||||
|
}: ManaStatsSectionProps) {
|
||||||
|
const getTierMultiplier = (skillId: string) => {
|
||||||
|
// Simplified - import from skill-evolution in real implementation
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-blue-400 game-panel-title text-xs flex items-center gap-2">
|
||||||
|
<Droplet className="w-4 h-4" />
|
||||||
|
Mana Stats
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Base Max Mana:</span>
|
||||||
|
<span className="text-gray-200">100</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Mana Well Bonus:</span>
|
||||||
|
<span className="text-blue-300">
|
||||||
|
{(() => {
|
||||||
|
const mw = store.skillTiers?.manaWell || 1;
|
||||||
|
const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell';
|
||||||
|
const level = store.skills[tieredSkillId] || store.skills.manaWell || 0;
|
||||||
|
const tierMult = getTierMultiplier(tieredSkillId);
|
||||||
|
return `+${fmt(level * 100 * tierMult)} (${level} lvl × 100 × ${tierMult}x tier)`;
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Prestige Mana Well:</span>
|
||||||
|
<span className="text-blue-300">+{fmt((store.prestigeUpgrades.manaWell || 0) * 500)}</span>
|
||||||
|
</div>
|
||||||
|
{upgradeEffects.maxManaBonus > 0 && (
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-amber-400">Upgrade Mana Bonus:</span>
|
||||||
|
<span className="text-amber-300">+{fmt(upgradeEffects.maxManaBonus)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.maxManaMultiplier > 1 && (
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-amber-400">Upgrade Mana Multiplier:</span>
|
||||||
|
<span className="text-amber-300">×{fmtDec(upgradeEffects.maxManaMultiplier, 2)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||||||
|
<span className="text-gray-300">Total Max Mana:</span>
|
||||||
|
<span className="text-blue-400">{fmt(maxMana)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Base Regen:</span>
|
||||||
|
<span className="text-gray-200">2/hr</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Mana Flow Bonus:</span>
|
||||||
|
<span className="text-blue-300">
|
||||||
|
{(() => {
|
||||||
|
const mf = store.skillTiers?.manaFlow || 1;
|
||||||
|
const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow';
|
||||||
|
const level = store.skills[tieredSkillId] || store.skills.manaFlow || 0;
|
||||||
|
const tierMult = getTierMultiplier(tieredSkillId);
|
||||||
|
return `+${fmtDec(level * 1 * tierMult)}/hr (${level} lvl × 1 × ${tierMult}x tier)`;
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Mana Spring Bonus:</span>
|
||||||
|
<span className="text-blue-300">+{(store.skills.manaSpring || 0) * 2}/hr</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Prestige Mana Flow:</span>
|
||||||
|
<span className="text-blue-300">+{fmtDec((store.prestigeUpgrades.manaFlow || 0) * 0.5)}/hr</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Temporal Echo:</span>
|
||||||
|
<span className="text-blue-300">×{fmtDec(1 + (store.prestigeUpgrades.temporalEcho || 0) * 0.1, 2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||||||
|
<span className="text-gray-300">Base Regen:</span>
|
||||||
|
<span className="text-blue-400">{fmtDec(baseRegen, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
{upgradeEffects.regenBonus > 0 && (
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-amber-400">Upgrade Regen Bonus:</span>
|
||||||
|
<span className="text-amber-300">+{fmtDec(upgradeEffects.regenBonus, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.permanentRegenBonus > 0 && (
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-amber-400">Permanent Regen Bonus:</span>
|
||||||
|
<span className="text-amber-300">+{fmtDec(upgradeEffects.permanentRegenBonus, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.regenMultiplier > 1 && (
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-amber-400">Upgrade Regen Multiplier:</span>
|
||||||
|
<span className="text-amber-300">×{fmtDec(upgradeEffects.regenMultiplier, 2)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Click Mana Value:</span>
|
||||||
|
<span className="text-purple-300">+{clickMana}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Mana Tap Bonus:</span>
|
||||||
|
<span className="text-purple-300">+{store.skills.manaTap || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Mana Surge Bonus:</span>
|
||||||
|
<span className="text-purple-300">+{(store.skills.manaSurge || 0) * 3}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Mana Overflow:</span>
|
||||||
|
<span className="text-purple-300">×{fmtDec(1 + (store.skills.manaOverflow || 0) * 0.25, 2)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Meditation Multiplier:</span>
|
||||||
|
<span className={`font-semibold ${meditationMultiplier > 1.5 ? 'text-purple-400' : 'text-gray-300'}`}>
|
||||||
|
{fmtDec(meditationMultiplier, 2)}x
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Incursion Strength:</span>
|
||||||
|
<span className="text-red-400">{Math.round(upgradeEffects.incursionStrength * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||||||
|
<span className="text-gray-300">Effective Regen:</span>
|
||||||
|
<span className="text-green-400 font-semibold">{fmtDec(effectiveRegen, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Special Effects */}
|
||||||
|
{(upgradeEffects.hasSteadyStream || upgradeEffects.hasManaTorrent ||
|
||||||
|
upgradeEffects.hasDesperateWells || upgradeEffects.manaCascadeBonus > 0 ||
|
||||||
|
upgradeEffects.manaWaterfallBonus > 0) && (
|
||||||
|
<>
|
||||||
|
<div className="mt-3 mb-2"><span className="text-xs text-amber-400 game-panel-title">Special Effects</span></div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{upgradeEffects.hasSteadyStream && (
|
||||||
|
<div className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||||
|
<span className="text-gray-300">Steady Stream:</span>
|
||||||
|
<span className="text-green-400">Immune to incursion</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.manaCascadeBonus > 0 && (
|
||||||
|
<div className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||||
|
<span className="text-gray-300">Mana Cascade:</span>
|
||||||
|
<span className="text-cyan-400">+{fmtDec(upgradeEffects.manaCascadeBonus, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.manaWaterfallBonus > 0 && (
|
||||||
|
<div className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||||
|
<span className="text-gray-300">Mana Waterfall:</span>
|
||||||
|
<span className="text-cyan-400">+{fmtDec(upgradeEffects.manaWaterfallBonus, 2)}/hr</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.hasFlowSurge && (
|
||||||
|
<div className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||||
|
<span className="text-gray-300">Flow Surge:</span>
|
||||||
|
<span className="text-cyan-400">Clicks +100% regen for 1hr</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.hasManaOverflow && (
|
||||||
|
<div className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||||
|
<span className="text-gray-300">Mana Overflow:</span>
|
||||||
|
<span className="text-cyan-400">Raw can exceed max by 20%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.hasEternalFlow && (
|
||||||
|
<div className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||||
|
<span className="text-gray-300">Eternal Flow:</span>
|
||||||
|
<span className="text-green-400">Regen immune to ALL penalties</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.hasManaTorrent && upgradeEffects.rawMana > maxMana * 0.75 && (
|
||||||
|
<div className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||||
|
<span className="text-gray-300">Mana Torrent:</span>
|
||||||
|
<span className="text-cyan-400">+50% regen (high mana)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{upgradeEffects.hasDesperateWells && upgradeEffects.rawMana < maxMana * 0.25 && (
|
||||||
|
<div className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||||
|
<span className="text-gray-300">Desperate Wells:</span>
|
||||||
|
<span className="text-cyan-400">+50% regen (low mana)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* Element Max */}
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-700">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Element Capacity:</span>
|
||||||
|
<span className="text-green-300">{elemMax}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Trophy } from 'lucide-react';
|
||||||
|
import { fmtDec } from '@/lib/game/store';
|
||||||
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
|
|
||||||
|
interface PactStatusSectionProps {
|
||||||
|
store: any;
|
||||||
|
pactMultiplier: number;
|
||||||
|
pactInsightMultiplier: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PactStatusSection({ store, pactMultiplier, pactInsightMultiplier }: PactStatusSectionProps) {
|
||||||
|
const pactInterferenceMitigation = store.pactInterferenceMitigation || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||||||
|
<Trophy className="w-4 h-4" />
|
||||||
|
Pact Status
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Pact Slots:</span>
|
||||||
|
<span className="text-amber-300">{store.signedPacts.length} / {1 + (store.prestigeUpgrades.pactCapacity || 0)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Damage Multiplier:</span>
|
||||||
|
<span className="text-amber-300">×{fmtDec(pactMultiplier, 2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Insight Multiplier:</span>
|
||||||
|
<span className="text-purple-300">×{fmtDec(pactInsightMultiplier, 2)}</span>
|
||||||
|
</div>
|
||||||
|
{store.signedPacts.length > 1 && (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Interference Mitigation:</span>
|
||||||
|
<span className="text-green-300">{Math.min(pactInterferenceMitigation, 5) * 10}%</span>
|
||||||
|
</div>
|
||||||
|
{pactInterferenceMitigation >= 5 && (
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-cyan-400">Synergy Bonus:</span>
|
||||||
|
<span className="text-cyan-300">+{(pactInterferenceMitigation - 5) * 10}%</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm text-gray-400 mb-2">Unlocked Mana Types:</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{Object.keys(store.elements).map((id) => {
|
||||||
|
const state = store.elements[id];
|
||||||
|
if (!state.unlocked) return null;
|
||||||
|
const elem = ELEMENTS[id];
|
||||||
|
return (
|
||||||
|
<span key={id} className="px-2 py-1 text-xs rounded border" style={{ borderColor: elem?.color, color: elem?.color }}>
|
||||||
|
{elem?.sym} {elem?.name}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { BookOpen } from 'lucide-react';
|
||||||
|
import { fmtDec } from '@/lib/game/store';
|
||||||
|
|
||||||
|
interface StudyStatsSectionProps {
|
||||||
|
studySpeedMult: number;
|
||||||
|
studyCostMult: number;
|
||||||
|
store: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StudyStatsSection({ studySpeedMult, studyCostMult, store }: StudyStatsSectionProps) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||||||
|
<BookOpen className="w-4 h-4" />
|
||||||
|
Study Stats
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Study Speed:</span>
|
||||||
|
<span className="text-purple-300">×{fmtDec(studySpeedMult, 2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Quick Learner Bonus:</span>
|
||||||
|
<span className="text-purple-300">+{((store.skills.quickLearner || 0) * 10)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Study Cost:</span>
|
||||||
|
<span className="text-purple-300">{Math.round(studyCostMult * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Focused Mind Bonus:</span>
|
||||||
|
<span className="text-purple-300">-{((store.skills.focusedMind || 0) * 5)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Progress Retention:</span>
|
||||||
|
<span className="text-purple-300">{Math.round((1 + (store.skills.knowledgeRetention || 0) * 0.2) * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,33 +1,28 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { GameCard } from '@/components/ui/game-card';
|
import { GameCard } from '@/components/ui/game-card';
|
||||||
import { SectionHeader } from '@/components/ui/section-header';
|
|
||||||
import { StatRow } from '@/components/ui/stat-row';
|
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { AlertCircle, Wand2, Scroll, Trash2, Plus, Minus, Check } from 'lucide-react';
|
|
||||||
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||||
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects';
|
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
||||||
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, EquipmentCategory } from '@/lib/game/types';
|
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress } from '@/lib/game/types';
|
||||||
import { fmt, type GameStore } from '@/lib/game/store';
|
import { type GameStore } from '@/lib/game/store';
|
||||||
|
import type { EnchantmentDesignerProps } from './EnchantmentDesigner/types';
|
||||||
export interface EnchantmentDesignerProps {
|
import { EquipmentTypeSelector } from './EnchantmentDesigner/EquipmentTypeSelector';
|
||||||
store: GameStore;
|
import { EffectSelector } from './EnchantmentDesigner/EffectSelector';
|
||||||
selectedEquipmentType: string | null;
|
import { SavedDesigns } from './EnchantmentDesigner/SavedDesigns';
|
||||||
setSelectedEquipmentType: (type: string | null) => void;
|
import { DesignForm } from './EnchantmentDesigner/DesignForm';
|
||||||
selectedEffects: DesignEffect[];
|
import {
|
||||||
setSelectedEffects: (effects: DesignEffect[]) => void;
|
getAvailableEffects,
|
||||||
designName: string;
|
getIncompatibleEffects,
|
||||||
setDesignName: (name: string) => void;
|
getOwnedEquipmentTypes,
|
||||||
selectedDesign: string | null;
|
getIncompatibilityReason,
|
||||||
setSelectedDesign: (id: string | null) => void;
|
calculateDesignCapacityCost,
|
||||||
}
|
getEquipmentCapacity,
|
||||||
|
calculateDesignTime,
|
||||||
|
addEffectToDesign,
|
||||||
|
removeEffectFromDesign,
|
||||||
|
} from './EnchantmentDesigner/utils';
|
||||||
|
|
||||||
export function EnchantmentDesigner({
|
export function EnchantmentDesigner({
|
||||||
store,
|
store,
|
||||||
@@ -52,55 +47,23 @@ export function EnchantmentDesigner({
|
|||||||
const efficiencyBonus = (skills.efficientEnchant || 0) * 0.05;
|
const efficiencyBonus = (skills.efficientEnchant || 0) * 0.05;
|
||||||
|
|
||||||
// Calculate total capacity cost for current design
|
// Calculate total capacity cost for current design
|
||||||
const designCapacityCost = selectedEffects.reduce(
|
const designCapacityCost = calculateDesignCapacityCost(selectedEffects, efficiencyBonus);
|
||||||
(total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get capacity limit for selected equipment type
|
// Get capacity limit for selected equipment type
|
||||||
const selectedEquipmentCapacity = selectedEquipmentType ? EQUIPMENT_TYPES[selectedEquipmentType]?.baseCapacity || 0 : 0;
|
const selectedEquipmentCapacity = getEquipmentCapacity(selectedEquipmentType);
|
||||||
const isOverCapacity = selectedEquipmentType ? designCapacityCost > selectedEquipmentCapacity : false;
|
const isOverCapacity = selectedEquipmentType ? designCapacityCost > selectedEquipmentCapacity : false;
|
||||||
|
|
||||||
// Calculate design time
|
// Calculate design time
|
||||||
const designTime = selectedEffects.reduce((total, eff) => total + 0.5 * eff.stacks, 1);
|
const designTime = calculateDesignTime(selectedEffects);
|
||||||
|
|
||||||
// Add effect to design
|
// Add effect to design
|
||||||
const addEffect = (effectId: string) => {
|
const addEffect = (effectId: string) => {
|
||||||
const existing = selectedEffects.find(e => e.effectId === effectId);
|
addEffectToDesign(effectId, selectedEffects, efficiencyBonus, setSelectedEffects);
|
||||||
const effectDef = ENCHANTMENT_EFFECTS[effectId];
|
|
||||||
if (!effectDef) return;
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
if (existing.stacks < effectDef.maxStacks) {
|
|
||||||
setSelectedEffects(selectedEffects.map(e =>
|
|
||||||
e.effectId === effectId
|
|
||||||
? { ...e, stacks: e.stacks + 1 }
|
|
||||||
: e
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSelectedEffects([...selectedEffects, {
|
|
||||||
effectId,
|
|
||||||
stacks: 1,
|
|
||||||
capacityCost: calculateEffectCapacityCost(effectId, 1, efficiencyBonus),
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove effect from design
|
// Remove effect from design
|
||||||
const removeEffect = (effectId: string) => {
|
const removeEffect = (effectId: string) => {
|
||||||
const existing = selectedEffects.find(e => e.effectId === effectId);
|
removeEffectFromDesign(effectId, selectedEffects, setSelectedEffects);
|
||||||
if (!existing) return;
|
|
||||||
|
|
||||||
if (existing.stacks > 1) {
|
|
||||||
setSelectedEffects(selectedEffects.map(e =>
|
|
||||||
e.effectId === effectId
|
|
||||||
? { ...e, stacks: e.stacks - 1 }
|
|
||||||
: e
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
setSelectedEffects(selectedEffects.filter(e => e.effectId !== effectId));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create design
|
// Create design
|
||||||
@@ -117,331 +80,73 @@ export function EnchantmentDesigner({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get available effects for selected equipment type (only unlocked ones)
|
// Get available effects for selected equipment type (only unlocked ones)
|
||||||
const getAvailableEffects = () => {
|
const availableEffects = getAvailableEffects(selectedEquipmentType, unlockedEffects);
|
||||||
if (!selectedEquipmentType) return [];
|
|
||||||
const type = EQUIPMENT_TYPES[selectedEquipmentType];
|
|
||||||
if (!type) return [];
|
|
||||||
|
|
||||||
return Object.values(ENCHANTMENT_EFFECTS).filter(
|
|
||||||
effect =>
|
|
||||||
effect.allowedEquipmentCategories.includes(type.category) &&
|
|
||||||
unlockedEffects.includes(effect.id)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get incompatible effects (unlocked but not for this equipment type)
|
// Get incompatible effects (unlocked but not for this equipment type)
|
||||||
// Requirement (task3 bug #7): Show incompatible enchantments in greyed-out "Unavailable" section
|
const incompatibleEffects = getIncompatibleEffects(selectedEquipmentType, unlockedEffects);
|
||||||
const getIncompatibleEffects = () => {
|
|
||||||
if (!selectedEquipmentType) return [];
|
|
||||||
const type = EQUIPMENT_TYPES[selectedEquipmentType];
|
|
||||||
if (!type) return [];
|
|
||||||
|
|
||||||
return Object.values(ENCHANTMENT_EFFECTS).filter(
|
|
||||||
effect =>
|
|
||||||
!effect.allowedEquipmentCategories.includes(type.category) &&
|
|
||||||
unlockedEffects.includes(effect.id)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get equipment types that the player actually owns (has instances of)
|
// Get equipment types that the player actually owns (has instances of)
|
||||||
// This ensures enchantment compatibility is based on owned items, not just blueprints
|
const ownedEquipmentTypes = getOwnedEquipmentTypes(store);
|
||||||
const getOwnedEquipmentTypes = () => {
|
|
||||||
// Get all unique equipment type IDs from owned instances
|
|
||||||
const ownedEquipmentTypeIds = new Set<string>();
|
|
||||||
|
|
||||||
// Check all equipment instances the player owns
|
|
||||||
for (const instance of Object.values(store.equipmentInstances)) {
|
|
||||||
ownedEquipmentTypeIds.add(instance.typeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter EQUIPMENT_TYPES to only include types the player owns
|
|
||||||
return Object.values(EQUIPMENT_TYPES).filter(type => ownedEquipmentTypeIds.has(type.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const ownedEquipmentTypes = getOwnedEquipmentTypes();
|
|
||||||
const availableEffects = getAvailableEffects();
|
|
||||||
const incompatibleEffects = getIncompatibleEffects();
|
|
||||||
|
|
||||||
// Get the reason why an effect is incompatible
|
// Get the reason why an effect is incompatible
|
||||||
const getIncompatibilityReason = (effect: typeof ENCHANTMENT_EFFECTS[string]): string => {
|
const getIncompatibilityReasonWrapper = (effect: { id: string; name: string; description: string; allowedEquipmentCategories: any[] }) => {
|
||||||
if (!selectedEquipmentType) return 'No equipment selected';
|
return getIncompatibilityReason(effect, selectedEquipmentType);
|
||||||
const type = EQUIPMENT_TYPES[selectedEquipmentType];
|
|
||||||
if (!type) return 'Unknown equipment type';
|
|
||||||
|
|
||||||
// Check what categories this effect is allowed for
|
|
||||||
const allowedCategories = effect.allowedEquipmentCategories;
|
|
||||||
const equipmentCategory = type.category;
|
|
||||||
|
|
||||||
if (allowedCategories.includes(equipmentCategory)) {
|
|
||||||
return 'Compatible';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide specific reasons
|
|
||||||
if (allowedCategories.includes('weapon' as EquipmentCategory) && equipmentCategory !== 'sword' && equipmentCategory !== 'caster' && equipmentCategory !== 'catalyst') {
|
|
||||||
return `Requires a weapon (${allowedCategories.filter(c => ['sword', 'caster', 'catalyst'].includes(c)).join(', ')})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Requires ${allowedCategories.join(' or ')} equipment`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render stage
|
// Render stage
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
{/* Equipment Type Selection */}
|
{/* Equipment Type Selection */}
|
||||||
<GameCard variant="default">
|
<EquipmentTypeSelector
|
||||||
<SectionHeader title="1. Select Equipment Type" />
|
ownedEquipmentTypes={ownedEquipmentTypes}
|
||||||
{designProgress ? (
|
selectedEquipmentType={selectedEquipmentType}
|
||||||
<div className="space-y-3">
|
setSelectedEquipmentType={setSelectedEquipmentType}
|
||||||
<div className="text-sm text-[var(--text-secondary)]">
|
designProgress={designProgress}
|
||||||
Designing for: {EQUIPMENT_TYPES[designProgress.equipmentType]?.name}
|
cancelDesign={cancelDesign}
|
||||||
</div>
|
/>
|
||||||
<div className="text-sm font-semibold text-[var(--mana-light)]">{designProgress.name}</div>
|
|
||||||
<Progress
|
|
||||||
value={(designProgress.progress / designProgress.required) * 100}
|
|
||||||
className="h-3 bg-[var(--bg-sunken)]"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between text-xs text-[var(--text-muted)]">
|
|
||||||
<span>{designProgress.progress.toFixed(1)}h / {designProgress.required.toFixed(1)}h</span>
|
|
||||||
<ActionButton size="sm" variant="outline" onClick={cancelDesign}>Cancel</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ScrollArea className="h-64">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{ownedEquipmentTypes.map(type => (
|
|
||||||
<div
|
|
||||||
key={type.id}
|
|
||||||
className={`p-2 rounded border cursor-pointer transition-all
|
|
||||||
${selectedEquipmentType === type.id
|
|
||||||
? 'border-[var(--mana-light)] bg-[var(--mana-light)]/10'
|
|
||||||
: 'border-[var(--border-default)] bg-[var(--bg-sunken)]/50 hover:border-[var(--border-default)]'
|
|
||||||
}`}
|
|
||||||
onClick={() => setSelectedEquipmentType(type.id)}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`Select ${type.name}`}
|
|
||||||
>
|
|
||||||
<div className="text-sm font-semibold text-[var(--text-primary)]">{type.name}</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)]">Cap: {type.baseCapacity}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{ownedEquipmentTypes.length === 0 && (
|
|
||||||
<div className="text-center text-[var(--text-muted)] py-4 text-sm">
|
|
||||||
No equipment blueprints owned. Craft or find equipment blueprints first.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
)}
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Effect Selection */}
|
{/* Effect Selection */}
|
||||||
<GameCard variant="default">
|
<GameCard variant="default">
|
||||||
<SectionHeader title="2. Select Effects" />
|
<EffectSelector
|
||||||
{enchantingLevel < 1 ? (
|
selectedEquipmentType={selectedEquipmentType}
|
||||||
<div className="text-center text-[var(--text-muted)] py-8">
|
selectedEffects={selectedEffects}
|
||||||
<Wand2 className="w-12 h-12 mx-auto mb-2 opacity-50 text-[var(--text-disabled)]" />
|
setSelectedEffects={setSelectedEffects}
|
||||||
<p>Learn Enchanting skill to design enchantments</p>
|
availableEffects={availableEffects}
|
||||||
</div>
|
incompatibleEffects={incompatibleEffects}
|
||||||
) : designProgress ? (
|
enchantingLevel={enchantingLevel}
|
||||||
<div className="space-y-2">
|
efficiencyBonus={efficiencyBonus}
|
||||||
<div className="text-sm text-[var(--text-secondary)]">Design in progress...</div>
|
designProgress={designProgress}
|
||||||
{designProgress.effects.map(eff => {
|
addEffect={addEffect}
|
||||||
const def = ENCHANTMENT_EFFECTS[eff.effectId];
|
removeEffect={removeEffect}
|
||||||
return (
|
getIncompatibilityReason={getIncompatibilityReasonWrapper}
|
||||||
<div key={eff.effectId} className="flex justify-between text-sm text-[var(--text-primary)]">
|
/>
|
||||||
<span>{def?.name} x{eff.stacks}</span>
|
|
||||||
<span className="text-[var(--text-muted)]">{eff.capacityCost} cap</span>
|
{/* Selected effects summary - only show when not in design progress and equipment type is selected */}
|
||||||
</div>
|
{!designProgress && selectedEquipmentType && (
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : !selectedEquipmentType ? (
|
|
||||||
<div className="text-center text-[var(--text-muted)] py-8">
|
|
||||||
Select an equipment type first
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<ScrollArea className="h-48 mb-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{/* Compatible Effects */}
|
|
||||||
{availableEffects.map(effect => {
|
|
||||||
const selected = selectedEffects.find(e => e.effectId === effect.id);
|
|
||||||
const cost = calculateEffectCapacityCost(effect.id, (selected?.stacks || 0) + 1, efficiencyBonus);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={effect.id}
|
|
||||||
className={`p-2 rounded border transition-all
|
|
||||||
${selected
|
|
||||||
? 'border-[var(--mana-stellar)] bg-[var(--mana-stellar)]/10'
|
|
||||||
: 'border-[var(--border-default)] bg-[var(--bg-sunken)]/50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-sm font-semibold text-[var(--text-primary)]">{effect.name}</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)]">{effect.description}</div>
|
|
||||||
<div className="text-xs text-[var(--text-disabled)] mt-1">
|
|
||||||
Cost: {effect.baseCapacityCost} cap | Max: {effect.maxStacks}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{selected && (
|
|
||||||
<ActionButton
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={() => removeEffect(effect.id)}
|
|
||||||
>
|
|
||||||
<Minus className="w-3 h-3" />
|
|
||||||
</ActionButton>
|
|
||||||
)}
|
|
||||||
<ActionButton
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={() => addEffect(effect.id)}
|
|
||||||
disabled={!selected && selectedEffects.length >= 5}
|
|
||||||
>
|
|
||||||
<Plus className="w-3 h-3" />
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{selected && (
|
|
||||||
<Badge variant="outline" className="mt-1 text-xs border-[var(--mana-stellar)] text-[var(--mana-stellar)]">
|
|
||||||
{selected.stacks}/{effect.maxStacks}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Incompatible Effects - Requirement: greyed-out "Unavailable" section with tooltips */}
|
|
||||||
{incompatibleEffects.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Separator className="bg-[var(--border-subtle)] my-2" />
|
|
||||||
<div className="text-xs font-semibold text-[var(--text-disabled)] uppercase tracking-wider mb-2">
|
|
||||||
Unavailable
|
|
||||||
</div>
|
|
||||||
{incompatibleEffects.map(effect => {
|
|
||||||
const reason = getIncompatibilityReason(effect);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider key={effect.id}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div
|
|
||||||
className="p-2 rounded border border-[var(--border-subtle)] bg-[var(--bg-sunken)]/30 opacity-50 cursor-not-allowed"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-sm font-semibold text-[var(--text-disabled)]">{effect.name}</div>
|
|
||||||
<div className="text-xs text-[var(--text-disabled)]">{effect.description}</div>
|
|
||||||
</div>
|
|
||||||
<AlertCircle size={14} className="text-[var(--text-disabled)]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
|
||||||
<p className="font-semibold">Incompatible Effect</p>
|
|
||||||
<p className="text-xs text-[var(--text-muted)] mt-1">{reason}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
{/* Selected effects summary */}
|
|
||||||
<Separator className="bg-[var(--border-subtle)] my-2" />
|
<Separator className="bg-[var(--border-subtle)] my-2" />
|
||||||
<div className="space-y-2">
|
<DesignForm
|
||||||
<input
|
designName={designName}
|
||||||
type="text"
|
setDesignName={setDesignName}
|
||||||
placeholder="Design name..."
|
selectedEffects={selectedEffects}
|
||||||
value={designName}
|
designCapacityCost={designCapacityCost}
|
||||||
onChange={(e) => setDesignName(e.target.value)}
|
selectedEquipmentCapacity={selectedEquipmentCapacity}
|
||||||
className="w-full bg-[var(--bg-sunken)] border border-[var(--border-default)] rounded px-3 py-2 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-disabled)] focus:outline-none focus:border-[var(--border-focus)]"
|
isOverCapacity={isOverCapacity}
|
||||||
aria-label="Design name"
|
designTime={designTime}
|
||||||
/>
|
selectedEquipmentType={selectedEquipmentType}
|
||||||
<StatRow
|
handleCreateDesign={handleCreateDesign}
|
||||||
label="Total Capacity:"
|
/>
|
||||||
value={
|
|
||||||
<span className={isOverCapacity ? 'text-[var(--color-danger)]' : 'text-[var(--color-success)]'}>
|
|
||||||
{designCapacityCost.toFixed(0)} / {selectedEquipmentCapacity}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StatRow
|
|
||||||
label="Design Time:"
|
|
||||||
value={`${designTime.toFixed(1)}h`}
|
|
||||||
highlight="default"
|
|
||||||
/>
|
|
||||||
<ActionButton
|
|
||||||
className="w-full"
|
|
||||||
disabled={!designName || selectedEffects.length === 0 || isOverCapacity}
|
|
||||||
onClick={handleCreateDesign}
|
|
||||||
>
|
|
||||||
{isOverCapacity ? 'Over Capacity!' : `Start Design (${designTime.toFixed(1)}h)`}
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</GameCard>
|
</GameCard>
|
||||||
|
|
||||||
{/* Saved Designs */}
|
{/* Saved Designs */}
|
||||||
<GameCard variant="default" className="lg:col-span-2">
|
<SavedDesigns
|
||||||
<SectionHeader title={`Saved Designs (${enchantmentDesigns.length})`} />
|
enchantmentDesigns={enchantmentDesigns}
|
||||||
{enchantmentDesigns.length === 0 ? (
|
selectedDesign={selectedDesign}
|
||||||
<div className="text-center text-[var(--text-muted)] py-4">
|
setSelectedDesign={setSelectedDesign}
|
||||||
No saved designs yet
|
deleteDesign={deleteDesign}
|
||||||
</div>
|
/>
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
||||||
{enchantmentDesigns.map(design => (
|
|
||||||
<div
|
|
||||||
key={design.id}
|
|
||||||
className={`p-3 rounded border cursor-pointer transition-all
|
|
||||||
${selectedDesign === design.id
|
|
||||||
? 'border-[var(--mana-light)] bg-[var(--mana-light)]/10'
|
|
||||||
: 'border-[var(--border-default)] bg-[var(--bg-sunken)]/50 hover:border-[var(--border-default)]'
|
|
||||||
}`}
|
|
||||||
onClick={() => setSelectedDesign(design.id)}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`Select design: ${design.name}`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start">
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold text-[var(--text-primary)]">{design.name}</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)]">
|
|
||||||
{EQUIPMENT_TYPES[design.equipmentType]?.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ActionButton
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-6 w-6 p-0 text-[var(--text-muted)] hover:text-[var(--color-danger)]"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
deleteDesign(design.id);
|
|
||||||
}}
|
|
||||||
aria-label={`Delete design: ${design.name}`}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-xs text-[var(--text-muted)]">
|
|
||||||
{design.effects.length} effects | {design.totalCapacityUsed} cap
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</GameCard>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
|
import { StatRow } from '@/components/ui/stat-row';
|
||||||
|
import type { DesignFormProps } from './types';
|
||||||
|
|
||||||
|
export function DesignForm({
|
||||||
|
designName,
|
||||||
|
setDesignName,
|
||||||
|
selectedEffects,
|
||||||
|
designCapacityCost,
|
||||||
|
selectedEquipmentCapacity,
|
||||||
|
isOverCapacity,
|
||||||
|
designTime,
|
||||||
|
selectedEquipmentType,
|
||||||
|
handleCreateDesign,
|
||||||
|
}: DesignFormProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Design name..."
|
||||||
|
value={designName}
|
||||||
|
onChange={(e) => setDesignName(e.target.value)}
|
||||||
|
className="w-full bg-[var(--bg-sunken)] border border-[var(--border-default)] rounded px-3 py-2 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-disabled)] focus:outline-none focus:border-[var(--border-focus)]"
|
||||||
|
aria-label="Design name"
|
||||||
|
/>
|
||||||
|
<StatRow
|
||||||
|
label="Total Capacity:"
|
||||||
|
value={
|
||||||
|
<span className={isOverCapacity ? 'text-[var(--color-danger)]' : 'text-[var(--color-success)]'}>
|
||||||
|
{designCapacityCost.toFixed(0)} / {selectedEquipmentCapacity}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatRow
|
||||||
|
label="Design Time:"
|
||||||
|
value={`${designTime.toFixed(1)}h`}
|
||||||
|
highlight="default"
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
className="w-full"
|
||||||
|
disabled={!designName || selectedEffects.length === 0 || isOverCapacity}
|
||||||
|
onClick={handleCreateDesign}
|
||||||
|
>
|
||||||
|
{isOverCapacity ? 'Over Capacity!' : `Start Design (${designTime.toFixed(1)}h)`}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DesignForm.displayName = 'DesignForm';
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { GameCard } from '@/components/ui/game-card';
|
||||||
|
import { SectionHeader } from '@/components/ui/section-header';
|
||||||
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { AlertCircle, Wand2, Plus, Minus } from 'lucide-react';
|
||||||
|
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects';
|
||||||
|
import type { EffectSelectorProps } from './types';
|
||||||
|
|
||||||
|
export function EffectSelector({
|
||||||
|
selectedEquipmentType,
|
||||||
|
selectedEffects,
|
||||||
|
setSelectedEffects,
|
||||||
|
availableEffects,
|
||||||
|
incompatibleEffects,
|
||||||
|
enchantingLevel,
|
||||||
|
efficiencyBonus,
|
||||||
|
designProgress,
|
||||||
|
addEffect,
|
||||||
|
removeEffect,
|
||||||
|
getIncompatibilityReason,
|
||||||
|
}: EffectSelectorProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{enchantingLevel < 1 ? (
|
||||||
|
<div className="text-center text-[var(--text-muted)] py-8">
|
||||||
|
<Wand2 className="w-12 h-12 mx-auto mb-2 opacity-50 text-[var(--text-disabled)]" />
|
||||||
|
<p>Learn Enchanting skill to design enchantments</p>
|
||||||
|
</div>
|
||||||
|
) : designProgress ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm text-[var(--text-secondary)]">Design in progress...</div>
|
||||||
|
{designProgress.effects.map(eff => {
|
||||||
|
const def = ENCHANTMENT_EFFECTS[eff.effectId];
|
||||||
|
return (
|
||||||
|
<div key={eff.effectId} className="flex justify-between text-sm text-[var(--text-primary)]">
|
||||||
|
<span>{def?.name} x{eff.stacks}</span>
|
||||||
|
<span className="text-[var(--text-muted)]">{eff.capacityCost} cap</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : !selectedEquipmentType ? (
|
||||||
|
<div className="text-center text-[var(--text-muted)] py-8">
|
||||||
|
Select an equipment type first
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ScrollArea className="h-48 mb-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Compatible Effects */}
|
||||||
|
{availableEffects.map(effect => {
|
||||||
|
const selected = selectedEffects.find(e => e.effectId === effect.id);
|
||||||
|
const cost = calculateEffectCapacityCost(effect.id, (selected?.stacks || 0) + 1, efficiencyBonus);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={effect.id}
|
||||||
|
className={`p-2 rounded border transition-all
|
||||||
|
${selected
|
||||||
|
? 'border-[var(--mana-stellar)] bg-[var(--mana-stellar)]/10'
|
||||||
|
: 'border-[var(--border-default)] bg-[var(--bg-sunken)]/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-semibold text-[var(--text-primary)]">{effect.name}</div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)]">{effect.description}</div>
|
||||||
|
<div className="text-xs text-[var(--text-disabled)] mt-1">
|
||||||
|
Cost: {effect.baseCapacityCost} cap | Max: {effect.maxStacks}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{selected && (
|
||||||
|
<ActionButton
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
onClick={() => removeEffect(effect.id)}
|
||||||
|
>
|
||||||
|
<Minus className="w-3 h-3" />
|
||||||
|
</ActionButton>
|
||||||
|
)}
|
||||||
|
<ActionButton
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
onClick={() => addEffect(effect.id)}
|
||||||
|
disabled={!selected && selectedEffects.length >= 5}
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3" />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selected && (
|
||||||
|
<Badge variant="outline" className="mt-1 text-xs border-[var(--mana-stellar)] text-[var(--mana-stellar)]">
|
||||||
|
{selected.stacks}/{effect.maxStacks}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Incompatible Effects - Requirement: greyed-out "Unavailable" section with tooltips */}
|
||||||
|
{incompatibleEffects.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Separator className="bg-[var(--border-subtle)] my-2" />
|
||||||
|
<div className="text-xs font-semibold text-[var(--text-disabled)] uppercase tracking-wider mb-2">
|
||||||
|
Unavailable
|
||||||
|
</div>
|
||||||
|
{incompatibleEffects.map(effect => {
|
||||||
|
const reason = getIncompatibilityReason(effect);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider key={effect.id}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
className="p-2 rounded border border-[var(--border-subtle)] bg-[var(--bg-sunken)]/30 opacity-50 cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-semibold text-[var(--text-disabled)]">{effect.name}</div>
|
||||||
|
<div className="text-xs text-[var(--text-disabled)]">{effect.description}</div>
|
||||||
|
</div>
|
||||||
|
<AlertCircle size={14} className="text-[var(--text-disabled)]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
||||||
|
<p className="font-semibold">Incompatible Effect</p>
|
||||||
|
<p className="text-xs text-[var(--text-muted)] mt-1">{reason}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EffectSelector.displayName = 'EffectSelector';
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { GameCard } from '@/components/ui/game-card';
|
||||||
|
import { SectionHeader } from '@/components/ui/section-header';
|
||||||
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import type { EquipmentTypeSelectorProps } from './types';
|
||||||
|
|
||||||
|
export function EquipmentTypeSelector({
|
||||||
|
ownedEquipmentTypes,
|
||||||
|
selectedEquipmentType,
|
||||||
|
setSelectedEquipmentType,
|
||||||
|
designProgress,
|
||||||
|
cancelDesign,
|
||||||
|
}: EquipmentTypeSelectorProps) {
|
||||||
|
return (
|
||||||
|
<GameCard variant="default">
|
||||||
|
<SectionHeader title="1. Select Equipment Type" />
|
||||||
|
{designProgress ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-sm text-[var(--text-secondary)]">
|
||||||
|
Designing for: {designProgress.equipmentType}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-semibold text-[var(--mana-light)]">{designProgress.name}</div>
|
||||||
|
<Progress
|
||||||
|
value={(designProgress.progress / designProgress.required) * 100}
|
||||||
|
className="h-3 bg-[var(--bg-sunken)]"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-[var(--text-muted)]">
|
||||||
|
<span>{designProgress.progress.toFixed(1)}h / {designProgress.required.toFixed(1)}h</span>
|
||||||
|
<ActionButton size="sm" variant="outline" onClick={cancelDesign}>Cancel</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="h-64">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{ownedEquipmentTypes.map(type => (
|
||||||
|
<div
|
||||||
|
key={type.id}
|
||||||
|
className={`p-2 rounded border cursor-pointer transition-all
|
||||||
|
${selectedEquipmentType === type.id
|
||||||
|
? 'border-[var(--mana-light)] bg-[var(--mana-light)]/10'
|
||||||
|
: 'border-[var(--border-default)] bg-[var(--bg-sunken)]/50 hover:border-[var(--border-default)]'
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedEquipmentType(type.id)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`Select ${type.name}`}
|
||||||
|
>
|
||||||
|
<div className="text-sm font-semibold text-[var(--text-primary)]">{type.name}</div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)]">Cap: {type.baseCapacity}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{ownedEquipmentTypes.length === 0 && (
|
||||||
|
<div className="text-center text-[var(--text-muted)] py-4 text-sm">
|
||||||
|
No equipment blueprints owned. Craft or find equipment blueprints first.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
</GameCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EquipmentTypeSelector.displayName = 'EquipmentTypeSelector';
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { GameCard } from '@/components/ui/game-card';
|
||||||
|
import { SectionHeader } from '@/components/ui/section-header';
|
||||||
|
import { ActionButton } from '@/components/ui/action-button';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||||
|
import type { SavedDesignsProps } from './types';
|
||||||
|
|
||||||
|
export function SavedDesigns({
|
||||||
|
enchantmentDesigns,
|
||||||
|
selectedDesign,
|
||||||
|
setSelectedDesign,
|
||||||
|
deleteDesign,
|
||||||
|
}: SavedDesignsProps) {
|
||||||
|
return (
|
||||||
|
<GameCard variant="default" className="lg:col-span-2">
|
||||||
|
<SectionHeader title={`Saved Designs (${enchantmentDesigns.length})`} />
|
||||||
|
{enchantmentDesigns.length === 0 ? (
|
||||||
|
<div className="text-center text-[var(--text-muted)] py-4">
|
||||||
|
No saved designs yet
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{enchantmentDesigns.map(design => (
|
||||||
|
<div
|
||||||
|
key={design.id}
|
||||||
|
className={`p-3 rounded border cursor-pointer transition-all
|
||||||
|
${selectedDesign === design.id
|
||||||
|
? 'border-[var(--mana-light)] bg-[var(--mana-light)]/10'
|
||||||
|
: 'border-[var(--border-default)] bg-[var(--bg-sunken)]/50 hover:border-[var(--border-default)]'
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedDesign(design.id)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={`Select design: ${design.name}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-[var(--text-primary)]">{design.name}</div>
|
||||||
|
<div className="text-xs text-[var(--text-muted)]">
|
||||||
|
{EQUIPMENT_TYPES[design.equipmentType]?.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ActionButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 p-0 text-[var(--text-muted)] hover:text-[var(--color-danger)]"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteDesign(design.id);
|
||||||
|
}}
|
||||||
|
aria-label={`Delete design: ${design.name}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-[var(--text-muted)]">
|
||||||
|
{design.effects.length} effects | {design.totalCapacityUsed} cap
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</GameCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SavedDesigns.displayName = 'SavedDesigns';
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, EquipmentCategory } from '@/lib/game/types';
|
||||||
|
import type { GameStore } from '@/lib/game/store';
|
||||||
|
|
||||||
|
export interface EnchantmentDesignerProps {
|
||||||
|
store: GameStore;
|
||||||
|
selectedEquipmentType: string | null;
|
||||||
|
setSelectedEquipmentType: (type: string | null) => void;
|
||||||
|
selectedEffects: DesignEffect[];
|
||||||
|
setSelectedEffects: (effects: DesignEffect[]) => void;
|
||||||
|
designName: string;
|
||||||
|
setDesignName: (name: string) => void;
|
||||||
|
selectedDesign: string | null;
|
||||||
|
setSelectedDesign: (id: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EquipmentTypeSelectorProps {
|
||||||
|
ownedEquipmentTypes: Array<{ id: string; name: string; baseCapacity: number }>;
|
||||||
|
selectedEquipmentType: string | null;
|
||||||
|
setSelectedEquipmentType: (type: string | null) => void;
|
||||||
|
designProgress: EquipmentCraftingProgress | null;
|
||||||
|
cancelDesign: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EffectSelectorProps {
|
||||||
|
selectedEquipmentType: string | null;
|
||||||
|
selectedEffects: DesignEffect[];
|
||||||
|
setSelectedEffects: (effects: DesignEffect[]) => void;
|
||||||
|
availableEffects: Array<{ id: string; name: string; description: string; baseCapacityCost: number; maxStacks: number }>;
|
||||||
|
incompatibleEffects: Array<{ id: string; name: string; description: string }>;
|
||||||
|
enchantingLevel: number;
|
||||||
|
efficiencyBonus: number;
|
||||||
|
designProgress: EquipmentCraftingProgress | null;
|
||||||
|
addEffect: (effectId: string) => void;
|
||||||
|
removeEffect: (effectId: string) => void;
|
||||||
|
getIncompatibilityReason: (effect: { id: string; name: string; description: string; allowedEquipmentCategories: EquipmentCategory[] }) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedDesignsProps {
|
||||||
|
enchantmentDesigns: EnchantmentDesign[];
|
||||||
|
selectedDesign: string | null;
|
||||||
|
setSelectedDesign: (id: string | null) => void;
|
||||||
|
deleteDesign: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DesignFormProps {
|
||||||
|
designName: string;
|
||||||
|
setDesignName: (name: string) => void;
|
||||||
|
selectedEffects: DesignEffect[];
|
||||||
|
designCapacityCost: number;
|
||||||
|
selectedEquipmentCapacity: number;
|
||||||
|
isOverCapacity: boolean;
|
||||||
|
designTime: number;
|
||||||
|
selectedEquipmentType: string | null;
|
||||||
|
handleCreateDesign: () => void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||||
|
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects';
|
||||||
|
import type { DesignEffect, EquipmentCategory } from '@/lib/game/types';
|
||||||
|
import type { GameStore } from '@/lib/game/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available effects for selected equipment type (only unlocked ones)
|
||||||
|
* Requirement (task3 bug #7): Show incompatible enchantments in greyed-out "Unavailable" section
|
||||||
|
*/
|
||||||
|
export function getAvailableEffects(
|
||||||
|
selectedEquipmentType: string | null,
|
||||||
|
unlockedEffects: string[]
|
||||||
|
) {
|
||||||
|
if (!selectedEquipmentType) return [];
|
||||||
|
const type = EQUIPMENT_TYPES[selectedEquipmentType];
|
||||||
|
if (!type) return [];
|
||||||
|
|
||||||
|
return Object.values(ENCHANTMENT_EFFECTS).filter(
|
||||||
|
effect =>
|
||||||
|
effect.allowedEquipmentCategories.includes(type.category) &&
|
||||||
|
unlockedEffects.includes(effect.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get incompatible effects (unlocked but not for this equipment type)
|
||||||
|
*/
|
||||||
|
export function getIncompatibleEffects(
|
||||||
|
selectedEquipmentType: string | null,
|
||||||
|
unlockedEffects: string[]
|
||||||
|
) {
|
||||||
|
if (!selectedEquipmentType) return [];
|
||||||
|
const type = EQUIPMENT_TYPES[selectedEquipmentType];
|
||||||
|
if (!type) return [];
|
||||||
|
|
||||||
|
return Object.values(ENCHANTMENT_EFFECTS).filter(
|
||||||
|
effect =>
|
||||||
|
!effect.allowedEquipmentCategories.includes(type.category) &&
|
||||||
|
unlockedEffects.includes(effect.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get equipment types that the player actually owns (has instances of)
|
||||||
|
* This ensures enchantment compatibility is based on owned items, not just blueprints
|
||||||
|
*/
|
||||||
|
export function getOwnedEquipmentTypes(store: GameStore) {
|
||||||
|
// Get all unique equipment type IDs from owned instances
|
||||||
|
const ownedEquipmentTypeIds = new Set<string>();
|
||||||
|
|
||||||
|
// Check all equipment instances the player owns
|
||||||
|
for (const instance of Object.values(store.equipmentInstances)) {
|
||||||
|
ownedEquipmentTypeIds.add(instance.typeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter EQUIPMENT_TYPES to only include types the player owns
|
||||||
|
return Object.values(EQUIPMENT_TYPES).filter(type => ownedEquipmentTypeIds.has(type.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the reason why an effect is incompatible
|
||||||
|
*/
|
||||||
|
export function getIncompatibilityReason(
|
||||||
|
effect: { id: string; name: string; description: string; allowedEquipmentCategories: EquipmentCategory[] },
|
||||||
|
selectedEquipmentType: string | null
|
||||||
|
): string {
|
||||||
|
if (!selectedEquipmentType) return 'No equipment selected';
|
||||||
|
const type = EQUIPMENT_TYPES[selectedEquipmentType];
|
||||||
|
if (!type) return 'Unknown equipment type';
|
||||||
|
|
||||||
|
// Check what categories this effect is allowed for
|
||||||
|
const allowedCategories = effect.allowedEquipmentCategories;
|
||||||
|
const equipmentCategory = type.category;
|
||||||
|
|
||||||
|
if (allowedCategories.includes(equipmentCategory)) {
|
||||||
|
return 'Compatible';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide specific reasons
|
||||||
|
if (allowedCategories.includes('weapon' as EquipmentCategory) && equipmentCategory !== 'sword' && equipmentCategory !== 'caster' && equipmentCategory !== 'catalyst') {
|
||||||
|
return `Requires a weapon (${allowedCategories.filter(c => ['sword', 'caster', 'catalyst'].includes(c)).join(', ')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Requires ${allowedCategories.join(' or ')} equipment`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate total capacity cost for current design
|
||||||
|
*/
|
||||||
|
export function calculateDesignCapacityCost(
|
||||||
|
selectedEffects: DesignEffect[],
|
||||||
|
efficiencyBonus: number
|
||||||
|
): number {
|
||||||
|
return selectedEffects.reduce(
|
||||||
|
(total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get capacity limit for selected equipment type
|
||||||
|
*/
|
||||||
|
export function getEquipmentCapacity(selectedEquipmentType: string | null): number {
|
||||||
|
return selectedEquipmentType ? EQUIPMENT_TYPES[selectedEquipmentType]?.baseCapacity || 0 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate design time
|
||||||
|
*/
|
||||||
|
export function calculateDesignTime(selectedEffects: DesignEffect[]): number {
|
||||||
|
return selectedEffects.reduce((total, eff) => total + 0.5 * eff.stacks, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add effect to design
|
||||||
|
*/
|
||||||
|
export function addEffectToDesign(
|
||||||
|
effectId: string,
|
||||||
|
selectedEffects: DesignEffect[],
|
||||||
|
efficiencyBonus: number,
|
||||||
|
setSelectedEffects: (effects: DesignEffect[]) => void
|
||||||
|
) {
|
||||||
|
const existing = selectedEffects.find(e => e.effectId === effectId);
|
||||||
|
const effectDef = ENCHANTMENT_EFFECTS[effectId];
|
||||||
|
if (!effectDef) return;
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.stacks < effectDef.maxStacks) {
|
||||||
|
setSelectedEffects(selectedEffects.map(e =>
|
||||||
|
e.effectId === effectId
|
||||||
|
? { ...e, stacks: e.stacks + 1 }
|
||||||
|
: e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSelectedEffects([...selectedEffects, {
|
||||||
|
effectId,
|
||||||
|
stacks: 1,
|
||||||
|
capacityCost: calculateEffectCapacityCost(effectId, 1, efficiencyBonus),
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove effect from design
|
||||||
|
*/
|
||||||
|
export function removeEffectFromDesign(
|
||||||
|
effectId: string,
|
||||||
|
selectedEffects: DesignEffect[],
|
||||||
|
setSelectedEffects: (effects: DesignEffect[]) => void
|
||||||
|
) {
|
||||||
|
const existing = selectedEffects.find(e => e.effectId === effectId);
|
||||||
|
if (!existing) return;
|
||||||
|
|
||||||
|
if (existing.stacks > 1) {
|
||||||
|
setSelectedEffects(selectedEffects.map(e =>
|
||||||
|
e.effectId === effectId
|
||||||
|
? { ...e, stacks: e.stacks - 1 }
|
||||||
|
: e
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
setSelectedEffects(selectedEffects.filter(e => e.effectId !== effectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useGameStore, useGameLoop } from '@/lib/game/store';
|
import { useGameStore } from '@/lib/game/store';
|
||||||
|
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||||
import {
|
import {
|
||||||
ELEMENTS,
|
ELEMENTS,
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Ascension Skills Tests
|
||||||
|
*
|
||||||
|
* Tests for ascension-related skills: Insight Harvest, Guardian Bane
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { SKILLS_DEF } from '../constants';
|
||||||
|
import { calcInsight } from '../computed-stats';
|
||||||
|
import type { GameState } from '../types';
|
||||||
|
|
||||||
|
function createMockState(overrides: Partial<GameState> = {}): GameState {
|
||||||
|
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
||||||
|
const baseElements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death', 'transference', 'metal', 'sand', 'crystal', 'stellar', 'void', 'lightning'];
|
||||||
|
baseElements.forEach((k) => {
|
||||||
|
elements[k] = { current: 0, max: 10, unlocked: baseElements.slice(0, 4).includes(k) };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
day: 1,
|
||||||
|
hour: 0,
|
||||||
|
loopCount: 0,
|
||||||
|
gameOver: false,
|
||||||
|
victory: false,
|
||||||
|
paused: false,
|
||||||
|
rawMana: 100,
|
||||||
|
meditateTicks: 0,
|
||||||
|
totalManaGathered: 0,
|
||||||
|
elements,
|
||||||
|
currentFloor: 1,
|
||||||
|
floorHP: 100,
|
||||||
|
floorMaxHP: 100,
|
||||||
|
castProgress: 0,
|
||||||
|
currentRoom: {
|
||||||
|
roomType: 'combat',
|
||||||
|
enemies: [{ id: 'enemy', hp: 100, maxHP: 100, armor: 0, dodgeChance: 0, element: 'fire' }],
|
||||||
|
},
|
||||||
|
maxFloorReached: 1,
|
||||||
|
signedPacts: [],
|
||||||
|
activeSpell: 'manaBolt',
|
||||||
|
currentAction: 'meditate',
|
||||||
|
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
||||||
|
skills: {},
|
||||||
|
skillProgress: {},
|
||||||
|
skillUpgrades: {},
|
||||||
|
skillTiers: {},
|
||||||
|
parallelStudyTarget: null,
|
||||||
|
equippedInstances: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory1: null, accessory2: null },
|
||||||
|
equipmentInstances: {},
|
||||||
|
enchantmentDesigns: [],
|
||||||
|
designProgress: null,
|
||||||
|
preparationProgress: null,
|
||||||
|
applicationProgress: null,
|
||||||
|
equipmentCraftingProgress: null,
|
||||||
|
unlockedEffects: [],
|
||||||
|
equipmentSpellStates: [],
|
||||||
|
equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null },
|
||||||
|
inventory: [],
|
||||||
|
blueprints: {},
|
||||||
|
lootInventory: { materials: {}, blueprints: [] },
|
||||||
|
schedule: [],
|
||||||
|
autoSchedule: false,
|
||||||
|
studyQueue: [],
|
||||||
|
craftQueue: [],
|
||||||
|
currentStudyTarget: null,
|
||||||
|
achievements: { unlocked: [], progress: {} },
|
||||||
|
totalSpellsCast: 0,
|
||||||
|
totalDamageDealt: 0,
|
||||||
|
totalCraftsCompleted: 0,
|
||||||
|
attunements: {
|
||||||
|
enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 },
|
||||||
|
invoker: { id: 'invoker', active: false, level: 1, experience: 0 },
|
||||||
|
fabricator: { id: 'fabricator', active: false, level: 1, experience: 0 },
|
||||||
|
},
|
||||||
|
golemancy: {
|
||||||
|
enabledGolems: [],
|
||||||
|
summonedGolems: [],
|
||||||
|
lastSummonFloor: 0,
|
||||||
|
},
|
||||||
|
insight: 0,
|
||||||
|
totalInsight: 0,
|
||||||
|
prestigeUpgrades: {},
|
||||||
|
memorySlots: 3,
|
||||||
|
memories: [],
|
||||||
|
incursionStrength: 0,
|
||||||
|
containmentWards: 0,
|
||||||
|
log: [],
|
||||||
|
loopInsight: 0,
|
||||||
|
...overrides,
|
||||||
|
} as GameState;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Ascension Skills', () => {
|
||||||
|
describe('Insight Harvest (+10% insight gain)', () => {
|
||||||
|
it('should multiply insight gain by 10% per level', () => {
|
||||||
|
const state0 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 0 } });
|
||||||
|
const state1 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 1 } });
|
||||||
|
const state5 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 5 } });
|
||||||
|
|
||||||
|
const insight0 = calcInsight(state0);
|
||||||
|
const insight1 = calcInsight(state1);
|
||||||
|
const insight5 = calcInsight(state5);
|
||||||
|
|
||||||
|
expect(insight1).toBeGreaterThan(insight0);
|
||||||
|
expect(insight5).toBeGreaterThan(insight1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.insightHarvest.desc).toBe("+10% insight gain");
|
||||||
|
expect(SKILLS_DEF.insightHarvest.max).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Guardian Bane (+20% dmg vs guardians)', () => {
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.guardianBane.desc).toBe("+20% dmg vs guardians");
|
||||||
|
expect(SKILLS_DEF.guardianBane.max).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Ascension skills tests defined.');
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* Skill Integration Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { SKILLS_DEF, SKILL_EVOLUTION_PATHS, getTierMultiplier, getNextTierSkill, generateTierSkillDef } from '../constants';
|
||||||
|
import { SKILL_EVOLUTION_PATHS as EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill as NextTier, getTierMultiplier as TierMultiplier, generateTierSkillDef as GenerateTier } from '../skill-evolution';
|
||||||
|
|
||||||
|
describe('Integration Tests', () => {
|
||||||
|
it('skill costs should scale with level', () => {
|
||||||
|
const skill = SKILLS_DEF.manaWell;
|
||||||
|
for (let level = 0; level < skill.max; level++) {
|
||||||
|
const cost = skill.base * (level + 1);
|
||||||
|
expect(cost).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all skills should have valid categories', () => {
|
||||||
|
const validCategories = ['mana', 'study', 'ascension', 'enchant', 'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'research', 'craft', 'hybrid'];
|
||||||
|
Object.values(SKILLS_DEF).forEach(skill => {
|
||||||
|
expect(validCategories).toContain(skill.cat);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all prerequisite skills should exist', () => {
|
||||||
|
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
|
||||||
|
if (skill.req) {
|
||||||
|
Object.keys(skill.req).forEach(reqId => {
|
||||||
|
expect(SKILLS_DEF[reqId]).toBeDefined();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all prerequisite levels should be within skill max', () => {
|
||||||
|
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
|
||||||
|
if (skill.req) {
|
||||||
|
Object.entries(skill.req).forEach(([reqId, reqLevel]) => {
|
||||||
|
expect(reqLevel).toBeLessThanOrEqual(SKILLS_DEF[reqId].max);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all attunement-requiring skills should have valid attunement', () => {
|
||||||
|
const validAttunements = ['enchanter', 'invoker', 'fabricator'];
|
||||||
|
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
|
||||||
|
if (skill.attunement) {
|
||||||
|
expect(validAttunements).toContain(skill.attunement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Skill Evolution Tests ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Skill Evolution', () => {
|
||||||
|
it('skills with max > 1 should have evolution paths', () => {
|
||||||
|
const skillsWithMaxGt1 = Object.entries(SKILLS_DEF)
|
||||||
|
.filter(([_, def]) => def.max > 1)
|
||||||
|
.map(([id]) => id);
|
||||||
|
|
||||||
|
for (const skillId of skillsWithMaxGt1) {
|
||||||
|
expect(SKILL_EVOLUTION_PATHS[skillId], `Missing evolution path for ${skillId}`).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tier multiplier should be 10^(tier-1)', () => {
|
||||||
|
expect(getTierMultiplier('manaWell')).toBe(1);
|
||||||
|
expect(getTierMultiplier('manaWell_t2')).toBe(10);
|
||||||
|
expect(getTierMultiplier('manaWell_t3')).toBe(100);
|
||||||
|
expect(getTierMultiplier('manaWell_t4')).toBe(1000);
|
||||||
|
expect(getTierMultiplier('manaWell_t5')).toBe(10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getNextTierSkill should return correct next tier', () => {
|
||||||
|
expect(getNextTierSkill('manaWell')).toBe('manaWell_t2');
|
||||||
|
expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3');
|
||||||
|
expect(getNextTierSkill('manaWell_t5')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generateTierSkillDef should return valid definitions', () => {
|
||||||
|
const tier1 = generateTierSkillDef('manaWell', 1);
|
||||||
|
expect(tier1).not.toBeNull();
|
||||||
|
expect(tier1?.name).toBe('Mana Well');
|
||||||
|
expect(tier1?.multiplier).toBe(1);
|
||||||
|
|
||||||
|
const tier2 = generateTierSkillDef('manaWell', 2);
|
||||||
|
expect(tier2).not.toBeNull();
|
||||||
|
expect(tier2?.name).toBe('Deep Reservoir');
|
||||||
|
expect(tier2?.multiplier).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Integration and skill evolution tests defined.');
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
/**
|
||||||
|
* Mana Skills Tests
|
||||||
|
*
|
||||||
|
* Tests for mana-related skills: Mana Well, Mana Flow, Mana Spring,
|
||||||
|
* Elemental Attunement, Mana Overflow, Mana Tap, Mana Surge
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
computeMaxMana,
|
||||||
|
computeElementMax,
|
||||||
|
computeRegen,
|
||||||
|
computeClickMana,
|
||||||
|
} from '../computed-stats';
|
||||||
|
import { SKILLS_DEF } from '../constants';
|
||||||
|
import type { GameState } from '../types';
|
||||||
|
|
||||||
|
// ─── Test Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function createMockState(overrides: Partial<GameState> = {}): GameState {
|
||||||
|
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
||||||
|
const baseElements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death', 'transference', 'metal', 'sand', 'crystal', 'stellar', 'void', 'lightning'];
|
||||||
|
baseElements.forEach((k) => {
|
||||||
|
elements[k] = { current: 0, max: 10, unlocked: baseElements.slice(0, 4).includes(k) };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
day: 1,
|
||||||
|
hour: 0,
|
||||||
|
loopCount: 0,
|
||||||
|
gameOver: false,
|
||||||
|
victory: false,
|
||||||
|
paused: false,
|
||||||
|
rawMana: 100,
|
||||||
|
meditateTicks: 0,
|
||||||
|
totalManaGathered: 0,
|
||||||
|
elements,
|
||||||
|
currentFloor: 1,
|
||||||
|
floorHP: 100,
|
||||||
|
floorMaxHP: 100,
|
||||||
|
castProgress: 0,
|
||||||
|
currentRoom: {
|
||||||
|
roomType: 'combat',
|
||||||
|
enemies: [{ id: 'enemy', hp: 100, maxHP: 100, armor: 0, dodgeChance: 0, element: 'fire' }],
|
||||||
|
},
|
||||||
|
maxFloorReached: 1,
|
||||||
|
signedPacts: [],
|
||||||
|
activeSpell: 'manaBolt',
|
||||||
|
currentAction: 'meditate',
|
||||||
|
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
||||||
|
skills: {},
|
||||||
|
skillProgress: {},
|
||||||
|
skillUpgrades: {},
|
||||||
|
skillTiers: {},
|
||||||
|
parallelStudyTarget: null,
|
||||||
|
equippedInstances: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory1: null, accessory2: null },
|
||||||
|
equipmentInstances: {},
|
||||||
|
enchantmentDesigns: [],
|
||||||
|
designProgress: null,
|
||||||
|
preparationProgress: null,
|
||||||
|
applicationProgress: null,
|
||||||
|
equipmentCraftingProgress: null,
|
||||||
|
unlockedEffects: [],
|
||||||
|
equipmentSpellStates: [],
|
||||||
|
equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null },
|
||||||
|
inventory: [],
|
||||||
|
blueprints: {},
|
||||||
|
lootInventory: { materials: {}, blueprints: [] },
|
||||||
|
schedule: [],
|
||||||
|
autoSchedule: false,
|
||||||
|
studyQueue: [],
|
||||||
|
craftQueue: [],
|
||||||
|
currentStudyTarget: null,
|
||||||
|
achievements: { unlocked: [], progress: {} },
|
||||||
|
totalSpellsCast: 0,
|
||||||
|
totalDamageDealt: 0,
|
||||||
|
totalCraftsCompleted: 0,
|
||||||
|
attunements: {
|
||||||
|
enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 },
|
||||||
|
invoker: { id: 'invoker', active: false, level: 1, experience: 0 },
|
||||||
|
fabricator: { id: 'fabricator', active: false, level: 1, experience: 0 },
|
||||||
|
},
|
||||||
|
golemancy: {
|
||||||
|
enabledGolems: [],
|
||||||
|
summonedGolems: [],
|
||||||
|
lastSummonFloor: 0,
|
||||||
|
},
|
||||||
|
insight: 0,
|
||||||
|
totalInsight: 0,
|
||||||
|
prestigeUpgrades: {},
|
||||||
|
memorySlots: 3,
|
||||||
|
memories: [],
|
||||||
|
incursionStrength: 0,
|
||||||
|
containmentWards: 0,
|
||||||
|
log: [],
|
||||||
|
loopInsight: 0,
|
||||||
|
...overrides,
|
||||||
|
} as GameState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Mana Skills Tests ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Mana Skills', () => {
|
||||||
|
describe('Mana Well (+100 max mana)', () => {
|
||||||
|
it('should add 100 max mana per level', () => {
|
||||||
|
const state0 = createMockState({ skills: { manaWell: 0 } });
|
||||||
|
const state1 = createMockState({ skills: { manaWell: 1 } });
|
||||||
|
const state5 = createMockState({ skills: { manaWell: 5 } });
|
||||||
|
const state10 = createMockState({ skills: { manaWell: 10 } });
|
||||||
|
|
||||||
|
expect(computeMaxMana(state0, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100);
|
||||||
|
expect(computeMaxMana(state1, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 100);
|
||||||
|
expect(computeMaxMana(state5, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 500);
|
||||||
|
expect(computeMaxMana(state10, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.manaWell.desc).toBe("+100 max mana");
|
||||||
|
expect(SKILLS_DEF.manaWell.max).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have upgrade tree', () => {
|
||||||
|
expect(SKILLS_DEF.manaWell).toBeDefined();
|
||||||
|
expect(SKILLS_DEF.manaWell.max).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mana Flow (+1 regen/hr)', () => {
|
||||||
|
it('should add 1 regen per hour per level', () => {
|
||||||
|
const state0 = createMockState({ skills: { manaFlow: 0 } });
|
||||||
|
const state1 = createMockState({ skills: { manaFlow: 1 } });
|
||||||
|
const state5 = createMockState({ skills: { manaFlow: 5 } });
|
||||||
|
|
||||||
|
const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 };
|
||||||
|
expect(computeRegen(state0, effects)).toBe(2);
|
||||||
|
expect(computeRegen(state1, effects)).toBe(2 + 1);
|
||||||
|
expect(computeRegen(state5, effects)).toBe(2 + 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.manaFlow.desc).toBe("+1 regen/hr");
|
||||||
|
expect(SKILLS_DEF.manaFlow.max).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mana Spring (+2 mana regen)', () => {
|
||||||
|
it('should add 2 mana regen', () => {
|
||||||
|
const state0 = createMockState({ skills: { manaSpring: 0 } });
|
||||||
|
const state1 = createMockState({ skills: { manaSpring: 1 } });
|
||||||
|
|
||||||
|
const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 };
|
||||||
|
expect(computeRegen(state0, effects)).toBe(2);
|
||||||
|
expect(computeRegen(state1, effects)).toBe(2 + 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.manaSpring.desc).toBe("+2 mana regen");
|
||||||
|
expect(SKILLS_DEF.manaSpring.max).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Elemental Attunement (+50 elem mana cap)', () => {
|
||||||
|
it('should add 50 element mana capacity per level', () => {
|
||||||
|
const state0 = createMockState({ skills: { elemAttune: 0 } });
|
||||||
|
const state1 = createMockState({ skills: { elemAttune: 1 } });
|
||||||
|
const state5 = createMockState({ skills: { elemAttune: 5 } });
|
||||||
|
|
||||||
|
expect(computeElementMax(state0, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10);
|
||||||
|
expect(computeElementMax(state1, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 50);
|
||||||
|
expect(computeElementMax(state5, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.elemAttune.desc).toBe("+50 elem mana cap");
|
||||||
|
expect(SKILLS_DEF.elemAttune.max).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mana Overflow (+25% mana from clicks)', () => {
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.manaOverflow.desc).toBe("+25% mana from clicks");
|
||||||
|
expect(SKILLS_DEF.manaOverflow.max).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require Mana Well 3', () => {
|
||||||
|
expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mana Tap (+1 mana/click)', () => {
|
||||||
|
it('should add 1 mana per click', () => {
|
||||||
|
const state0 = createMockState({ skills: { manaTap: 0 } });
|
||||||
|
const state1 = createMockState({ skills: { manaTap: 1 } });
|
||||||
|
|
||||||
|
expect(computeClickMana(state0, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(1);
|
||||||
|
expect(computeClickMana(state1, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.manaTap.desc).toBe("+1 mana/click");
|
||||||
|
expect(SKILLS_DEF.manaTap.max).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Mana Surge (+3 mana/click)', () => {
|
||||||
|
it('should add 3 mana per click', () => {
|
||||||
|
const state1 = createMockState({ skills: { manaSurge: 1 } });
|
||||||
|
expect(computeClickMana(state1, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(1 + 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stack with Mana Tap', () => {
|
||||||
|
const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } });
|
||||||
|
expect(computeClickMana(state, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(1 + 1 + 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require Mana Tap 1', () => {
|
||||||
|
expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Mana skills tests defined.');
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Prestige Upgrade Tests for Skills
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { PRESTIGE_DEF } from '../constants';
|
||||||
|
import { computeMaxMana, computeElementMax } from '../computed-stats';
|
||||||
|
import type { GameState } from '../types';
|
||||||
|
|
||||||
|
function createMockState(overrides: Partial<GameState> = {}): GameState {
|
||||||
|
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
||||||
|
const baseElements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death', 'transference', 'metal', 'sand', 'crystal', 'stellar', 'void', 'lightning'];
|
||||||
|
baseElements.forEach((k) => {
|
||||||
|
elements[k] = { current: 0, max: 10, unlocked: baseElements.slice(0, 4).includes(k) };
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
day: 1,
|
||||||
|
hour: 0,
|
||||||
|
loopCount: 0,
|
||||||
|
gameOver: false,
|
||||||
|
victory: false,
|
||||||
|
paused: false,
|
||||||
|
rawMana: 100,
|
||||||
|
meditateTicks: 0,
|
||||||
|
totalManaGathered: 0,
|
||||||
|
elements,
|
||||||
|
currentFloor: 1,
|
||||||
|
floorHP: 100,
|
||||||
|
floorMaxHP: 100,
|
||||||
|
castProgress: 0,
|
||||||
|
currentRoom: {
|
||||||
|
roomType: 'combat',
|
||||||
|
enemies: [{ id: 'enemy', hp: 100, maxHP: 100, armor: 0, dodgeChance: 0, element: 'fire' }],
|
||||||
|
},
|
||||||
|
maxFloorReached: 1,
|
||||||
|
signedPacts: [],
|
||||||
|
activeSpell: 'manaBolt',
|
||||||
|
currentAction: 'meditate',
|
||||||
|
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
||||||
|
skills: {},
|
||||||
|
skillProgress: {},
|
||||||
|
skillUpgrades: {},
|
||||||
|
skillTiers: {},
|
||||||
|
parallelStudyTarget: null,
|
||||||
|
equippedInstances: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory1: null, accessory2: null },
|
||||||
|
equipmentInstances: {},
|
||||||
|
enchantmentDesigns: [],
|
||||||
|
designProgress: null,
|
||||||
|
preparationProgress: null,
|
||||||
|
applicationProgress: null,
|
||||||
|
equipmentCraftingProgress: null,
|
||||||
|
unlockedEffects: [],
|
||||||
|
equipmentSpellStates: [],
|
||||||
|
equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null },
|
||||||
|
inventory: [],
|
||||||
|
blueprints: {},
|
||||||
|
lootInventory: { materials: {}, blueprints: [] },
|
||||||
|
schedule: [],
|
||||||
|
autoSchedule: false,
|
||||||
|
studyQueue: [],
|
||||||
|
craftQueue: [],
|
||||||
|
currentStudyTarget: null,
|
||||||
|
achievements: { unlocked: [], progress: {} },
|
||||||
|
totalSpellsCast: 0,
|
||||||
|
totalDamageDealt: 0,
|
||||||
|
totalCraftsCompleted: 0,
|
||||||
|
attunements: {
|
||||||
|
enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 },
|
||||||
|
invoker: { id: 'invoker', active: false, level: 1, experience: 0 },
|
||||||
|
fabricator: { id: 'fabricator', active: false, level: 1, experience: 0 },
|
||||||
|
},
|
||||||
|
golemancy: {
|
||||||
|
enabledGolems: [],
|
||||||
|
summonedGolems: [],
|
||||||
|
lastSummonFloor: 0,
|
||||||
|
},
|
||||||
|
insight: 0,
|
||||||
|
totalInsight: 0,
|
||||||
|
prestigeUpgrades: {},
|
||||||
|
memorySlots: 3,
|
||||||
|
memories: [],
|
||||||
|
incursionStrength: 0,
|
||||||
|
containmentWards: 0,
|
||||||
|
log: [],
|
||||||
|
loopInsight: 0,
|
||||||
|
...overrides,
|
||||||
|
} as GameState;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Prestige Upgrades', () => {
|
||||||
|
it('all prestige upgrades should have valid costs', () => {
|
||||||
|
Object.entries(PRESTIGE_DEF).forEach(([id, upgrade]) => {
|
||||||
|
expect(upgrade.cost).toBeGreaterThan(0);
|
||||||
|
expect(upgrade.max).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Mana Well prestige should add 500 starting max mana', () => {
|
||||||
|
const state0 = createMockState({ prestigeUpgrades: { manaWell: 0 } });
|
||||||
|
const state1 = createMockState({ prestigeUpgrades: { manaWell: 1 } });
|
||||||
|
const state5 = createMockState({ prestigeUpgrades: { manaWell: 5 } });
|
||||||
|
|
||||||
|
expect(computeMaxMana(state0, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100);
|
||||||
|
expect(computeMaxMana(state1, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 500);
|
||||||
|
expect(computeMaxMana(state5, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 2500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Elemental Attunement prestige should add 25 element cap', () => {
|
||||||
|
const state0 = createMockState({ prestigeUpgrades: { elementalAttune: 0 } });
|
||||||
|
const state1 = createMockState({ prestigeUpgrades: { elementalAttune: 1 } });
|
||||||
|
const state10 = createMockState({ prestigeUpgrades: { elementalAttune: 10 } });
|
||||||
|
|
||||||
|
expect(computeElementMax(state0, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10);
|
||||||
|
expect(computeElementMax(state1, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 25);
|
||||||
|
expect(computeElementMax(state10, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 250);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Prestige upgrade tests defined.');
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Skill Prerequisites Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { SKILLS_DEF } from '../constants';
|
||||||
|
|
||||||
|
describe('Skill Prerequisites', () => {
|
||||||
|
it('Mana Overflow should require Mana Well 3', () => {
|
||||||
|
expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Mana Surge should require Mana Tap 1', () => {
|
||||||
|
expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Deep Trance should require Meditation 1', () => {
|
||||||
|
expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Void Meditation should require Deep Trance 1', () => {
|
||||||
|
expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Efficient Enchant should require Enchanting 3', () => {
|
||||||
|
expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Skill prerequisites tests defined.');
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Specialized Skills Tests
|
||||||
|
*
|
||||||
|
* Tests for Enchanter and Golemancy skills
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { SKILLS_DEF } from '../constants';
|
||||||
|
|
||||||
|
describe('Enchanter Skills', () => {
|
||||||
|
describe('Enchanting (Unlock enchantment design)', () => {
|
||||||
|
it('skill definition should exist', () => {
|
||||||
|
expect(SKILLS_DEF.enchanting).toBeDefined();
|
||||||
|
expect(SKILLS_DEF.enchanting.attunement).toBe('enchanter');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Efficient Enchant (-5% enchantment capacity cost)', () => {
|
||||||
|
it('skill definition should exist', () => {
|
||||||
|
expect(SKILLS_DEF.efficientEnchant).toBeDefined();
|
||||||
|
expect(SKILLS_DEF.efficientEnchant.max).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require Enchanting 3', () => {
|
||||||
|
expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Disenchanting (Recover mana from removed enchantments)', () => {
|
||||||
|
it('skill definition should not exist', () => {
|
||||||
|
// disenchanting skill removed - see Bug 13
|
||||||
|
expect(SKILLS_DEF.disenchanting).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Golemancy Skills', () => {
|
||||||
|
describe('Golem Mastery (+10% golem damage)', () => {
|
||||||
|
it('skill definition should exist', () => {
|
||||||
|
expect(SKILLS_DEF.golemMastery).toBeDefined();
|
||||||
|
expect(SKILLS_DEF.golemMastery.attunementReq).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Golem Efficiency (+5% attack speed)', () => {
|
||||||
|
it('skill definition should exist', () => {
|
||||||
|
expect(SKILLS_DEF.golemEfficiency).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Golem Longevity (+1 floor duration)', () => {
|
||||||
|
it('skill definition should exist', () => {
|
||||||
|
expect(SKILLS_DEF.golemLongevity).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Golem Siphon (-10% maintenance)', () => {
|
||||||
|
it('skill definition should exist', () => {
|
||||||
|
expect(SKILLS_DEF.golemSiphon).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Specialized skills tests defined.');
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Study Skills Tests
|
||||||
|
*
|
||||||
|
* Tests for study-related skills: Quick Learner, Focused Mind,
|
||||||
|
* Meditation Focus, Knowledge Retention, Deep Trance, Void Meditation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
getStudySpeedMultiplier,
|
||||||
|
getStudyCostMultiplier,
|
||||||
|
getMeditationBonus,
|
||||||
|
} from '../computed-stats';
|
||||||
|
import { SKILLS_DEF } from '../constants';
|
||||||
|
|
||||||
|
describe('Study Skills', () => {
|
||||||
|
describe('Quick Learner (+10% study speed)', () => {
|
||||||
|
it('should multiply study speed by 10% per level', () => {
|
||||||
|
expect(getStudySpeedMultiplier({})).toBe(1);
|
||||||
|
expect(getStudySpeedMultiplier({ quickLearner: 1 })).toBe(1.1);
|
||||||
|
expect(getStudySpeedMultiplier({ quickLearner: 3 })).toBe(1.3);
|
||||||
|
expect(getStudySpeedMultiplier({ quickLearner: 5 })).toBe(1.5);
|
||||||
|
expect(getStudySpeedMultiplier({ quickLearner: 10 })).toBe(2.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.quickLearner.desc).toBe("+10% study speed");
|
||||||
|
expect(SKILLS_DEF.quickLearner.max).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Focused Mind (-5% study mana cost)', () => {
|
||||||
|
it('should reduce study mana cost by 5% per level', () => {
|
||||||
|
expect(getStudyCostMultiplier({})).toBe(1);
|
||||||
|
expect(getStudyCostMultiplier({ focusedMind: 1 })).toBe(0.95);
|
||||||
|
expect(getStudyCostMultiplier({ focusedMind: 3 })).toBe(0.85);
|
||||||
|
expect(getStudyCostMultiplier({ focusedMind: 5 })).toBe(0.75);
|
||||||
|
expect(getStudyCostMultiplier({ focusedMind: 10 })).toBe(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.focusedMind.desc).toBe("-5% study mana cost");
|
||||||
|
expect(SKILLS_DEF.focusedMind.max).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Meditation Focus (Up to 2.5x regen after 4hrs)', () => {
|
||||||
|
it('should provide meditation bonus caps', () => {
|
||||||
|
expect(SKILLS_DEF.meditation.desc).toContain("2.5x");
|
||||||
|
expect(SKILLS_DEF.meditation.max).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Knowledge Retention (+20% study progress saved)', () => {
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.knowledgeRetention.desc).toBe("+20% study progress saved on cancel");
|
||||||
|
expect(SKILLS_DEF.knowledgeRetention.max).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Deep Trance (Extend to 6hrs for 3x)', () => {
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.deepTrance.desc).toContain("6hrs");
|
||||||
|
expect(SKILLS_DEF.deepTrance.max).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require Meditation 1', () => {
|
||||||
|
expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Void Meditation (Extend to 8hrs for 5x)', () => {
|
||||||
|
it('skill definition should match description', () => {
|
||||||
|
expect(SKILLS_DEF.voidMeditation.desc).toContain("8hrs");
|
||||||
|
expect(SKILLS_DEF.voidMeditation.max).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require Deep Trance 1', () => {
|
||||||
|
expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Meditation Bonus Tests ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Meditation Bonus', () => {
|
||||||
|
it('should start at 1x with no meditation', () => {
|
||||||
|
expect(getMeditationBonus(0, {})).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ramp up over time without skills', () => {
|
||||||
|
const bonus1hr = getMeditationBonus(25, {}); // 1 hour of ticks
|
||||||
|
expect(bonus1hr).toBeGreaterThan(1);
|
||||||
|
|
||||||
|
const bonus4hr = getMeditationBonus(100, {}); // 4 hours
|
||||||
|
expect(bonus4hr).toBeGreaterThan(bonus1hr);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cap at 1.5x without meditation skill', () => {
|
||||||
|
const bonus = getMeditationBonus(200, {}); // 8 hours
|
||||||
|
expect(bonus).toBe(1.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give 2.5x with meditation skill after 4 hours', () => {
|
||||||
|
const bonus = getMeditationBonus(100, { meditation: 1 });
|
||||||
|
expect(bonus).toBe(2.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give 3.0x with deepTrance skill after 6 hours', () => {
|
||||||
|
const bonus = getMeditationBonus(150, { meditation: 1, deepTrance: 1 });
|
||||||
|
expect(bonus).toBe(3.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give 5.0x with voidMeditation skill after 8 hours', () => {
|
||||||
|
const bonus = getMeditationBonus(200, { meditation: 1, deepTrance: 1, voidMeditation: 1 });
|
||||||
|
expect(bonus).toBe(5.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Study skills tests defined.');
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Study Times Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { SKILLS_DEF } from '../constants';
|
||||||
|
|
||||||
|
describe('Study Times', () => {
|
||||||
|
it('all skills should have reasonable study times', () => {
|
||||||
|
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
|
||||||
|
expect(skill.studyTime).toBeGreaterThan(0);
|
||||||
|
expect(skill.studyTime).toBeLessThanOrEqual(72);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ascension skills should have long study times', () => {
|
||||||
|
const ascensionSkills = Object.entries(SKILLS_DEF).filter(([, s]) => s.cat === 'ascension');
|
||||||
|
ascensionSkills.forEach(([, skill]) => {
|
||||||
|
expect(skill.studyTime).toBeGreaterThanOrEqual(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Study times tests defined.');
|
||||||
@@ -1,589 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* Comprehensive Skill Tests
|
* Skills Tests - Main Index
|
||||||
*
|
*
|
||||||
* Tests each skill to verify they work exactly as their descriptions say.
|
* This file re-exports all individual skill test files.
|
||||||
* Updated for the new skill system with tiers and upgrade trees.
|
* Each test file is focused on a specific area of functionality.
|
||||||
|
*
|
||||||
|
* Original file: skills.test.ts (589 lines)
|
||||||
|
* Refactored into 8 smaller test files.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import './skills-tests/mana-skills.test';
|
||||||
import {
|
import './skills-tests/study-skills.test';
|
||||||
computeMaxMana,
|
import './skills-tests/ascension-skills.test';
|
||||||
computeElementMax,
|
import './skills-tests/specialized-skills.test';
|
||||||
computeRegen,
|
import './skills-tests/skill-prerequisites.test';
|
||||||
computeClickMana,
|
import './skills-tests/study-times.test';
|
||||||
calcInsight,
|
import './skills-tests/prestige-upgrades.test';
|
||||||
getMeditationBonus,
|
import './skills-tests/integration-and-evolution.test';
|
||||||
} from '../computed-stats';
|
|
||||||
import {
|
console.log('✅ All skills tests complete (refactored from 589 lines to 8 focused test files).');
|
||||||
SKILLS_DEF,
|
|
||||||
PRESTIGE_DEF,
|
|
||||||
GUARDIANS,
|
|
||||||
getStudySpeedMultiplier,
|
|
||||||
getStudyCostMultiplier,
|
|
||||||
ELEMENTS,
|
|
||||||
} from '../constants';
|
|
||||||
import {
|
|
||||||
SKILL_EVOLUTION_PATHS,
|
|
||||||
getUpgradesForSkillAtMilestone,
|
|
||||||
getNextTierSkill,
|
|
||||||
getTierMultiplier,
|
|
||||||
generateTierSkillDef,
|
|
||||||
canTierUp,
|
|
||||||
} from '../skill-evolution';
|
|
||||||
import type { GameState } from '../types';
|
|
||||||
|
|
||||||
// ─── Test Helpers ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function createMockState(overrides: Partial<GameState> = {}): GameState {
|
|
||||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
|
||||||
const baseElements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death', 'transference', 'metal', 'sand', 'crystal', 'stellar', 'void', 'lightning'];
|
|
||||||
baseElements.forEach((k) => {
|
|
||||||
elements[k] = { current: 0, max: 10, unlocked: baseElements.slice(0, 4).includes(k) };
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
day: 1,
|
|
||||||
hour: 0,
|
|
||||||
loopCount: 0,
|
|
||||||
gameOver: false,
|
|
||||||
victory: false,
|
|
||||||
paused: false,
|
|
||||||
rawMana: 100,
|
|
||||||
meditateTicks: 0,
|
|
||||||
totalManaGathered: 0,
|
|
||||||
elements,
|
|
||||||
currentFloor: 1,
|
|
||||||
floorHP: 100,
|
|
||||||
floorMaxHP: 100,
|
|
||||||
castProgress: 0,
|
|
||||||
currentRoom: {
|
|
||||||
roomType: 'combat',
|
|
||||||
enemies: [{ id: 'enemy', hp: 100, maxHP: 100, armor: 0, dodgeChance: 0, element: 'fire' }],
|
|
||||||
},
|
|
||||||
maxFloorReached: 1,
|
|
||||||
signedPacts: [],
|
|
||||||
activeSpell: 'manaBolt',
|
|
||||||
currentAction: 'meditate',
|
|
||||||
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
|
||||||
skills: {},
|
|
||||||
skillProgress: {},
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
parallelStudyTarget: null,
|
|
||||||
equippedInstances: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory1: null, accessory2: null },
|
|
||||||
equipmentInstances: {},
|
|
||||||
enchantmentDesigns: [],
|
|
||||||
designProgress: null,
|
|
||||||
preparationProgress: null,
|
|
||||||
applicationProgress: null,
|
|
||||||
equipmentCraftingProgress: null,
|
|
||||||
unlockedEffects: [],
|
|
||||||
equipmentSpellStates: [],
|
|
||||||
equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null },
|
|
||||||
inventory: [],
|
|
||||||
blueprints: {},
|
|
||||||
lootInventory: { materials: {}, blueprints: [] },
|
|
||||||
schedule: [],
|
|
||||||
autoSchedule: false,
|
|
||||||
studyQueue: [],
|
|
||||||
craftQueue: [],
|
|
||||||
currentStudyTarget: null,
|
|
||||||
achievements: { unlocked: [], progress: {} },
|
|
||||||
totalSpellsCast: 0,
|
|
||||||
totalDamageDealt: 0,
|
|
||||||
totalCraftsCompleted: 0,
|
|
||||||
attunements: {
|
|
||||||
enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 },
|
|
||||||
invoker: { id: 'invoker', active: false, level: 1, experience: 0 },
|
|
||||||
fabricator: { id: 'fabricator', active: false, level: 1, experience: 0 },
|
|
||||||
},
|
|
||||||
golemancy: {
|
|
||||||
enabledGolems: [],
|
|
||||||
summonedGolems: [],
|
|
||||||
lastSummonFloor: 0,
|
|
||||||
},
|
|
||||||
insight: 0,
|
|
||||||
totalInsight: 0,
|
|
||||||
prestigeUpgrades: {},
|
|
||||||
memorySlots: 3,
|
|
||||||
memories: [],
|
|
||||||
incursionStrength: 0,
|
|
||||||
containmentWards: 0,
|
|
||||||
log: [],
|
|
||||||
loopInsight: 0,
|
|
||||||
...overrides,
|
|
||||||
} as GameState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Mana Skills Tests ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Mana Skills', () => {
|
|
||||||
describe('Mana Well (+100 max mana)', () => {
|
|
||||||
it('should add 100 max mana per level', () => {
|
|
||||||
const state0 = createMockState({ skills: { manaWell: 0 } });
|
|
||||||
const state1 = createMockState({ skills: { manaWell: 1 } });
|
|
||||||
const state5 = createMockState({ skills: { manaWell: 5 } });
|
|
||||||
const state10 = createMockState({ skills: { manaWell: 10 } });
|
|
||||||
|
|
||||||
expect(computeMaxMana(state0, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100);
|
|
||||||
expect(computeMaxMana(state1, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 100);
|
|
||||||
expect(computeMaxMana(state5, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 500);
|
|
||||||
expect(computeMaxMana(state10, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.manaWell.desc).toBe("+100 max mana");
|
|
||||||
expect(SKILLS_DEF.manaWell.max).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have upgrade tree', () => {
|
|
||||||
expect(SKILL_EVOLUTION_PATHS.manaWell).toBeDefined();
|
|
||||||
expect(SKILL_EVOLUTION_PATHS.manaWell.tiers.length).toBe(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Flow (+1 regen/hr)', () => {
|
|
||||||
it('should add 1 regen per hour per level', () => {
|
|
||||||
const state0 = createMockState({ skills: { manaFlow: 0 } });
|
|
||||||
const state1 = createMockState({ skills: { manaFlow: 1 } });
|
|
||||||
const state5 = createMockState({ skills: { manaFlow: 5 } });
|
|
||||||
|
|
||||||
const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 };
|
|
||||||
expect(computeRegen(state0, effects)).toBe(2);
|
|
||||||
expect(computeRegen(state1, effects)).toBe(2 + 1);
|
|
||||||
expect(computeRegen(state5, effects)).toBe(2 + 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.manaFlow.desc).toBe("+1 regen/hr");
|
|
||||||
expect(SKILLS_DEF.manaFlow.max).toBe(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Spring (+2 mana regen)', () => {
|
|
||||||
it('should add 2 mana regen', () => {
|
|
||||||
const state0 = createMockState({ skills: { manaSpring: 0 } });
|
|
||||||
const state1 = createMockState({ skills: { manaSpring: 1 } });
|
|
||||||
|
|
||||||
const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 };
|
|
||||||
expect(computeRegen(state0, effects)).toBe(2);
|
|
||||||
expect(computeRegen(state1, effects)).toBe(2 + 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.manaSpring.desc).toBe("+2 mana regen");
|
|
||||||
expect(SKILLS_DEF.manaSpring.max).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Elemental Attunement (+50 elem mana cap)', () => {
|
|
||||||
it('should add 50 element mana capacity per level', () => {
|
|
||||||
const state0 = createMockState({ skills: { elemAttune: 0 } });
|
|
||||||
const state1 = createMockState({ skills: { elemAttune: 1 } });
|
|
||||||
const state5 = createMockState({ skills: { elemAttune: 5 } });
|
|
||||||
|
|
||||||
expect(computeElementMax(state0, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10);
|
|
||||||
expect(computeElementMax(state1, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 50);
|
|
||||||
expect(computeElementMax(state5, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 250);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.elemAttune.desc).toBe("+50 elem mana cap");
|
|
||||||
expect(SKILLS_DEF.elemAttune.max).toBe(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Overflow (+25% mana from clicks)', () => {
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.manaOverflow.desc).toBe("+25% mana from clicks");
|
|
||||||
expect(SKILLS_DEF.manaOverflow.max).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require Mana Well 3', () => {
|
|
||||||
expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Tap (+1 mana/click)', () => {
|
|
||||||
it('should add 1 mana per click', () => {
|
|
||||||
const state0 = createMockState({ skills: { manaTap: 0 } });
|
|
||||||
const state1 = createMockState({ skills: { manaTap: 1 } });
|
|
||||||
|
|
||||||
expect(computeClickMana(state0, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(1);
|
|
||||||
expect(computeClickMana(state1, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.manaTap.desc).toBe("+1 mana/click");
|
|
||||||
expect(SKILLS_DEF.manaTap.max).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Surge (+3 mana/click)', () => {
|
|
||||||
it('should add 3 mana per click', () => {
|
|
||||||
const state1 = createMockState({ skills: { manaSurge: 1 } });
|
|
||||||
expect(computeClickMana(state1, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(1 + 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack with Mana Tap', () => {
|
|
||||||
const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } });
|
|
||||||
expect(computeClickMana(state, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(1 + 1 + 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require Mana Tap 1', () => {
|
|
||||||
expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Study Skills Tests ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Study Skills', () => {
|
|
||||||
describe('Quick Learner (+10% study speed)', () => {
|
|
||||||
it('should multiply study speed by 10% per level', () => {
|
|
||||||
expect(getStudySpeedMultiplier({})).toBe(1);
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 1 })).toBe(1.1);
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 3 })).toBe(1.3);
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 5 })).toBe(1.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.quickLearner.desc).toBe("+10% study speed");
|
|
||||||
expect(SKILLS_DEF.quickLearner.max).toBe(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Focused Mind (-5% study mana cost)', () => {
|
|
||||||
it('should reduce study mana cost by 5% per level', () => {
|
|
||||||
expect(getStudyCostMultiplier({})).toBe(1);
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 1 })).toBe(0.95);
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 3 })).toBe(0.85);
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 5 })).toBe(0.75);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.focusedMind.desc).toBe("-5% study mana cost");
|
|
||||||
expect(SKILLS_DEF.focusedMind.max).toBe(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Meditation Focus (Up to 2.5x regen after 4hrs)', () => {
|
|
||||||
it('should provide meditation bonus caps', () => {
|
|
||||||
expect(SKILLS_DEF.meditation.desc).toContain("2.5x");
|
|
||||||
expect(SKILLS_DEF.meditation.max).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Knowledge Retention (+20% study progress saved)', () => {
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.knowledgeRetention.desc).toBe("+20% study progress saved on cancel");
|
|
||||||
expect(SKILLS_DEF.knowledgeRetention.max).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Deep Trance (Extend to 6hrs for 3x)', () => {
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.deepTrance.desc).toContain("6hrs");
|
|
||||||
expect(SKILLS_DEF.deepTrance.max).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require Meditation 1', () => {
|
|
||||||
expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Void Meditation (Extend to 8hrs for 5x)', () => {
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.voidMeditation.desc).toContain("8hrs");
|
|
||||||
expect(SKILLS_DEF.voidMeditation.max).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require Deep Trance 1', () => {
|
|
||||||
expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Ascension Skills Tests ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Ascension Skills', () => {
|
|
||||||
describe('Insight Harvest (+10% insight gain)', () => {
|
|
||||||
it('should multiply insight gain by 10% per level', () => {
|
|
||||||
const state0 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 0 } });
|
|
||||||
const state1 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 1 } });
|
|
||||||
const state5 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 5 } });
|
|
||||||
|
|
||||||
const insight0 = calcInsight(state0);
|
|
||||||
const insight1 = calcInsight(state1);
|
|
||||||
const insight5 = calcInsight(state5);
|
|
||||||
|
|
||||||
expect(insight1).toBeGreaterThan(insight0);
|
|
||||||
expect(insight5).toBeGreaterThan(insight1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.insightHarvest.desc).toBe("+10% insight gain");
|
|
||||||
expect(SKILLS_DEF.insightHarvest.max).toBe(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Guardian Bane (+20% dmg vs guardians)', () => {
|
|
||||||
it('skill definition should match description', () => {
|
|
||||||
expect(SKILLS_DEF.guardianBane.desc).toBe("+20% dmg vs guardians");
|
|
||||||
expect(SKILLS_DEF.guardianBane.max).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Enchanter Skills Tests ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Enchanter Skills', () => {
|
|
||||||
describe('Enchanting (Unlock enchantment design)', () => {
|
|
||||||
it('skill definition should exist', () => {
|
|
||||||
expect(SKILLS_DEF.enchanting).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.enchanting.attunement).toBe('enchanter');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Efficient Enchant (-5% enchantment capacity cost)', () => {
|
|
||||||
it('skill definition should exist', () => {
|
|
||||||
expect(SKILLS_DEF.efficientEnchant).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.efficientEnchant.max).toBe(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Disenchanting (Recover mana from removed enchantments)', () => {
|
|
||||||
it('skill definition should exist', () => {
|
|
||||||
// disenchanting skill removed - see Bug 13
|
|
||||||
expect(SKILLS_DEF.disenchanting).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Golemancy Skills Tests ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Golemancy Skills', () => {
|
|
||||||
describe('Golem Mastery (+10% golem damage)', () => {
|
|
||||||
it('skill definition should exist', () => {
|
|
||||||
expect(SKILLS_DEF.golemMastery).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.golemMastery.attunementReq).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Golem Efficiency (+5% attack speed)', () => {
|
|
||||||
it('skill definition should exist', () => {
|
|
||||||
expect(SKILLS_DEF.golemEfficiency).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Golem Longevity (+1 floor duration)', () => {
|
|
||||||
it('skill definition should exist', () => {
|
|
||||||
expect(SKILLS_DEF.golemLongevity).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Golem Siphon (-10% maintenance)', () => {
|
|
||||||
it('skill definition should exist', () => {
|
|
||||||
expect(SKILLS_DEF.golemSiphon).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Meditation Bonus Tests ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Meditation Bonus', () => {
|
|
||||||
it('should start at 1x with no meditation', () => {
|
|
||||||
expect(getMeditationBonus(0, {})).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ramp up over time without skills', () => {
|
|
||||||
const bonus1hr = getMeditationBonus(25, {}); // 1 hour of ticks
|
|
||||||
expect(bonus1hr).toBeGreaterThan(1);
|
|
||||||
|
|
||||||
const bonus4hr = getMeditationBonus(100, {}); // 4 hours
|
|
||||||
expect(bonus4hr).toBeGreaterThan(bonus1hr);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should cap at 1.5x without meditation skill', () => {
|
|
||||||
const bonus = getMeditationBonus(200, {}); // 8 hours
|
|
||||||
expect(bonus).toBe(1.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should give 2.5x with meditation skill after 4 hours', () => {
|
|
||||||
const bonus = getMeditationBonus(100, { meditation: 1 });
|
|
||||||
expect(bonus).toBe(2.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should give 3.0x with deepTrance skill after 6 hours', () => {
|
|
||||||
const bonus = getMeditationBonus(150, { meditation: 1, deepTrance: 1 });
|
|
||||||
expect(bonus).toBe(3.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should give 5.0x with voidMeditation skill after 8 hours', () => {
|
|
||||||
const bonus = getMeditationBonus(200, { meditation: 1, deepTrance: 1, voidMeditation: 1 });
|
|
||||||
expect(bonus).toBe(5.0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Skill Prerequisites Tests ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Skill Prerequisites', () => {
|
|
||||||
it('Mana Overflow should require Mana Well 3', () => {
|
|
||||||
expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Mana Surge should require Mana Tap 1', () => {
|
|
||||||
expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Deep Trance should require Meditation 1', () => {
|
|
||||||
expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Void Meditation should require Deep Trance 1', () => {
|
|
||||||
expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Efficient Enchant should require Enchanting 3', () => {
|
|
||||||
expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Study Time Tests ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Study Times', () => {
|
|
||||||
it('all skills should have reasonable study times', () => {
|
|
||||||
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
|
|
||||||
expect(skill.studyTime).toBeGreaterThan(0);
|
|
||||||
expect(skill.studyTime).toBeLessThanOrEqual(72);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ascension skills should have long study times', () => {
|
|
||||||
const ascensionSkills = Object.entries(SKILLS_DEF).filter(([, s]) => s.cat === 'ascension');
|
|
||||||
ascensionSkills.forEach(([, skill]) => {
|
|
||||||
expect(skill.studyTime).toBeGreaterThanOrEqual(20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Prestige Upgrade Tests ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Prestige Upgrades', () => {
|
|
||||||
it('all prestige upgrades should have valid costs', () => {
|
|
||||||
Object.entries(PRESTIGE_DEF).forEach(([id, upgrade]) => {
|
|
||||||
expect(upgrade.cost).toBeGreaterThan(0);
|
|
||||||
expect(upgrade.max).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Mana Well prestige should add 500 starting max mana', () => {
|
|
||||||
const state0 = createMockState({ prestigeUpgrades: { manaWell: 0 } });
|
|
||||||
const state1 = createMockState({ prestigeUpgrades: { manaWell: 1 } });
|
|
||||||
const state5 = createMockState({ prestigeUpgrades: { manaWell: 5 } });
|
|
||||||
|
|
||||||
expect(computeMaxMana(state0, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100);
|
|
||||||
expect(computeMaxMana(state1, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 500);
|
|
||||||
expect(computeMaxMana(state5, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 2500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Elemental Attunement prestige should add 25 element cap', () => {
|
|
||||||
const state0 = createMockState({ prestigeUpgrades: { elementalAttune: 0 } });
|
|
||||||
const state1 = createMockState({ prestigeUpgrades: { elementalAttune: 1 } });
|
|
||||||
const state10 = createMockState({ prestigeUpgrades: { elementalAttune: 10 } });
|
|
||||||
|
|
||||||
expect(computeElementMax(state0, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10);
|
|
||||||
expect(computeElementMax(state1, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 25);
|
|
||||||
expect(computeElementMax(state10, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 250);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Integration Tests ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Integration Tests', () => {
|
|
||||||
it('skill costs should scale with level', () => {
|
|
||||||
const skill = SKILLS_DEF.manaWell;
|
|
||||||
for (let level = 0; level < skill.max; level++) {
|
|
||||||
const cost = skill.base * (level + 1);
|
|
||||||
expect(cost).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('all skills should have valid categories', () => {
|
|
||||||
const validCategories = ['mana', 'study', 'ascension', 'enchant', 'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'research', 'craft', 'hybrid'];
|
|
||||||
Object.values(SKILLS_DEF).forEach(skill => {
|
|
||||||
expect(validCategories).toContain(skill.cat);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('all prerequisite skills should exist', () => {
|
|
||||||
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
|
|
||||||
if (skill.req) {
|
|
||||||
Object.keys(skill.req).forEach(reqId => {
|
|
||||||
expect(SKILLS_DEF[reqId]).toBeDefined();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('all prerequisite levels should be within skill max', () => {
|
|
||||||
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
|
|
||||||
if (skill.req) {
|
|
||||||
Object.entries(skill.req).forEach(([reqId, reqLevel]) => {
|
|
||||||
expect(reqLevel).toBeLessThanOrEqual(SKILLS_DEF[reqId].max);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('all attunement-requiring skills should have valid attunement', () => {
|
|
||||||
const validAttunements = ['enchanter', 'invoker', 'fabricator'];
|
|
||||||
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
|
|
||||||
if (skill.attunement) {
|
|
||||||
expect(validAttunements).toContain(skill.attunement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Skill Evolution Tests ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Skill Evolution', () => {
|
|
||||||
it('skills with max > 1 should have evolution paths', () => {
|
|
||||||
const skillsWithMaxGt1 = Object.entries(SKILLS_DEF)
|
|
||||||
.filter(([_, def]) => def.max > 1)
|
|
||||||
.map(([id]) => id);
|
|
||||||
|
|
||||||
for (const skillId of skillsWithMaxGt1) {
|
|
||||||
expect(SKILL_EVOLUTION_PATHS[skillId], `Missing evolution path for ${skillId}`).toBeDefined();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tier multiplier should be 10^(tier-1)', () => {
|
|
||||||
expect(getTierMultiplier('manaWell')).toBe(1);
|
|
||||||
expect(getTierMultiplier('manaWell_t2')).toBe(10);
|
|
||||||
expect(getTierMultiplier('manaWell_t3')).toBe(100);
|
|
||||||
expect(getTierMultiplier('manaWell_t4')).toBe(1000);
|
|
||||||
expect(getTierMultiplier('manaWell_t5')).toBe(10000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getNextTierSkill should return correct next tier', () => {
|
|
||||||
expect(getNextTierSkill('manaWell')).toBe('manaWell_t2');
|
|
||||||
expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3');
|
|
||||||
expect(getNextTierSkill('manaWell_t5')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('generateTierSkillDef should return valid definitions', () => {
|
|
||||||
const tier1 = generateTierSkillDef('manaWell', 1);
|
|
||||||
expect(tier1).not.toBeNull();
|
|
||||||
expect(tier1?.name).toBe('Mana Well');
|
|
||||||
expect(tier1?.multiplier).toBe(1);
|
|
||||||
|
|
||||||
const tier2 = generateTierSkillDef('manaWell', 2);
|
|
||||||
expect(tier2).not.toBeNull();
|
|
||||||
expect(tier2?.name).toBe('Deep Reservoir');
|
|
||||||
expect(tier2?.multiplier).toBe(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ All skill tests defined.');
|
|
||||||
|
|||||||
Executable → Regular
+4
-218
@@ -1,100 +1,7 @@
|
|||||||
// ─── Attunement System ─────────────────────────────────────────────────────────
|
// ─── Attunement Definitions ─────────────────────────────────────────
|
||||||
// Attunements are powerful magical bonds tied to specific body locations
|
// Data file containing all attunement definitions
|
||||||
// Each grants a unique capability, primary mana type, and skill tree
|
|
||||||
|
|
||||||
import type { SkillDef } from './types';
|
import type { AttunementDef, AttunementType } from '../types';
|
||||||
|
|
||||||
// ─── Body Slots ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export type AttunementSlot =
|
|
||||||
| 'rightHand'
|
|
||||||
| 'leftHand'
|
|
||||||
| 'head'
|
|
||||||
| 'back'
|
|
||||||
| 'chest'
|
|
||||||
| 'leftLeg'
|
|
||||||
| 'rightLeg';
|
|
||||||
|
|
||||||
export const ATTUNEMENT_SLOTS: AttunementSlot[] = [
|
|
||||||
'rightHand',
|
|
||||||
'leftHand',
|
|
||||||
'head',
|
|
||||||
'back',
|
|
||||||
'chest',
|
|
||||||
'leftLeg',
|
|
||||||
'rightLeg',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Slot display names
|
|
||||||
export const ATTUNEMENT_SLOT_NAMES: Record<AttunementSlot, string> = {
|
|
||||||
rightHand: 'Right Hand',
|
|
||||||
leftHand: 'Left Hand',
|
|
||||||
head: 'Head',
|
|
||||||
back: 'Back',
|
|
||||||
chest: 'Heart',
|
|
||||||
leftLeg: 'Left Leg',
|
|
||||||
rightLeg: 'Right Leg',
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Mana Types ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export type ManaType =
|
|
||||||
// Primary mana types from attunements
|
|
||||||
| 'transference' // Enchanter - moving/enchanting
|
|
||||||
| 'form' // Caster - shaping spells
|
|
||||||
| 'vision' // Seer - perception/revelation
|
|
||||||
| 'barrier' // Warden - protection/defense
|
|
||||||
| 'flow' // Strider - movement/swiftness
|
|
||||||
| 'stability' // Anchor - grounding/endurance
|
|
||||||
// Guardian pact types (Invoker)
|
|
||||||
| 'fire'
|
|
||||||
| 'water'
|
|
||||||
| 'earth'
|
|
||||||
| 'air'
|
|
||||||
| 'light'
|
|
||||||
| 'dark'
|
|
||||||
| 'life'
|
|
||||||
| 'death'
|
|
||||||
// Raw mana
|
|
||||||
| 'raw';
|
|
||||||
|
|
||||||
// ─── Attunement Types ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export type AttunementType =
|
|
||||||
| 'enchanter'
|
|
||||||
| 'caster'
|
|
||||||
| 'seer'
|
|
||||||
| 'warden'
|
|
||||||
| 'invoker'
|
|
||||||
| 'strider'
|
|
||||||
| 'anchor';
|
|
||||||
|
|
||||||
// ─── Attunement Definition ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface AttunementDef {
|
|
||||||
id: AttunementType;
|
|
||||||
name: string;
|
|
||||||
slot: AttunementSlot;
|
|
||||||
description: string;
|
|
||||||
capability: string; // What this attunement unlocks
|
|
||||||
primaryManaType: ManaType | null; // null for Invoker (uses guardian types)
|
|
||||||
rawManaRegen: number; // Base raw mana regen bonus
|
|
||||||
autoConvertRate: number; // Raw mana -> primary mana per hour
|
|
||||||
skills: Record<string, SkillDef>; // Attunement-specific skills
|
|
||||||
icon: string; // Lucide icon name
|
|
||||||
color: string; // Theme color
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Attunement State ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface AttunementState {
|
|
||||||
unlocked: boolean;
|
|
||||||
level: number; // Attunement level (from challenges)
|
|
||||||
manaPool: number; // Current primary mana
|
|
||||||
maxMana: number; // Max primary mana pool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Attunement Definitions ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -151,11 +58,7 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// ... rest of attunement definitions (same as original data.ts)
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// CASTER - Left Hand
|
|
||||||
// Shapes raw mana into spell patterns. Enhanced spell damage.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
caster: {
|
caster: {
|
||||||
id: 'caster',
|
id: 'caster',
|
||||||
name: 'Caster',
|
name: 'Caster',
|
||||||
@@ -203,11 +106,6 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// SEER - Head
|
|
||||||
// Perception and revelation. Critical hit bonus and weakness detection.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
seer: {
|
seer: {
|
||||||
id: 'seer',
|
id: 'seer',
|
||||||
name: 'Seer',
|
name: 'Seer',
|
||||||
@@ -255,11 +153,6 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// WARDEN - Back
|
|
||||||
// Protection and defense. Damage reduction and shields.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
warden: {
|
warden: {
|
||||||
id: 'warden',
|
id: 'warden',
|
||||||
name: 'Warden',
|
name: 'Warden',
|
||||||
@@ -307,11 +200,6 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// INVOKER - Chest/Heart
|
|
||||||
// Pact with guardians. No primary mana - uses guardian elemental types.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
invoker: {
|
invoker: {
|
||||||
id: 'invoker',
|
id: 'invoker',
|
||||||
name: 'Invoker',
|
name: 'Invoker',
|
||||||
@@ -360,11 +248,6 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// STRIDER - Left Leg
|
|
||||||
// Movement and swiftness. Attack speed and mobility.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
strider: {
|
strider: {
|
||||||
id: 'strider',
|
id: 'strider',
|
||||||
name: 'Strider',
|
name: 'Strider',
|
||||||
@@ -412,11 +295,6 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// ANCHOR - Right Leg
|
|
||||||
// Stability and endurance. Max mana and knockback resistance.
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
anchor: {
|
anchor: {
|
||||||
id: 'anchor',
|
id: 'anchor',
|
||||||
name: 'Anchor',
|
name: 'Anchor',
|
||||||
@@ -465,95 +343,3 @@ export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Helper Functions ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the attunement for a specific body slot
|
|
||||||
*/
|
|
||||||
export function getAttunementForSlot(slot: AttunementSlot): AttunementDef | undefined {
|
|
||||||
return Object.values(ATTUNEMENTS).find(a => a.slot === slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the starting attunement (Enchanter - right hand)
|
|
||||||
*/
|
|
||||||
export function getStartingAttunement(): AttunementDef {
|
|
||||||
return ATTUNEMENTS.enchanter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an attunement is unlocked for the player
|
|
||||||
*/
|
|
||||||
export function isAttunementUnlocked(
|
|
||||||
attunementStates: Record<AttunementType, AttunementState>,
|
|
||||||
attunementType: AttunementType
|
|
||||||
): boolean {
|
|
||||||
return attunementStates[attunementType]?.unlocked ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get total raw mana regen from all unlocked attunements
|
|
||||||
*/
|
|
||||||
export function getTotalAttunementRegen(
|
|
||||||
attunementStates: Record<AttunementType, AttunementState>
|
|
||||||
): number {
|
|
||||||
let total = 0;
|
|
||||||
for (const [type, state] of Object.entries(attunementStates)) {
|
|
||||||
if (state.unlocked) {
|
|
||||||
const def = ATTUNEMENTS[type as AttunementType];
|
|
||||||
if (def) {
|
|
||||||
total += def.rawManaRegen * (1 + state.level * 0.1); // +10% per level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get mana type display name
|
|
||||||
*/
|
|
||||||
export function getManaTypeName(type: ManaType): string {
|
|
||||||
const names: Record<ManaType, string> = {
|
|
||||||
raw: 'Raw Mana',
|
|
||||||
transference: 'Transference',
|
|
||||||
form: 'Form',
|
|
||||||
vision: 'Vision',
|
|
||||||
barrier: 'Barrier',
|
|
||||||
flow: 'Flow',
|
|
||||||
stability: 'Stability',
|
|
||||||
fire: 'Fire',
|
|
||||||
water: 'Water',
|
|
||||||
earth: 'Earth',
|
|
||||||
air: 'Air',
|
|
||||||
light: 'Light',
|
|
||||||
dark: 'Dark',
|
|
||||||
life: 'Life',
|
|
||||||
death: 'Death',
|
|
||||||
};
|
|
||||||
return names[type] || type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get mana type color
|
|
||||||
*/
|
|
||||||
export function getManaTypeColor(type: ManaType): string {
|
|
||||||
const colors: Record<ManaType, string> = {
|
|
||||||
raw: '#A78BFA', // Light purple
|
|
||||||
transference: '#8B5CF6', // Purple
|
|
||||||
form: '#3B82F6', // Blue
|
|
||||||
vision: '#F59E0B', // Amber
|
|
||||||
barrier: '#10B981', // Green
|
|
||||||
flow: '#06B6D4', // Cyan
|
|
||||||
stability: '#78716C', // Stone
|
|
||||||
fire: '#EF4444', // Red
|
|
||||||
water: '#3B82F6', // Blue
|
|
||||||
earth: '#A16207', // Brown
|
|
||||||
air: '#94A3B8', // Slate
|
|
||||||
light: '#FCD34D', // Yellow
|
|
||||||
dark: '#6B7280', // Gray
|
|
||||||
life: '#22C55E', // Green
|
|
||||||
death: '#7C3AED', // Violet
|
|
||||||
};
|
|
||||||
return colors[type] || '#A78BFA';
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// ─── Attunement System ─────────────────────────────────────────────────
|
||||||
|
// Attunements are powerful magical bonds tied to specific body locations
|
||||||
|
// Each grants a unique capability, primary mana type, and skill tree
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export type {
|
||||||
|
AttunementSlot,
|
||||||
|
AttunementType,
|
||||||
|
AttunementDef,
|
||||||
|
AttunementState,
|
||||||
|
ManaType
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ATTUNEMENT_SLOTS,
|
||||||
|
ATTUNEMENT_SLOT_NAMES
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// Re-export data
|
||||||
|
export { ATTUNEMENTS } from './data';
|
||||||
|
|
||||||
|
// Re-export utils
|
||||||
|
export {
|
||||||
|
getAttunementForSlot,
|
||||||
|
getStartingAttunement,
|
||||||
|
isAttunementUnlocked,
|
||||||
|
getTotalAttunementRegen,
|
||||||
|
getManaTypeName,
|
||||||
|
getManaTypeColor,
|
||||||
|
} from './utils';
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// ─── Attunement Types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type AttunementSlot =
|
||||||
|
| 'rightHand'
|
||||||
|
| 'leftHand'
|
||||||
|
| 'head'
|
||||||
|
| 'back'
|
||||||
|
| 'chest'
|
||||||
|
| 'leftLeg'
|
||||||
|
| 'rightLeg';
|
||||||
|
|
||||||
|
export const ATTUNEMENT_SLOTS: AttunementSlot[] = [
|
||||||
|
'rightHand',
|
||||||
|
'leftHand',
|
||||||
|
'head',
|
||||||
|
'back',
|
||||||
|
'chest',
|
||||||
|
'leftLeg',
|
||||||
|
'rightLeg',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Slot display names
|
||||||
|
export const ATTUNEMENT_SLOT_NAMES: Record<AttunementSlot, string> = {
|
||||||
|
rightHand: 'Right Hand',
|
||||||
|
leftHand: 'Left Hand',
|
||||||
|
head: 'Head',
|
||||||
|
back: 'Back',
|
||||||
|
chest: 'Heart',
|
||||||
|
leftLeg: 'Left Leg',
|
||||||
|
rightLeg: 'Right Leg',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Mana Types ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type ManaType =
|
||||||
|
// Primary mana types from attunements
|
||||||
|
| 'transference' // Enchanter - moving/enchanting
|
||||||
|
| 'form' // Caster - shaping spells
|
||||||
|
| 'vision' // Seer - perception/revelation
|
||||||
|
| 'barrier' // Warden - protection/defense
|
||||||
|
| 'flow' // Strider - movement/swiftness
|
||||||
|
| 'stability' // Anchor - grounding/endurance
|
||||||
|
// Guardian pact types (Invoker)
|
||||||
|
| 'fire'
|
||||||
|
| 'water'
|
||||||
|
| 'earth'
|
||||||
|
| 'air'
|
||||||
|
| 'light'
|
||||||
|
| 'dark'
|
||||||
|
| 'life'
|
||||||
|
| 'death'
|
||||||
|
// Raw mana
|
||||||
|
| 'raw';
|
||||||
|
|
||||||
|
// ─── Attunement Types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type AttunementType =
|
||||||
|
| 'enchanter'
|
||||||
|
| 'caster'
|
||||||
|
| 'seer'
|
||||||
|
| 'warden'
|
||||||
|
| 'invoker'
|
||||||
|
| 'strider'
|
||||||
|
| 'anchor';
|
||||||
|
|
||||||
|
// ─── Attunement Definition ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface AttunementDef {
|
||||||
|
id: AttunementType;
|
||||||
|
name: string;
|
||||||
|
slot: AttunementSlot;
|
||||||
|
description: string;
|
||||||
|
capability: string; // What this attunement unlocks
|
||||||
|
primaryManaType: ManaType | null; // null for Invoker (uses guardian types)
|
||||||
|
rawManaRegen: number; // Base raw mana regen bonus
|
||||||
|
autoConvertRate: number; // Raw mana -> primary mana per hour
|
||||||
|
skills: Record<string, SkillDef>; // Attunement-specific skills
|
||||||
|
icon: string; // Lucide icon name
|
||||||
|
color: string; // Theme color
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Attunement State ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface AttunementState {
|
||||||
|
unlocked: boolean;
|
||||||
|
level: number; // Attunement level (from challenges)
|
||||||
|
manaPool: number; // Current primary mana
|
||||||
|
maxMana: number; // Max primary mana pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill definition (imported from types but re-defined here for clarity)
|
||||||
|
export interface SkillDef {
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
cat: string;
|
||||||
|
max: number;
|
||||||
|
base: number;
|
||||||
|
studyTime: number;
|
||||||
|
req?: Record<string, number>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// ─── Attunement Helper Functions ─────────────────────────
|
||||||
|
|
||||||
|
import type { AttunementSlot, AttunementType, AttunementState, ManaType, AttunementDef } from './types';
|
||||||
|
import { ATTUNEMENTS } from './data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attunement for a specific body slot
|
||||||
|
*/
|
||||||
|
export function getAttunementForSlot(slot: AttunementSlot): AttunementDef | undefined {
|
||||||
|
return Object.values(ATTUNEMENTS).find(a => a.slot === slot) as AttunementDef | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the starting attunement (Enchanter - right hand)
|
||||||
|
*/
|
||||||
|
export function getStartingAttunement(): AttunementDef {
|
||||||
|
return ATTUNEMENTS.enchanter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an attunement is unlocked for the player
|
||||||
|
*/
|
||||||
|
export function isAttunementUnlocked(
|
||||||
|
attunementStates: Record<AttunementType, AttunementState>,
|
||||||
|
attunementType: AttunementType
|
||||||
|
): boolean {
|
||||||
|
return attunementStates[attunementType]?.unlocked ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total raw mana regen from all unlocked attunements
|
||||||
|
*/
|
||||||
|
export function getTotalAttunementRegen(
|
||||||
|
attunementStates: Record<AttunementType, AttunementState>
|
||||||
|
): number {
|
||||||
|
let total = 0;
|
||||||
|
for (const [type, state] of Object.entries(attunementStates)) {
|
||||||
|
if (state.unlocked) {
|
||||||
|
const def = ATTUNEMENTS[type as AttunementType];
|
||||||
|
if (def) {
|
||||||
|
total += def.rawManaRegen * (1 + state.level * 0.1); // +10% per level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mana type display name
|
||||||
|
*/
|
||||||
|
export function getManaTypeName(type: ManaType): string {
|
||||||
|
const names: Record<ManaType, string> = {
|
||||||
|
raw: 'Raw Mana',
|
||||||
|
transference: 'Transference',
|
||||||
|
form: 'Form',
|
||||||
|
vision: 'Vision',
|
||||||
|
barrier: 'Barrier',
|
||||||
|
flow: 'Flow',
|
||||||
|
stability: 'Stability',
|
||||||
|
fire: 'Fire',
|
||||||
|
water: 'Water',
|
||||||
|
earth: 'Earth',
|
||||||
|
air: 'Air',
|
||||||
|
light: 'Light',
|
||||||
|
dark: 'Dark',
|
||||||
|
life: 'Life',
|
||||||
|
death: 'Death',
|
||||||
|
};
|
||||||
|
return names[type] || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mana type color
|
||||||
|
*/
|
||||||
|
export function getManaTypeColor(type: ManaType): string {
|
||||||
|
const colors: Record<ManaType, string> = {
|
||||||
|
raw: '#A78BFA', // Light purple
|
||||||
|
transference: '#8B5CF6', // Purple
|
||||||
|
form: '#3B82F6', // Blue
|
||||||
|
vision: '#F59E0B', // Amber
|
||||||
|
barrier: '#10B981', // Green
|
||||||
|
flow: '#06B6D4', // Cyan
|
||||||
|
stability: '#78716C', // Stone
|
||||||
|
fire: '#EF4444', // Red
|
||||||
|
water: '#3B82F6', // Blue
|
||||||
|
earth: '#A16207', // Brown
|
||||||
|
air: '#94A3B8', // Slate
|
||||||
|
light: '#FCD34D', // Yellow
|
||||||
|
dark: '#6B7280', // Gray
|
||||||
|
life: '#22C55E', // Green
|
||||||
|
death: '#7C3AED', // Violet
|
||||||
|
};
|
||||||
|
return colors[type] || '#A78BFA';
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
// ─── Advanced Spells (Tier 2) ────────────────────────────────────────────────
|
||||||
|
// 8-12 hours study
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { elemCost } from '../elements';
|
||||||
|
|
||||||
|
export const ADVANCED_SPELLS: Record<string, SpellDef> = {
|
||||||
|
// Tier 2 - Advanced Spells (8-12 hours study)
|
||||||
|
inferno: {
|
||||||
|
name: "Inferno",
|
||||||
|
elem: "fire",
|
||||||
|
dmg: 60,
|
||||||
|
cost: elemCost("fire", 8),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1,
|
||||||
|
unlock: 1000,
|
||||||
|
studyTime: 8,
|
||||||
|
desc: "Engulf your enemy in flames."
|
||||||
|
},
|
||||||
|
flameWave: {
|
||||||
|
name: "Flame Wave",
|
||||||
|
elem: "fire",
|
||||||
|
dmg: 45,
|
||||||
|
cost: elemCost("fire", 6),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.5,
|
||||||
|
unlock: 800,
|
||||||
|
studyTime: 6,
|
||||||
|
desc: "A wave of fire sweeps across the battlefield."
|
||||||
|
},
|
||||||
|
tidalWave: {
|
||||||
|
name: "Tidal Wave",
|
||||||
|
elem: "water",
|
||||||
|
dmg: 55,
|
||||||
|
cost: elemCost("water", 8),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1,
|
||||||
|
unlock: 1000,
|
||||||
|
studyTime: 8,
|
||||||
|
desc: "A massive wave crashes down."
|
||||||
|
},
|
||||||
|
iceStorm: {
|
||||||
|
name: "Ice Storm",
|
||||||
|
elem: "water",
|
||||||
|
dmg: 50,
|
||||||
|
cost: elemCost("water", 7),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.2,
|
||||||
|
unlock: 900,
|
||||||
|
studyTime: 7,
|
||||||
|
desc: "A storm of ice shards."
|
||||||
|
},
|
||||||
|
earthquake: {
|
||||||
|
name: "Earthquake",
|
||||||
|
elem: "earth",
|
||||||
|
dmg: 70,
|
||||||
|
cost: elemCost("earth", 10),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 0.8,
|
||||||
|
unlock: 1200,
|
||||||
|
studyTime: 10,
|
||||||
|
desc: "Shake the very foundation."
|
||||||
|
},
|
||||||
|
stoneBarrage: {
|
||||||
|
name: "Stone Barrage",
|
||||||
|
elem: "earth",
|
||||||
|
dmg: 55,
|
||||||
|
cost: elemCost("earth", 7),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.2,
|
||||||
|
unlock: 1000,
|
||||||
|
studyTime: 8,
|
||||||
|
desc: "Multiple stone projectiles."
|
||||||
|
},
|
||||||
|
hurricane: {
|
||||||
|
name: "Hurricane",
|
||||||
|
elem: "air",
|
||||||
|
dmg: 50,
|
||||||
|
cost: elemCost("air", 8),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1,
|
||||||
|
unlock: 1000,
|
||||||
|
studyTime: 8,
|
||||||
|
desc: "A devastating hurricane."
|
||||||
|
},
|
||||||
|
windBlade: {
|
||||||
|
name: "Wind Blade",
|
||||||
|
elem: "air",
|
||||||
|
dmg: 40,
|
||||||
|
cost: elemCost("air", 5),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.8,
|
||||||
|
unlock: 700,
|
||||||
|
studyTime: 6,
|
||||||
|
desc: "A blade of cutting wind."
|
||||||
|
},
|
||||||
|
solarFlare: {
|
||||||
|
name: "Solar Flare",
|
||||||
|
elem: "light",
|
||||||
|
dmg: 65,
|
||||||
|
cost: elemCost("light", 9),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 0.9,
|
||||||
|
unlock: 1100,
|
||||||
|
studyTime: 9,
|
||||||
|
desc: "A blinding flare of solar energy."
|
||||||
|
},
|
||||||
|
divineSmite: {
|
||||||
|
name: "Divine Smite",
|
||||||
|
elem: "light",
|
||||||
|
dmg: 55,
|
||||||
|
cost: elemCost("light", 7),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.2,
|
||||||
|
unlock: 900,
|
||||||
|
studyTime: 7,
|
||||||
|
desc: "A smite of divine power."
|
||||||
|
},
|
||||||
|
voidRift: {
|
||||||
|
name: "Void Rift",
|
||||||
|
elem: "dark",
|
||||||
|
dmg: 55,
|
||||||
|
cost: elemCost("dark", 8),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1,
|
||||||
|
unlock: 1000,
|
||||||
|
studyTime: 8,
|
||||||
|
desc: "Open a rift to the void."
|
||||||
|
},
|
||||||
|
shadowStorm: {
|
||||||
|
name: "Shadow Storm",
|
||||||
|
elem: "dark",
|
||||||
|
dmg: 48,
|
||||||
|
cost: elemCost("dark", 6),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.3,
|
||||||
|
unlock: 800,
|
||||||
|
studyTime: 6,
|
||||||
|
desc: "A storm of shadows."
|
||||||
|
},
|
||||||
|
soulRend: {
|
||||||
|
name: "Soul Rend",
|
||||||
|
elem: "death",
|
||||||
|
dmg: 50,
|
||||||
|
cost: elemCost("death", 7),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.1,
|
||||||
|
unlock: 1100,
|
||||||
|
studyTime: 9,
|
||||||
|
desc: "Tear at the enemy's soul."
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// ─── AOE Spells ──────────────────────────────────────────────────────────────
|
||||||
|
// Hit multiple enemies, less damage per target
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { elemCost } from '../elements';
|
||||||
|
|
||||||
|
export const AOE_SPELLS: Record<string, SpellDef> = {
|
||||||
|
// Tier 1 AOE
|
||||||
|
fireballAoe: {
|
||||||
|
name: "Fireball (AOE)",
|
||||||
|
elem: "fire",
|
||||||
|
dmg: 8,
|
||||||
|
cost: elemCost("fire", 3),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 150,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "An explosive fireball that hits 3 enemies.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 3,
|
||||||
|
effects: [{ type: 'aoe', value: 3 }]
|
||||||
|
},
|
||||||
|
frostNova: {
|
||||||
|
name: "Frost Nova",
|
||||||
|
elem: "water",
|
||||||
|
dmg: 6,
|
||||||
|
cost: elemCost("water", 3),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 140,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "A burst of frost hitting 4 enemies. May freeze.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 4,
|
||||||
|
effects: [{ type: 'freeze', value: 0.15, chance: 0.2 }]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tier 2 AOE
|
||||||
|
meteorShower: {
|
||||||
|
name: "Meteor Shower",
|
||||||
|
elem: "fire",
|
||||||
|
dmg: 20,
|
||||||
|
cost: elemCost("fire", 8),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1,
|
||||||
|
unlock: 1200,
|
||||||
|
studyTime: 10,
|
||||||
|
desc: "Rain meteors on 5 enemies.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 5
|
||||||
|
},
|
||||||
|
blizzard: {
|
||||||
|
name: "Blizzard",
|
||||||
|
elem: "water",
|
||||||
|
dmg: 18,
|
||||||
|
cost: elemCost("water", 7),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.2,
|
||||||
|
unlock: 1000,
|
||||||
|
studyTime: 9,
|
||||||
|
desc: "A freezing blizzard hitting 4 enemies.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 4,
|
||||||
|
effects: [{ type: 'freeze', value: 0.1, chance: 0.15 }]
|
||||||
|
},
|
||||||
|
earthquakeAoe: {
|
||||||
|
name: "Earth Tremor",
|
||||||
|
elem: "earth",
|
||||||
|
dmg: 25,
|
||||||
|
cost: elemCost("earth", 8),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 0.8,
|
||||||
|
unlock: 1400,
|
||||||
|
studyTime: 10,
|
||||||
|
desc: "Shake the ground, hitting 3 enemies with high damage.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 3
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tier 3 AOE
|
||||||
|
apocalypse: {
|
||||||
|
name: "Apocalypse",
|
||||||
|
elem: "fire",
|
||||||
|
dmg: 80,
|
||||||
|
cost: elemCost("fire", 20),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.5,
|
||||||
|
unlock: 15000,
|
||||||
|
studyTime: 30,
|
||||||
|
desc: "End times. Hits ALL enemies with devastating fire.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 10
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
// ─── Basic Elemental Spells (Tier 1) ────────────────────────────────────────
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { elemCost } from '../elements';
|
||||||
|
|
||||||
|
export const BASIC_ELEMENTAL_SPELLS: Record<string, SpellDef> = {
|
||||||
|
// Tier 1 - Basic Elemental Spells (2-4 hours study)
|
||||||
|
fireball: {
|
||||||
|
name: "Fireball",
|
||||||
|
elem: "fire",
|
||||||
|
dmg: 15,
|
||||||
|
cost: elemCost("fire", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 100,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "Hurl a ball of fire at your enemy."
|
||||||
|
},
|
||||||
|
emberShot: {
|
||||||
|
name: "Ember Shot",
|
||||||
|
elem: "fire",
|
||||||
|
dmg: 10,
|
||||||
|
cost: elemCost("fire", 1),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 3,
|
||||||
|
unlock: 75,
|
||||||
|
studyTime: 1,
|
||||||
|
desc: "A quick shot of embers. Efficient fire damage."
|
||||||
|
},
|
||||||
|
waterJet: {
|
||||||
|
name: "Water Jet",
|
||||||
|
elem: "water",
|
||||||
|
dmg: 12,
|
||||||
|
cost: elemCost("water", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 100,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "A high-pressure jet of water."
|
||||||
|
},
|
||||||
|
iceShard: {
|
||||||
|
name: "Ice Shard",
|
||||||
|
elem: "water",
|
||||||
|
dmg: 14,
|
||||||
|
cost: elemCost("water", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 120,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "Launch a sharp shard of ice."
|
||||||
|
},
|
||||||
|
gust: {
|
||||||
|
name: "Gust",
|
||||||
|
elem: "air",
|
||||||
|
dmg: 10,
|
||||||
|
cost: elemCost("air", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 3,
|
||||||
|
unlock: 100,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "A powerful gust of wind."
|
||||||
|
},
|
||||||
|
windSlash: {
|
||||||
|
name: "Wind Slash",
|
||||||
|
elem: "air",
|
||||||
|
dmg: 12,
|
||||||
|
cost: elemCost("air", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2.5,
|
||||||
|
unlock: 110,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "A cutting blade of wind."
|
||||||
|
},
|
||||||
|
stoneBullet: {
|
||||||
|
name: "Stone Bullet",
|
||||||
|
elem: "earth",
|
||||||
|
dmg: 16,
|
||||||
|
cost: elemCost("earth", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 150,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "Launch a bullet of solid stone."
|
||||||
|
},
|
||||||
|
rockSpike: {
|
||||||
|
name: "Rock Spike",
|
||||||
|
elem: "earth",
|
||||||
|
dmg: 18,
|
||||||
|
cost: elemCost("earth", 3),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 1.5,
|
||||||
|
unlock: 180,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "Summon a spike of rock from below."
|
||||||
|
},
|
||||||
|
lightLance: {
|
||||||
|
name: "Light Lance",
|
||||||
|
elem: "light",
|
||||||
|
dmg: 18,
|
||||||
|
cost: elemCost("light", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 200,
|
||||||
|
studyTime: 4,
|
||||||
|
desc: "A piercing lance of pure light."
|
||||||
|
},
|
||||||
|
radiance: {
|
||||||
|
name: "Radiance",
|
||||||
|
elem: "light",
|
||||||
|
dmg: 14,
|
||||||
|
cost: elemCost("light", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2.5,
|
||||||
|
unlock: 180,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "Burst of radiant energy."
|
||||||
|
},
|
||||||
|
shadowBolt: {
|
||||||
|
name: "Shadow Bolt",
|
||||||
|
elem: "dark",
|
||||||
|
dmg: 16,
|
||||||
|
cost: elemCost("dark", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 200,
|
||||||
|
studyTime: 4,
|
||||||
|
desc: "A bolt of shadowy energy."
|
||||||
|
},
|
||||||
|
darkPulse: {
|
||||||
|
name: "Dark Pulse",
|
||||||
|
elem: "dark",
|
||||||
|
dmg: 12,
|
||||||
|
cost: elemCost("dark", 1),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 3,
|
||||||
|
unlock: 150,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "A quick pulse of darkness."
|
||||||
|
},
|
||||||
|
drain: {
|
||||||
|
name: "Drain",
|
||||||
|
elem: "death",
|
||||||
|
dmg: 10,
|
||||||
|
cost: elemCost("death", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 150,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "Drain life force from your enemy.",
|
||||||
|
},
|
||||||
|
rotTouch: {
|
||||||
|
name: "Rot Touch",
|
||||||
|
elem: "death",
|
||||||
|
dmg: 14,
|
||||||
|
cost: elemCost("death", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 170,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "Touch of decay and rot."
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// ─── Compound Mana Spells ───────────────────────────────────────────────────
|
||||||
|
// Blood, Metal, Wood, Sand
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { elemCost } from '../elements';
|
||||||
|
|
||||||
|
export const COMPOUND_SPELLS: Record<string, SpellDef> = {
|
||||||
|
// ─── METAL SPELLS (Fire + Earth) ─────────────────────────────────────────────
|
||||||
|
// Metal magic is slow but devastating with high armor pierce
|
||||||
|
metalShard: {
|
||||||
|
name: "Metal Shard",
|
||||||
|
elem: "metal",
|
||||||
|
dmg: 16,
|
||||||
|
cost: elemCost("metal", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 1.8,
|
||||||
|
unlock: 220,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "A sharpened metal shard. Slower but pierces armor.",
|
||||||
|
effects: [{ type: 'armor_pierce', value: 0.25 }]
|
||||||
|
},
|
||||||
|
ironFist: {
|
||||||
|
name: "Iron Fist",
|
||||||
|
elem: "metal",
|
||||||
|
dmg: 28,
|
||||||
|
cost: elemCost("metal", 4),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 1.5,
|
||||||
|
unlock: 350,
|
||||||
|
studyTime: 5,
|
||||||
|
desc: "A crushing fist of iron. High armor pierce.",
|
||||||
|
effects: [{ type: 'armor_pierce', value: 0.35 }]
|
||||||
|
},
|
||||||
|
steelTempest: {
|
||||||
|
name: "Steel Tempest",
|
||||||
|
elem: "metal",
|
||||||
|
dmg: 55,
|
||||||
|
cost: elemCost("metal", 8),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1,
|
||||||
|
unlock: 1300,
|
||||||
|
studyTime: 12,
|
||||||
|
desc: "A whirlwind of steel blades. Ignores much armor.",
|
||||||
|
effects: [{ type: 'armor_pierce', value: 0.45 }]
|
||||||
|
},
|
||||||
|
furnaceBlast: {
|
||||||
|
name: "Furnace Blast",
|
||||||
|
elem: "metal",
|
||||||
|
dmg: 200,
|
||||||
|
cost: elemCost("metal", 20),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.5,
|
||||||
|
unlock: 18000,
|
||||||
|
studyTime: 32,
|
||||||
|
desc: "Molten metal and fire combined. Devastating armor pierce.",
|
||||||
|
effects: [{ type: 'armor_pierce', value: 0.6 }]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ─── SAND SPELLS (Earth + Water) ────────────────────────────────────────────
|
||||||
|
// Sand magic slows enemies and deals steady damage
|
||||||
|
sandBlast: {
|
||||||
|
name: "Sand Blast",
|
||||||
|
elem: "sand",
|
||||||
|
dmg: 11,
|
||||||
|
cost: elemCost("sand", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 3,
|
||||||
|
unlock: 190,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "A blast of stinging sand. Fast casting.",
|
||||||
|
},
|
||||||
|
sandstorm: {
|
||||||
|
name: "Sandstorm",
|
||||||
|
elem: "sand",
|
||||||
|
dmg: 22,
|
||||||
|
cost: elemCost("sand", 4),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 300,
|
||||||
|
studyTime: 4,
|
||||||
|
desc: "A swirling sandstorm. Hits 2 enemies.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 2,
|
||||||
|
},
|
||||||
|
desertWind: {
|
||||||
|
name: "Desert Wind",
|
||||||
|
elem: "sand",
|
||||||
|
dmg: 38,
|
||||||
|
cost: elemCost("sand", 6),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.5,
|
||||||
|
unlock: 950,
|
||||||
|
studyTime: 8,
|
||||||
|
desc: "A scouring desert wind. Hits 3 enemies.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 3,
|
||||||
|
},
|
||||||
|
duneCollapse: {
|
||||||
|
name: "Dune Collapse",
|
||||||
|
elem: "sand",
|
||||||
|
dmg: 100,
|
||||||
|
cost: elemCost("sand", 16),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.6,
|
||||||
|
unlock: 14000,
|
||||||
|
studyTime: 28,
|
||||||
|
desc: "Dunes collapse on all enemies. Hits 5 targets.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// ─── Magic Sword Enchantments ───────────────────────────────────────────────
|
||||||
|
// For weapon enchanting system
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { rawCost } from '../elements';
|
||||||
|
|
||||||
|
export const ENCHANTMENT_SPELLS: Record<string, SpellDef> = {
|
||||||
|
fireBlade: {
|
||||||
|
name: "Fire Blade",
|
||||||
|
elem: "fire",
|
||||||
|
dmg: 3,
|
||||||
|
cost: rawCost(1),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 4,
|
||||||
|
unlock: 100,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "Enchant a blade with fire. Burns enemies over time.",
|
||||||
|
isWeaponEnchant: true,
|
||||||
|
effects: [{ type: 'burn', value: 2, duration: 3 }]
|
||||||
|
},
|
||||||
|
frostBlade: {
|
||||||
|
name: "Frost Blade",
|
||||||
|
elem: "water",
|
||||||
|
dmg: 3,
|
||||||
|
cost: rawCost(1),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 4,
|
||||||
|
unlock: 100,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "Enchant a blade with frost. Prevents enemy dodge.",
|
||||||
|
isWeaponEnchant: true,
|
||||||
|
effects: [{ type: 'freeze', value: 0, chance: 1 }] // 100% freeze = no dodge
|
||||||
|
},
|
||||||
|
lightningBlade: {
|
||||||
|
name: "Lightning Blade",
|
||||||
|
elem: "lightning",
|
||||||
|
dmg: 4,
|
||||||
|
cost: rawCost(1),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 5,
|
||||||
|
unlock: 150,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "Enchant a blade with lightning. Pierces 30% armor.",
|
||||||
|
isWeaponEnchant: true,
|
||||||
|
effects: [{ type: 'armor_pierce', value: 0.3 }]
|
||||||
|
},
|
||||||
|
voidBlade: {
|
||||||
|
name: "Void Blade",
|
||||||
|
elem: "dark",
|
||||||
|
dmg: 5,
|
||||||
|
cost: rawCost(2),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 3,
|
||||||
|
unlock: 800,
|
||||||
|
studyTime: 8,
|
||||||
|
desc: "Enchant a blade with void. +20% damage.",
|
||||||
|
isWeaponEnchant: true,
|
||||||
|
effects: [{ type: 'buff', value: 0.2 }]
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// ─── Legendary Spells (Tier 4) ──────────────────────────────────────────────
|
||||||
|
// 40-60 hours study, require exotic elements
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { elemCost } from '../elements';
|
||||||
|
|
||||||
|
export const LEGENDARY_SPELLS: Record<string, SpellDef> = {
|
||||||
|
// Tier 4 - Legendary Spells (40-60 hours study, require exotic elements)
|
||||||
|
stellarNova: {
|
||||||
|
name: "Stellar Nova",
|
||||||
|
elem: "stellar",
|
||||||
|
dmg: 500,
|
||||||
|
cost: elemCost("stellar", 15),
|
||||||
|
tier: 4,
|
||||||
|
castSpeed: 0.4,
|
||||||
|
unlock: 50000,
|
||||||
|
studyTime: 48,
|
||||||
|
desc: "A nova of stellar energy."
|
||||||
|
},
|
||||||
|
voidCollapse: {
|
||||||
|
name: "Void Collapse",
|
||||||
|
elem: "void",
|
||||||
|
dmg: 450,
|
||||||
|
cost: elemCost("void", 12),
|
||||||
|
tier: 4,
|
||||||
|
castSpeed: 0.45,
|
||||||
|
unlock: 40000,
|
||||||
|
studyTime: 42,
|
||||||
|
desc: "Collapse the void upon your enemy."
|
||||||
|
},
|
||||||
|
crystalShatter: {
|
||||||
|
name: "Crystal Shatter",
|
||||||
|
elem: "crystal",
|
||||||
|
dmg: 400,
|
||||||
|
cost: elemCost("crystal", 10),
|
||||||
|
tier: 4,
|
||||||
|
castSpeed: 0.5,
|
||||||
|
unlock: 35000,
|
||||||
|
studyTime: 36,
|
||||||
|
desc: "Shatter crystalline energy."
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// ─── Lightning Spells ────────────────────────────────────────────────────────
|
||||||
|
// Fast, armor-piercing, harder to dodge
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { elemCost } from '../elements';
|
||||||
|
|
||||||
|
export const LIGHTNING_SPELLS: Record<string, SpellDef> = {
|
||||||
|
// Tier 1 - Basic Lightning
|
||||||
|
spark: {
|
||||||
|
name: "Spark",
|
||||||
|
elem: "lightning",
|
||||||
|
dmg: 8,
|
||||||
|
cost: elemCost("lightning", 1),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 4,
|
||||||
|
unlock: 120,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "A quick spark of lightning. Very fast and hard to dodge.",
|
||||||
|
effects: [{ type: 'armor_pierce', value: 0.2 }]
|
||||||
|
},
|
||||||
|
lightningBolt: {
|
||||||
|
name: "Lightning Bolt",
|
||||||
|
elem: "lightning",
|
||||||
|
dmg: 14,
|
||||||
|
cost: elemCost("lightning", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 3,
|
||||||
|
unlock: 150,
|
||||||
|
studyTime: 3,
|
||||||
|
desc: "A bolt of lightning that pierces armor.",
|
||||||
|
effects: [{ type: 'armor_pierce', value: 0.3 }]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tier 2 - Advanced Lightning
|
||||||
|
chainLightning: {
|
||||||
|
name: "Chain Lightning",
|
||||||
|
elem: "lightning",
|
||||||
|
dmg: 25,
|
||||||
|
cost: elemCost("lightning", 5),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 2,
|
||||||
|
unlock: 900,
|
||||||
|
studyTime: 8,
|
||||||
|
desc: "Lightning that arcs between enemies. Hits 3 targets.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 3,
|
||||||
|
effects: [{ type: 'chain', value: 3 }]
|
||||||
|
},
|
||||||
|
stormCall: {
|
||||||
|
name: "Storm Call",
|
||||||
|
elem: "lightning",
|
||||||
|
dmg: 40,
|
||||||
|
cost: elemCost("lightning", 6),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.5,
|
||||||
|
unlock: 1100,
|
||||||
|
studyTime: 10,
|
||||||
|
desc: "Call down a storm. Hits 2 targets with armor pierce.",
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 2,
|
||||||
|
effects: [{ type: 'armor_pierce', value: 0.4 }]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tier 3 - Master Lightning
|
||||||
|
thunderStrike: {
|
||||||
|
name: "Thunder Strike",
|
||||||
|
elem: "lightning",
|
||||||
|
dmg: 150,
|
||||||
|
cost: elemCost("lightning", 15),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.8,
|
||||||
|
unlock: 10000,
|
||||||
|
studyTime: 24,
|
||||||
|
desc: "Devastating lightning that ignores 50% armor.",
|
||||||
|
effects: [{ type: 'armor_pierce', value: 0.5 }]
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// ─── Master Spells (Tier 3) ─────────────────────────────────────────────────
|
||||||
|
// 20-30 hours study
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { elemCost } from '../elements';
|
||||||
|
|
||||||
|
export const MASTER_SPELLS: Record<string, SpellDef> = {
|
||||||
|
// Tier 3 - Master Spells (20-30 hours study)
|
||||||
|
pyroclasm: {
|
||||||
|
name: "Pyroclasm",
|
||||||
|
elem: "fire",
|
||||||
|
dmg: 250,
|
||||||
|
cost: elemCost("fire", 25),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.6,
|
||||||
|
unlock: 10000,
|
||||||
|
studyTime: 24,
|
||||||
|
desc: "An eruption of volcanic fury."
|
||||||
|
},
|
||||||
|
tsunami: {
|
||||||
|
name: "Tsunami",
|
||||||
|
elem: "water",
|
||||||
|
dmg: 220,
|
||||||
|
cost: elemCost("water", 22),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.65,
|
||||||
|
unlock: 10000,
|
||||||
|
studyTime: 24,
|
||||||
|
desc: "A towering wall of water."
|
||||||
|
},
|
||||||
|
meteorStrike: {
|
||||||
|
name: "Meteor Strike",
|
||||||
|
elem: "earth",
|
||||||
|
dmg: 280,
|
||||||
|
cost: elemCost("earth", 28),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.5,
|
||||||
|
unlock: 12000,
|
||||||
|
studyTime: 28,
|
||||||
|
desc: "Call down a meteor from the heavens."
|
||||||
|
},
|
||||||
|
cosmicStorm: {
|
||||||
|
name: "Cosmic Storm",
|
||||||
|
elem: "air",
|
||||||
|
dmg: 200,
|
||||||
|
cost: elemCost("air", 20),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.7,
|
||||||
|
unlock: 10000,
|
||||||
|
studyTime: 24,
|
||||||
|
desc: "A storm of cosmic proportions."
|
||||||
|
},
|
||||||
|
heavenLight: {
|
||||||
|
name: "Heaven's Light",
|
||||||
|
elem: "light",
|
||||||
|
dmg: 240,
|
||||||
|
cost: elemCost("light", 24),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.6,
|
||||||
|
unlock: 11000,
|
||||||
|
studyTime: 26,
|
||||||
|
desc: "The light of heaven itself."
|
||||||
|
},
|
||||||
|
oblivion: {
|
||||||
|
name: "Oblivion",
|
||||||
|
elem: "dark",
|
||||||
|
dmg: 230,
|
||||||
|
cost: elemCost("dark", 23),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.6,
|
||||||
|
unlock: 10500,
|
||||||
|
studyTime: 25,
|
||||||
|
desc: "Consign to oblivion."
|
||||||
|
},
|
||||||
|
deathMark: {
|
||||||
|
name: "Death Mark",
|
||||||
|
elem: "death",
|
||||||
|
dmg: 200,
|
||||||
|
cost: elemCost("death", 20),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.7,
|
||||||
|
unlock: 10000,
|
||||||
|
studyTime: 24,
|
||||||
|
desc: "Mark for death."
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// ─── Raw Mana Spells (Tier 0) ───────────────────────────────────────────────
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { rawCost } from '../elements';
|
||||||
|
|
||||||
|
export const RAW_SPELLS: Record<string, SpellDef> = {
|
||||||
|
// Tier 0 - Basic Raw Mana Spells (fast, costs raw mana)
|
||||||
|
manaBolt: {
|
||||||
|
name: "Mana Bolt",
|
||||||
|
elem: "raw",
|
||||||
|
dmg: 5,
|
||||||
|
cost: rawCost(3),
|
||||||
|
tier: 0,
|
||||||
|
castSpeed: 3,
|
||||||
|
unlock: 0,
|
||||||
|
studyTime: 0,
|
||||||
|
desc: "A weak bolt of pure mana. Costs raw mana instead of elemental."
|
||||||
|
},
|
||||||
|
manaStrike: {
|
||||||
|
name: "Mana Strike",
|
||||||
|
elem: "raw",
|
||||||
|
dmg: 8,
|
||||||
|
cost: rawCost(5),
|
||||||
|
tier: 0,
|
||||||
|
castSpeed: 2.5,
|
||||||
|
unlock: 50,
|
||||||
|
studyTime: 1,
|
||||||
|
desc: "A concentrated strike of raw mana. Slightly stronger than Mana Bolt."
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// ─── Utility Mana Spells ────────────────────────────────────────────────────
|
||||||
|
// Mental, Transference, Force
|
||||||
|
import type { SpellDef } from '../../types';
|
||||||
|
import { elemCost } from '../elements';
|
||||||
|
|
||||||
|
export const UTILITY_SPELLS: Record<string, SpellDef> = {
|
||||||
|
// ─── TRANSFERENCE SPELLS ─────────────────────────────────────────────────────
|
||||||
|
// Transference magic moves mana and enhances efficiency
|
||||||
|
transferStrike: {
|
||||||
|
name: "Transfer Strike",
|
||||||
|
elem: "transference",
|
||||||
|
dmg: 9,
|
||||||
|
cost: elemCost("transference", 2),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 3,
|
||||||
|
unlock: 150,
|
||||||
|
studyTime: 2,
|
||||||
|
desc: "Strike that transfers energy. Very efficient.",
|
||||||
|
},
|
||||||
|
manaRip: {
|
||||||
|
name: "Mana Rip",
|
||||||
|
elem: "transference",
|
||||||
|
dmg: 16,
|
||||||
|
cost: elemCost("transference", 3),
|
||||||
|
tier: 1,
|
||||||
|
castSpeed: 2.5,
|
||||||
|
unlock: 250,
|
||||||
|
studyTime: 4,
|
||||||
|
desc: "Rip mana from the enemy. High efficiency.",
|
||||||
|
},
|
||||||
|
essenceDrain: {
|
||||||
|
name: "Essence Drain",
|
||||||
|
elem: "transference",
|
||||||
|
dmg: 42,
|
||||||
|
cost: elemCost("transference", 7),
|
||||||
|
tier: 2,
|
||||||
|
castSpeed: 1.3,
|
||||||
|
unlock: 1050,
|
||||||
|
studyTime: 10,
|
||||||
|
desc: "Drain the enemy's essence.",
|
||||||
|
},
|
||||||
|
soulTransfer: {
|
||||||
|
name: "Soul Transfer",
|
||||||
|
elem: "transference",
|
||||||
|
dmg: 130,
|
||||||
|
cost: elemCost("transference", 16),
|
||||||
|
tier: 3,
|
||||||
|
castSpeed: 0.6,
|
||||||
|
unlock: 13000,
|
||||||
|
studyTime: 26,
|
||||||
|
desc: "Transfer the soul's energy.",
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,826 +1,39 @@
|
|||||||
// ─── Spells ────────────────────────────────────────────────────────────────────
|
// ─── Spells ────────────────────────────────────────────────────────────────────
|
||||||
import type { SpellDef, SpellCost } from '../types';
|
// Main entry point - re-exports from modular spell definitions
|
||||||
import { rawCost, elemCost } from './elements';
|
// See spells-modules/ directory for individual spell categories
|
||||||
|
|
||||||
export const SPELLS_DEF: Record<string, SpellDef> = {
|
export { RAW_SPELLS } from './spells-modules/raw-spells';
|
||||||
// Tier 0 - Basic Raw Mana Spells (fast, costs raw mana)
|
export { BASIC_ELEMENTAL_SPELLS } from './spells-modules/basic-elemental-spells';
|
||||||
manaBolt: {
|
export { LIGHTNING_SPELLS } from './spells-modules/lightning-spells';
|
||||||
name: "Mana Bolt",
|
export { AOE_SPELLS } from './spells-modules/aoe-spells';
|
||||||
elem: "raw",
|
export { ADVANCED_SPELLS } from './spells-modules/advanced-spells';
|
||||||
dmg: 5,
|
export { MASTER_SPELLS } from './spells-modules/master-spells';
|
||||||
cost: rawCost(3),
|
export { LEGENDARY_SPELLS } from './spells-modules/legendary-spells';
|
||||||
tier: 0,
|
export { ENCHANTMENT_SPELLS } from './spells-modules/enchantment-spells';
|
||||||
castSpeed: 3,
|
export { COMPOUND_SPELLS } from './spells-modules/compound-spells';
|
||||||
unlock: 0,
|
export { UTILITY_SPELLS } from './spells-modules/utility-spells';
|
||||||
studyTime: 0,
|
|
||||||
desc: "A weak bolt of pure mana. Costs raw mana instead of elemental."
|
|
||||||
},
|
|
||||||
manaStrike: {
|
|
||||||
name: "Mana Strike",
|
|
||||||
elem: "raw",
|
|
||||||
dmg: 8,
|
|
||||||
cost: rawCost(5),
|
|
||||||
tier: 0,
|
|
||||||
castSpeed: 2.5,
|
|
||||||
unlock: 50,
|
|
||||||
studyTime: 1,
|
|
||||||
desc: "A concentrated strike of raw mana. Slightly stronger than Mana Bolt."
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 1 - Basic Elemental Spells (2-4 hours study)
|
// Convenience: export combined SPELLS_DEF for backward compatibility
|
||||||
fireball: {
|
import { RAW_SPELLS } from './spells-modules/raw-spells';
|
||||||
name: "Fireball",
|
import { BASIC_ELEMENTAL_SPELLS } from './spells-modules/basic-elemental-spells';
|
||||||
elem: "fire",
|
import { LIGHTNING_SPELLS } from './spells-modules/lightning-spells';
|
||||||
dmg: 15,
|
import { AOE_SPELLS } from './spells-modules/aoe-spells';
|
||||||
cost: elemCost("fire", 2),
|
import { ADVANCED_SPELLS } from './spells-modules/advanced-spells';
|
||||||
tier: 1,
|
import { MASTER_SPELLS } from './spells-modules/master-spells';
|
||||||
castSpeed: 2,
|
import { LEGENDARY_SPELLS } from './spells-modules/legendary-spells';
|
||||||
unlock: 100,
|
import { ENCHANTMENT_SPELLS } from './spells-modules/enchantment-spells';
|
||||||
studyTime: 2,
|
import { COMPOUND_SPELLS } from './spells-modules/compound-spells';
|
||||||
desc: "Hurl a ball of fire at your enemy."
|
import { UTILITY_SPELLS } from './spells-modules/utility-spells';
|
||||||
},
|
|
||||||
emberShot: {
|
|
||||||
name: "Ember Shot",
|
|
||||||
elem: "fire",
|
|
||||||
dmg: 10,
|
|
||||||
cost: elemCost("fire", 1),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 3,
|
|
||||||
unlock: 75,
|
|
||||||
studyTime: 1,
|
|
||||||
desc: "A quick shot of embers. Efficient fire damage."
|
|
||||||
},
|
|
||||||
waterJet: {
|
|
||||||
name: "Water Jet",
|
|
||||||
elem: "water",
|
|
||||||
dmg: 12,
|
|
||||||
cost: elemCost("water", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 100,
|
|
||||||
studyTime: 2,
|
|
||||||
desc: "A high-pressure jet of water."
|
|
||||||
},
|
|
||||||
iceShard: {
|
|
||||||
name: "Ice Shard",
|
|
||||||
elem: "water",
|
|
||||||
dmg: 14,
|
|
||||||
cost: elemCost("water", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 120,
|
|
||||||
studyTime: 2,
|
|
||||||
desc: "Launch a sharp shard of ice."
|
|
||||||
},
|
|
||||||
gust: {
|
|
||||||
name: "Gust",
|
|
||||||
elem: "air",
|
|
||||||
dmg: 10,
|
|
||||||
cost: elemCost("air", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 3,
|
|
||||||
unlock: 100,
|
|
||||||
studyTime: 2,
|
|
||||||
desc: "A powerful gust of wind."
|
|
||||||
},
|
|
||||||
windSlash: {
|
|
||||||
name: "Wind Slash",
|
|
||||||
elem: "air",
|
|
||||||
dmg: 12,
|
|
||||||
cost: elemCost("air", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2.5,
|
|
||||||
unlock: 110,
|
|
||||||
studyTime: 2,
|
|
||||||
desc: "A cutting blade of wind."
|
|
||||||
},
|
|
||||||
stoneBullet: {
|
|
||||||
name: "Stone Bullet",
|
|
||||||
elem: "earth",
|
|
||||||
dmg: 16,
|
|
||||||
cost: elemCost("earth", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 150,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "Launch a bullet of solid stone."
|
|
||||||
},
|
|
||||||
rockSpike: {
|
|
||||||
name: "Rock Spike",
|
|
||||||
elem: "earth",
|
|
||||||
dmg: 18,
|
|
||||||
cost: elemCost("earth", 3),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 1.5,
|
|
||||||
unlock: 180,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "Summon a spike of rock from below."
|
|
||||||
},
|
|
||||||
lightLance: {
|
|
||||||
name: "Light Lance",
|
|
||||||
elem: "light",
|
|
||||||
dmg: 18,
|
|
||||||
cost: elemCost("light", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 200,
|
|
||||||
studyTime: 4,
|
|
||||||
desc: "A piercing lance of pure light."
|
|
||||||
},
|
|
||||||
radiance: {
|
|
||||||
name: "Radiance",
|
|
||||||
elem: "light",
|
|
||||||
dmg: 14,
|
|
||||||
cost: elemCost("light", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2.5,
|
|
||||||
unlock: 180,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "Burst of radiant energy."
|
|
||||||
},
|
|
||||||
shadowBolt: {
|
|
||||||
name: "Shadow Bolt",
|
|
||||||
elem: "dark",
|
|
||||||
dmg: 16,
|
|
||||||
cost: elemCost("dark", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 200,
|
|
||||||
studyTime: 4,
|
|
||||||
desc: "A bolt of shadowy energy."
|
|
||||||
},
|
|
||||||
darkPulse: {
|
|
||||||
name: "Dark Pulse",
|
|
||||||
elem: "dark",
|
|
||||||
dmg: 12,
|
|
||||||
cost: elemCost("dark", 1),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 3,
|
|
||||||
unlock: 150,
|
|
||||||
studyTime: 2,
|
|
||||||
desc: "A quick pulse of darkness."
|
|
||||||
},
|
|
||||||
drain: {
|
|
||||||
name: "Drain",
|
|
||||||
elem: "death",
|
|
||||||
dmg: 10,
|
|
||||||
cost: elemCost("death", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 150,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "Drain life force from your enemy.",
|
|
||||||
},
|
|
||||||
rotTouch: {
|
|
||||||
name: "Rot Touch",
|
|
||||||
elem: "death",
|
|
||||||
dmg: 14,
|
|
||||||
cost: elemCost("death", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 170,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "Touch of decay and rot."
|
|
||||||
},
|
|
||||||
|
|
||||||
|
export const SPELLS_DEF: Record<string, import('../types').SpellDef> = {
|
||||||
// Tier 2 - Advanced Spells (8-12 hours study)
|
...RAW_SPELLS,
|
||||||
inferno: {
|
...BASIC_ELEMENTAL_SPELLS,
|
||||||
name: "Inferno",
|
...LIGHTNING_SPELLS,
|
||||||
elem: "fire",
|
...AOE_SPELLS,
|
||||||
dmg: 60,
|
...ADVANCED_SPELLS,
|
||||||
cost: elemCost("fire", 8),
|
...MASTER_SPELLS,
|
||||||
tier: 2,
|
...LEGENDARY_SPELLS,
|
||||||
castSpeed: 1,
|
...ENCHANTMENT_SPELLS,
|
||||||
unlock: 1000,
|
...COMPOUND_SPELLS,
|
||||||
studyTime: 8,
|
...UTILITY_SPELLS,
|
||||||
desc: "Engulf your enemy in flames."
|
|
||||||
},
|
|
||||||
flameWave: {
|
|
||||||
name: "Flame Wave",
|
|
||||||
elem: "fire",
|
|
||||||
dmg: 45,
|
|
||||||
cost: elemCost("fire", 6),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.5,
|
|
||||||
unlock: 800,
|
|
||||||
studyTime: 6,
|
|
||||||
desc: "A wave of fire sweeps across the battlefield."
|
|
||||||
},
|
|
||||||
tidalWave: {
|
|
||||||
name: "Tidal Wave",
|
|
||||||
elem: "water",
|
|
||||||
dmg: 55,
|
|
||||||
cost: elemCost("water", 8),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1,
|
|
||||||
unlock: 1000,
|
|
||||||
studyTime: 8,
|
|
||||||
desc: "A massive wave crashes down."
|
|
||||||
},
|
|
||||||
iceStorm: {
|
|
||||||
name: "Ice Storm",
|
|
||||||
elem: "water",
|
|
||||||
dmg: 50,
|
|
||||||
cost: elemCost("water", 7),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.2,
|
|
||||||
unlock: 900,
|
|
||||||
studyTime: 7,
|
|
||||||
desc: "A storm of ice shards."
|
|
||||||
},
|
|
||||||
earthquake: {
|
|
||||||
name: "Earthquake",
|
|
||||||
elem: "earth",
|
|
||||||
dmg: 70,
|
|
||||||
cost: elemCost("earth", 10),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 0.8,
|
|
||||||
unlock: 1200,
|
|
||||||
studyTime: 10,
|
|
||||||
desc: "Shake the very foundation."
|
|
||||||
},
|
|
||||||
stoneBarrage: {
|
|
||||||
name: "Stone Barrage",
|
|
||||||
elem: "earth",
|
|
||||||
dmg: 55,
|
|
||||||
cost: elemCost("earth", 7),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.2,
|
|
||||||
unlock: 1000,
|
|
||||||
studyTime: 8,
|
|
||||||
desc: "Multiple stone projectiles."
|
|
||||||
},
|
|
||||||
hurricane: {
|
|
||||||
name: "Hurricane",
|
|
||||||
elem: "air",
|
|
||||||
dmg: 50,
|
|
||||||
cost: elemCost("air", 8),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1,
|
|
||||||
unlock: 1000,
|
|
||||||
studyTime: 8,
|
|
||||||
desc: "A devastating hurricane."
|
|
||||||
},
|
|
||||||
windBlade: {
|
|
||||||
name: "Wind Blade",
|
|
||||||
elem: "air",
|
|
||||||
dmg: 40,
|
|
||||||
cost: elemCost("air", 5),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.8,
|
|
||||||
unlock: 700,
|
|
||||||
studyTime: 6,
|
|
||||||
desc: "A blade of cutting wind."
|
|
||||||
},
|
|
||||||
solarFlare: {
|
|
||||||
name: "Solar Flare",
|
|
||||||
elem: "light",
|
|
||||||
dmg: 65,
|
|
||||||
cost: elemCost("light", 9),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 0.9,
|
|
||||||
unlock: 1100,
|
|
||||||
studyTime: 9,
|
|
||||||
desc: "A blinding flare of solar energy."
|
|
||||||
},
|
|
||||||
divineSmite: {
|
|
||||||
name: "Divine Smite",
|
|
||||||
elem: "light",
|
|
||||||
dmg: 55,
|
|
||||||
cost: elemCost("light", 7),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.2,
|
|
||||||
unlock: 900,
|
|
||||||
studyTime: 7,
|
|
||||||
desc: "A smite of divine power."
|
|
||||||
},
|
|
||||||
voidRift: {
|
|
||||||
name: "Void Rift",
|
|
||||||
elem: "dark",
|
|
||||||
dmg: 55,
|
|
||||||
cost: elemCost("dark", 8),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1,
|
|
||||||
unlock: 1000,
|
|
||||||
studyTime: 8,
|
|
||||||
desc: "Open a rift to the void."
|
|
||||||
},
|
|
||||||
shadowStorm: {
|
|
||||||
name: "Shadow Storm",
|
|
||||||
elem: "dark",
|
|
||||||
dmg: 48,
|
|
||||||
cost: elemCost("dark", 6),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.3,
|
|
||||||
unlock: 800,
|
|
||||||
studyTime: 6,
|
|
||||||
desc: "A storm of shadows."
|
|
||||||
},
|
|
||||||
soulRend: {
|
|
||||||
name: "Soul Rend",
|
|
||||||
elem: "death",
|
|
||||||
dmg: 50,
|
|
||||||
cost: elemCost("death", 7),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.1,
|
|
||||||
unlock: 1100,
|
|
||||||
studyTime: 9,
|
|
||||||
desc: "Tear at the enemy's soul."
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 3 - Master Spells (20-30 hours study)
|
|
||||||
pyroclasm: {
|
|
||||||
name: "Pyroclasm",
|
|
||||||
elem: "fire",
|
|
||||||
dmg: 250,
|
|
||||||
cost: elemCost("fire", 25),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.6,
|
|
||||||
unlock: 10000,
|
|
||||||
studyTime: 24,
|
|
||||||
desc: "An eruption of volcanic fury."
|
|
||||||
},
|
|
||||||
tsunami: {
|
|
||||||
name: "Tsunami",
|
|
||||||
elem: "water",
|
|
||||||
dmg: 220,
|
|
||||||
cost: elemCost("water", 22),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.65,
|
|
||||||
unlock: 10000,
|
|
||||||
studyTime: 24,
|
|
||||||
desc: "A towering wall of water."
|
|
||||||
},
|
|
||||||
meteorStrike: {
|
|
||||||
name: "Meteor Strike",
|
|
||||||
elem: "earth",
|
|
||||||
dmg: 280,
|
|
||||||
cost: elemCost("earth", 28),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.5,
|
|
||||||
unlock: 12000,
|
|
||||||
studyTime: 28,
|
|
||||||
desc: "Call down a meteor from the heavens."
|
|
||||||
},
|
|
||||||
cosmicStorm: {
|
|
||||||
name: "Cosmic Storm",
|
|
||||||
elem: "air",
|
|
||||||
dmg: 200,
|
|
||||||
cost: elemCost("air", 20),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.7,
|
|
||||||
unlock: 10000,
|
|
||||||
studyTime: 24,
|
|
||||||
desc: "A storm of cosmic proportions."
|
|
||||||
},
|
|
||||||
heavenLight: {
|
|
||||||
name: "Heaven's Light",
|
|
||||||
elem: "light",
|
|
||||||
dmg: 240,
|
|
||||||
cost: elemCost("light", 24),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.6,
|
|
||||||
unlock: 11000,
|
|
||||||
studyTime: 26,
|
|
||||||
desc: "The light of heaven itself."
|
|
||||||
},
|
|
||||||
oblivion: {
|
|
||||||
name: "Oblivion",
|
|
||||||
elem: "dark",
|
|
||||||
dmg: 230,
|
|
||||||
cost: elemCost("dark", 23),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.6,
|
|
||||||
unlock: 10500,
|
|
||||||
studyTime: 25,
|
|
||||||
desc: "Consign to oblivion."
|
|
||||||
},
|
|
||||||
deathMark: {
|
|
||||||
name: "Death Mark",
|
|
||||||
elem: "death",
|
|
||||||
dmg: 200,
|
|
||||||
cost: elemCost("death", 20),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.7,
|
|
||||||
unlock: 10000,
|
|
||||||
studyTime: 24,
|
|
||||||
desc: "Mark for death."
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 4 - Legendary Spells (40-60 hours study, require exotic elements)
|
|
||||||
stellarNova: {
|
|
||||||
name: "Stellar Nova",
|
|
||||||
elem: "stellar",
|
|
||||||
dmg: 500,
|
|
||||||
cost: elemCost("stellar", 15),
|
|
||||||
tier: 4,
|
|
||||||
castSpeed: 0.4,
|
|
||||||
unlock: 50000,
|
|
||||||
studyTime: 48,
|
|
||||||
desc: "A nova of stellar energy."
|
|
||||||
},
|
|
||||||
voidCollapse: {
|
|
||||||
name: "Void Collapse",
|
|
||||||
elem: "void",
|
|
||||||
dmg: 450,
|
|
||||||
cost: elemCost("void", 12),
|
|
||||||
tier: 4,
|
|
||||||
castSpeed: 0.45,
|
|
||||||
unlock: 40000,
|
|
||||||
studyTime: 42,
|
|
||||||
desc: "Collapse the void upon your enemy."
|
|
||||||
},
|
|
||||||
crystalShatter: {
|
|
||||||
name: "Crystal Shatter",
|
|
||||||
elem: "crystal",
|
|
||||||
dmg: 400,
|
|
||||||
cost: elemCost("crystal", 10),
|
|
||||||
tier: 4,
|
|
||||||
castSpeed: 0.5,
|
|
||||||
unlock: 35000,
|
|
||||||
studyTime: 36,
|
|
||||||
desc: "Shatter crystalline energy."
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// LIGHTNING SPELLS - Fast, armor-piercing, harder to dodge
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Tier 1 - Basic Lightning
|
|
||||||
spark: {
|
|
||||||
name: "Spark",
|
|
||||||
elem: "lightning",
|
|
||||||
dmg: 8,
|
|
||||||
cost: elemCost("lightning", 1),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 4,
|
|
||||||
unlock: 120,
|
|
||||||
studyTime: 2,
|
|
||||||
desc: "A quick spark of lightning. Very fast and hard to dodge.",
|
|
||||||
effects: [{ type: 'armor_pierce', value: 0.2 }]
|
|
||||||
},
|
|
||||||
lightningBolt: {
|
|
||||||
name: "Lightning Bolt",
|
|
||||||
elem: "lightning",
|
|
||||||
dmg: 14,
|
|
||||||
cost: elemCost("lightning", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 3,
|
|
||||||
unlock: 150,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "A bolt of lightning that pierces armor.",
|
|
||||||
effects: [{ type: 'armor_pierce', value: 0.3 }]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 2 - Advanced Lightning
|
|
||||||
chainLightning: {
|
|
||||||
name: "Chain Lightning",
|
|
||||||
elem: "lightning",
|
|
||||||
dmg: 25,
|
|
||||||
cost: elemCost("lightning", 5),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 900,
|
|
||||||
studyTime: 8,
|
|
||||||
desc: "Lightning that arcs between enemies. Hits 3 targets.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 3,
|
|
||||||
effects: [{ type: 'chain', value: 3 }]
|
|
||||||
},
|
|
||||||
stormCall: {
|
|
||||||
name: "Storm Call",
|
|
||||||
elem: "lightning",
|
|
||||||
dmg: 40,
|
|
||||||
cost: elemCost("lightning", 6),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.5,
|
|
||||||
unlock: 1100,
|
|
||||||
studyTime: 10,
|
|
||||||
desc: "Call down a storm. Hits 2 targets with armor pierce.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 2,
|
|
||||||
effects: [{ type: 'armor_pierce', value: 0.4 }]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 3 - Master Lightning
|
|
||||||
thunderStrike: {
|
|
||||||
name: "Thunder Strike",
|
|
||||||
elem: "lightning",
|
|
||||||
dmg: 150,
|
|
||||||
cost: elemCost("lightning", 15),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.8,
|
|
||||||
unlock: 10000,
|
|
||||||
studyTime: 24,
|
|
||||||
desc: "Devastating lightning that ignores 50% armor.",
|
|
||||||
effects: [{ type: 'armor_pierce', value: 0.5 }]
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// AOE SPELLS - Hit multiple enemies, less damage per target
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Tier 1 AOE
|
|
||||||
fireballAoe: {
|
|
||||||
name: "Fireball (AOE)",
|
|
||||||
elem: "fire",
|
|
||||||
dmg: 8,
|
|
||||||
cost: elemCost("fire", 3),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 150,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "An explosive fireball that hits 3 enemies.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 3,
|
|
||||||
effects: [{ type: 'aoe', value: 3 }]
|
|
||||||
},
|
|
||||||
frostNova: {
|
|
||||||
name: "Frost Nova",
|
|
||||||
elem: "water",
|
|
||||||
dmg: 6,
|
|
||||||
cost: elemCost("water", 3),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 140,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "A burst of frost hitting 4 enemies. May freeze.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 4,
|
|
||||||
effects: [{ type: 'freeze', value: 0.15, chance: 0.2 }]
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 2 AOE
|
|
||||||
meteorShower: {
|
|
||||||
name: "Meteor Shower",
|
|
||||||
elem: "fire",
|
|
||||||
dmg: 20,
|
|
||||||
cost: elemCost("fire", 8),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1,
|
|
||||||
unlock: 1200,
|
|
||||||
studyTime: 10,
|
|
||||||
desc: "Rain meteors on 5 enemies.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 5
|
|
||||||
},
|
|
||||||
blizzard: {
|
|
||||||
name: "Blizzard",
|
|
||||||
elem: "water",
|
|
||||||
dmg: 18,
|
|
||||||
cost: elemCost("water", 7),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.2,
|
|
||||||
unlock: 1000,
|
|
||||||
studyTime: 9,
|
|
||||||
desc: "A freezing blizzard hitting 4 enemies.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 4,
|
|
||||||
effects: [{ type: 'freeze', value: 0.1, chance: 0.15 }]
|
|
||||||
},
|
|
||||||
earthquakeAoe: {
|
|
||||||
name: "Earth Tremor",
|
|
||||||
elem: "earth",
|
|
||||||
dmg: 25,
|
|
||||||
cost: elemCost("earth", 8),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 0.8,
|
|
||||||
unlock: 1400,
|
|
||||||
studyTime: 10,
|
|
||||||
desc: "Shake the ground, hitting 3 enemies with high damage.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 3
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 3 AOE
|
|
||||||
apocalypse: {
|
|
||||||
name: "Apocalypse",
|
|
||||||
elem: "fire",
|
|
||||||
dmg: 80,
|
|
||||||
cost: elemCost("fire", 20),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.5,
|
|
||||||
unlock: 15000,
|
|
||||||
studyTime: 30,
|
|
||||||
desc: "End times. Hits ALL enemies with devastating fire.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 10
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// MAGIC SWORD ENCHANTMENTS - For weapon enchanting system
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
fireBlade: {
|
|
||||||
name: "Fire Blade",
|
|
||||||
elem: "fire",
|
|
||||||
dmg: 3,
|
|
||||||
cost: rawCost(1),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 4,
|
|
||||||
unlock: 100,
|
|
||||||
studyTime: 2,
|
|
||||||
desc: "Enchant a blade with fire. Burns enemies over time.",
|
|
||||||
isWeaponEnchant: true,
|
|
||||||
effects: [{ type: 'burn', value: 2, duration: 3 }]
|
|
||||||
},
|
|
||||||
frostBlade: {
|
|
||||||
name: "Frost Blade",
|
|
||||||
elem: "water",
|
|
||||||
dmg: 3,
|
|
||||||
cost: rawCost(1),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 4,
|
|
||||||
unlock: 100,
|
|
||||||
studyTime: 2,
|
|
||||||
desc: "Enchant a blade with frost. Prevents enemy dodge.",
|
|
||||||
isWeaponEnchant: true,
|
|
||||||
effects: [{ type: 'freeze', value: 0, chance: 1 }] // 100% freeze = no dodge
|
|
||||||
},
|
|
||||||
lightningBlade: {
|
|
||||||
name: "Lightning Blade",
|
|
||||||
elem: "lightning",
|
|
||||||
dmg: 4,
|
|
||||||
cost: rawCost(1),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 5,
|
|
||||||
unlock: 150,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "Enchant a blade with lightning. Pierces 30% armor.",
|
|
||||||
isWeaponEnchant: true,
|
|
||||||
effects: [{ type: 'armor_pierce', value: 0.3 }]
|
|
||||||
},
|
|
||||||
voidBlade: {
|
|
||||||
name: "Void Blade",
|
|
||||||
elem: "dark",
|
|
||||||
dmg: 5,
|
|
||||||
cost: rawCost(2),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 3,
|
|
||||||
unlock: 800,
|
|
||||||
studyTime: 8,
|
|
||||||
desc: "Enchant a blade with void. +20% damage.",
|
|
||||||
isWeaponEnchant: true,
|
|
||||||
effects: [{ type: 'buff', value: 0.2 }]
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// COMPOUND MANA SPELLS - Blood, Metal, Wood, Sand
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// ─── METAL SPELLS (Fire + Earth) ─────────────────────────────────────────────
|
|
||||||
// Metal magic is slow but devastating with high armor pierce
|
|
||||||
metalShard: {
|
|
||||||
name: "Metal Shard",
|
|
||||||
elem: "metal",
|
|
||||||
dmg: 16,
|
|
||||||
cost: elemCost("metal", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 1.8,
|
|
||||||
unlock: 220,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "A sharpened metal shard. Slower but pierces armor.",
|
|
||||||
effects: [{ type: 'armor_pierce', value: 0.25 }]
|
|
||||||
},
|
|
||||||
ironFist: {
|
|
||||||
name: "Iron Fist",
|
|
||||||
elem: "metal",
|
|
||||||
dmg: 28,
|
|
||||||
cost: elemCost("metal", 4),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 1.5,
|
|
||||||
unlock: 350,
|
|
||||||
studyTime: 5,
|
|
||||||
desc: "A crushing fist of iron. High armor pierce.",
|
|
||||||
effects: [{ type: 'armor_pierce', value: 0.35 }]
|
|
||||||
},
|
|
||||||
steelTempest: {
|
|
||||||
name: "Steel Tempest",
|
|
||||||
elem: "metal",
|
|
||||||
dmg: 55,
|
|
||||||
cost: elemCost("metal", 8),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1,
|
|
||||||
unlock: 1300,
|
|
||||||
studyTime: 12,
|
|
||||||
desc: "A whirlwind of steel blades. Ignores much armor.",
|
|
||||||
effects: [{ type: 'armor_pierce', value: 0.45 }]
|
|
||||||
},
|
|
||||||
furnaceBlast: {
|
|
||||||
name: "Furnace Blast",
|
|
||||||
elem: "metal",
|
|
||||||
dmg: 200,
|
|
||||||
cost: elemCost("metal", 20),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.5,
|
|
||||||
unlock: 18000,
|
|
||||||
studyTime: 32,
|
|
||||||
desc: "Molten metal and fire combined. Devastating armor pierce.",
|
|
||||||
effects: [{ type: 'armor_pierce', value: 0.6 }]
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── SAND SPELLS (Earth + Water) ────────────────────────────────────────────
|
|
||||||
// Sand magic slows enemies and deals steady damage
|
|
||||||
sandBlast: {
|
|
||||||
name: "Sand Blast",
|
|
||||||
elem: "sand",
|
|
||||||
dmg: 11,
|
|
||||||
cost: elemCost("sand", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 3,
|
|
||||||
unlock: 190,
|
|
||||||
studyTime: 3,
|
|
||||||
desc: "A blast of stinging sand. Fast casting.",
|
|
||||||
},
|
|
||||||
sandstorm: {
|
|
||||||
name: "Sandstorm",
|
|
||||||
elem: "sand",
|
|
||||||
dmg: 22,
|
|
||||||
cost: elemCost("sand", 4),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2,
|
|
||||||
unlock: 300,
|
|
||||||
studyTime: 4,
|
|
||||||
desc: "A swirling sandstorm. Hits 2 enemies.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 2,
|
|
||||||
},
|
|
||||||
desertWind: {
|
|
||||||
name: "Desert Wind",
|
|
||||||
elem: "sand",
|
|
||||||
dmg: 38,
|
|
||||||
cost: elemCost("sand", 6),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.5,
|
|
||||||
unlock: 950,
|
|
||||||
studyTime: 8,
|
|
||||||
desc: "A scouring desert wind. Hits 3 enemies.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 3,
|
|
||||||
},
|
|
||||||
duneCollapse: {
|
|
||||||
name: "Dune Collapse",
|
|
||||||
elem: "sand",
|
|
||||||
dmg: 100,
|
|
||||||
cost: elemCost("sand", 16),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.6,
|
|
||||||
unlock: 14000,
|
|
||||||
studyTime: 28,
|
|
||||||
desc: "Dunes collapse on all enemies. Hits 5 targets.",
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 5,
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// UTILITY MANA SPELLS - Mental, Transference, Force
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// ─── TRANSFERENCE SPELLS ─────────────────────────────────────────────────────
|
|
||||||
// Transference magic moves mana and enhances efficiency
|
|
||||||
transferStrike: {
|
|
||||||
name: "Transfer Strike",
|
|
||||||
elem: "transference",
|
|
||||||
dmg: 9,
|
|
||||||
cost: elemCost("transference", 2),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 3,
|
|
||||||
unlock: 150,
|
|
||||||
studyTime: 2,
|
|
||||||
desc: "Strike that transfers energy. Very efficient.",
|
|
||||||
},
|
|
||||||
manaRip: {
|
|
||||||
name: "Mana Rip",
|
|
||||||
elem: "transference",
|
|
||||||
dmg: 16,
|
|
||||||
cost: elemCost("transference", 3),
|
|
||||||
tier: 1,
|
|
||||||
castSpeed: 2.5,
|
|
||||||
unlock: 250,
|
|
||||||
studyTime: 4,
|
|
||||||
desc: "Rip mana from the enemy. High efficiency.",
|
|
||||||
},
|
|
||||||
essenceDrain: {
|
|
||||||
name: "Essence Drain",
|
|
||||||
elem: "transference",
|
|
||||||
dmg: 42,
|
|
||||||
cost: elemCost("transference", 7),
|
|
||||||
tier: 2,
|
|
||||||
castSpeed: 1.3,
|
|
||||||
unlock: 1050,
|
|
||||||
studyTime: 10,
|
|
||||||
desc: "Drain the enemy's essence.",
|
|
||||||
},
|
|
||||||
soulTransfer: {
|
|
||||||
name: "Soul Transfer",
|
|
||||||
elem: "transference",
|
|
||||||
dmg: 130,
|
|
||||||
cost: elemCost("transference", 16),
|
|
||||||
tier: 3,
|
|
||||||
castSpeed: 0.6,
|
|
||||||
unlock: 13000,
|
|
||||||
studyTime: 26,
|
|
||||||
desc: "Transfer the soul's energy.",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,513 +0,0 @@
|
|||||||
// ─── Crafting Action Implementations ──────────────────────────────────────────
|
|
||||||
// Action implementations for crafting-slice.ts. Extracted to keep main slice focused.
|
|
||||||
// These functions implement the CraftingActions interface defined in crafting-slice.ts
|
|
||||||
|
|
||||||
import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, LootInventory, AttunementState } from './types';
|
|
||||||
import { EQUIPMENT_TYPES, type EquipmentSlot } from './data/equipment';
|
|
||||||
import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects';
|
|
||||||
import { CRAFTING_RECIPES } from './data/crafting-recipes';
|
|
||||||
import { computeEffects } from './upgrade-effects';
|
|
||||||
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
|
|
||||||
|
|
||||||
import * as CraftingUtils from './crafting-utils';
|
|
||||||
import * as CraftingDesign from './crafting-design';
|
|
||||||
import * as CraftingPrep from './crafting-prep';
|
|
||||||
import * as CraftingApply from './crafting-apply';
|
|
||||||
import * as CraftingEquipment from './crafting-equipment';
|
|
||||||
import * as CraftingLoot from './crafting-loot';
|
|
||||||
import * as CraftingAttunements from './crafting-attunements';
|
|
||||||
|
|
||||||
// ─── Equipment Management Actions ────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Create equipment instance
|
|
||||||
export function createEquipmentInstance(
|
|
||||||
typeId: string,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
): string | null {
|
|
||||||
const type = CraftingUtils.getEquipmentType(typeId);
|
|
||||||
if (!type) return null;
|
|
||||||
|
|
||||||
const instanceId = CraftingUtils.generateInstanceId();
|
|
||||||
const instance: EquipmentInstance = {
|
|
||||||
instanceId,
|
|
||||||
typeId,
|
|
||||||
name: type.name,
|
|
||||||
enchantments: [],
|
|
||||||
usedCapacity: 0,
|
|
||||||
totalCapacity: type.baseCapacity,
|
|
||||||
rarity: 'common',
|
|
||||||
quality: 100,
|
|
||||||
tags: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
set((state) => ({
|
|
||||||
equipmentInstances: {
|
|
||||||
...state.equipmentInstances,
|
|
||||||
[instanceId]: instance,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return instanceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equip item
|
|
||||||
export function equipItem(
|
|
||||||
instanceId: string,
|
|
||||||
slot: EquipmentSlot,
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
): boolean {
|
|
||||||
const state = get();
|
|
||||||
const instance = state.equipmentInstances[instanceId];
|
|
||||||
if (!instance) return false;
|
|
||||||
|
|
||||||
if (!CraftingUtils.canEquipInSlot(instance, slot, state.equippedInstances)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newEquipped = { ...state.equippedInstances };
|
|
||||||
for (const [s, id] of Object.entries(newEquipped)) {
|
|
||||||
if (id === instanceId) {
|
|
||||||
newEquipped[s as EquipmentSlot] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newEquipped[slot] = instanceId;
|
|
||||||
|
|
||||||
if (CraftingUtils.isTwoHanded(instance.typeId) && slot === 'mainHand') {
|
|
||||||
newEquipped.offHand = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(() => ({ equippedInstances: newEquipped }));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unequip item
|
|
||||||
export function unequipItem(
|
|
||||||
slot: EquipmentSlot,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
set((state) => ({
|
|
||||||
equippedInstances: {
|
|
||||||
...state.equippedInstances,
|
|
||||||
[slot]: null,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete equipment instance
|
|
||||||
export function deleteEquipmentInstance(
|
|
||||||
instanceId: string,
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
const state = get();
|
|
||||||
let newEquipped = { ...state.equippedInstances };
|
|
||||||
for (const [slot, id] of Object.entries(newEquipped)) {
|
|
||||||
if (id === instanceId) {
|
|
||||||
newEquipped[slot as EquipmentSlot] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newInstances = { ...state.equipmentInstances };
|
|
||||||
delete newInstances[instanceId];
|
|
||||||
|
|
||||||
set(() => ({
|
|
||||||
equippedInstances: newEquipped,
|
|
||||||
equipmentInstances: newInstances,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Enchantment Design Actions ────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function startDesigningEnchantment(
|
|
||||||
name: string,
|
|
||||||
equipmentTypeId: string,
|
|
||||||
effects: DesignEffect[],
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
): boolean {
|
|
||||||
const state = get();
|
|
||||||
const enchantingLevel = state.skills.enchanting || 0;
|
|
||||||
const validation = CraftingDesign.validateDesignEffects(
|
|
||||||
effects,
|
|
||||||
equipmentTypeId,
|
|
||||||
enchantingLevel
|
|
||||||
);
|
|
||||||
if (!validation.valid) return false;
|
|
||||||
|
|
||||||
const equipType = CraftingUtils.getEquipmentType(equipmentTypeId);
|
|
||||||
if (!equipType) return false;
|
|
||||||
|
|
||||||
const efficiencyBonus = ((state.skillUpgrades || {})['efficientEnchant'] || [])?.length * 0.05 || 0;
|
|
||||||
const totalCapacityCost = CraftingDesign.calculateDesignCapacityCost(effects, efficiencyBonus);
|
|
||||||
|
|
||||||
if (totalCapacityCost > equipType.baseCapacity) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
|
||||||
const hasEnchantMastery = hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_MASTERY);
|
|
||||||
|
|
||||||
let updates: any = {};
|
|
||||||
|
|
||||||
if (!state.designProgress) {
|
|
||||||
updates = {
|
|
||||||
currentAction: 'design' as const,
|
|
||||||
designProgress: {
|
|
||||||
designId: CraftingUtils.generateDesignId(),
|
|
||||||
progress: 0,
|
|
||||||
required: CraftingDesign.calculateDesignTime(effects),
|
|
||||||
name,
|
|
||||||
equipmentType: equipmentTypeId,
|
|
||||||
effects,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else if (hasEnchantMastery && !state.designProgress2) {
|
|
||||||
updates = {
|
|
||||||
designProgress2: {
|
|
||||||
designId: CraftingUtils.generateDesignId(),
|
|
||||||
progress: 0,
|
|
||||||
required: CraftingDesign.calculateDesignTime(effects),
|
|
||||||
name,
|
|
||||||
equipmentType: equipmentTypeId,
|
|
||||||
effects,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(() => updates);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cancelDesign(
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
const state = get();
|
|
||||||
if (state.designProgress2 && !state.designProgress) {
|
|
||||||
set(() => ({ designProgress2: null }));
|
|
||||||
} else {
|
|
||||||
set(() => ({
|
|
||||||
currentAction: 'meditate' as const,
|
|
||||||
designProgress: null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveDesign(
|
|
||||||
design: EnchantmentDesign,
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
const state = get();
|
|
||||||
if (state.designProgress2 && state.designProgress2.designId === design.id) {
|
|
||||||
set((state) => ({
|
|
||||||
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
|
||||||
designProgress2: null,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
set((state) => ({
|
|
||||||
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
|
||||||
designProgress: null,
|
|
||||||
currentAction: 'meditate' as const,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteDesign(
|
|
||||||
designId: string,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
set((state) => ({
|
|
||||||
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Enchantment Preparation Actions ────────────────────────────────────────
|
|
||||||
|
|
||||||
export function startPreparing(
|
|
||||||
equipmentInstanceId: string,
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
): boolean {
|
|
||||||
const state = get();
|
|
||||||
const instance = state.equipmentInstances[equipmentInstanceId];
|
|
||||||
|
|
||||||
const validation = CraftingPrep.canPrepareEquipment(
|
|
||||||
instance,
|
|
||||||
instance?.tags || []
|
|
||||||
);
|
|
||||||
if (!validation.canPrepare) return false;
|
|
||||||
|
|
||||||
if (!instance) return false;
|
|
||||||
|
|
||||||
const costs = CraftingPrep.calculatePreparationCosts(instance.totalCapacity);
|
|
||||||
|
|
||||||
if (state.rawMana < costs.manaTotal) return false;
|
|
||||||
|
|
||||||
set(() => ({
|
|
||||||
currentAction: 'prepare' as const,
|
|
||||||
preparationProgress: CraftingPrep.initializePreparationProgress(
|
|
||||||
equipmentInstanceId,
|
|
||||||
instance.totalCapacity
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cancelPreparation(
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
set(() => ({
|
|
||||||
currentAction: 'meditate' as const,
|
|
||||||
preparationProgress: null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Enchantment Application Actions ────────────────────────────────────────
|
|
||||||
|
|
||||||
export function startApplying(
|
|
||||||
equipmentInstanceId: string,
|
|
||||||
designId: string,
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
): boolean {
|
|
||||||
const state = get();
|
|
||||||
const instance = state.equipmentInstances[equipmentInstanceId];
|
|
||||||
const design = state.enchantmentDesigns.find(d => d.id === designId);
|
|
||||||
|
|
||||||
const validation = CraftingApply.canApplyEnchantment(
|
|
||||||
instance,
|
|
||||||
design,
|
|
||||||
state.currentAction
|
|
||||||
);
|
|
||||||
if (!validation.canApply) return false;
|
|
||||||
|
|
||||||
set(() => ({
|
|
||||||
currentAction: 'enchant' as const,
|
|
||||||
applicationProgress: CraftingApply.initializeApplicationProgress(
|
|
||||||
equipmentInstanceId,
|
|
||||||
designId,
|
|
||||||
design!
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pauseApplication(
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
set((state) => {
|
|
||||||
if (!state.applicationProgress) return {};
|
|
||||||
return {
|
|
||||||
applicationProgress: {
|
|
||||||
...state.applicationProgress,
|
|
||||||
paused: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resumeApplication(
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
set((state) => {
|
|
||||||
if (!state.applicationProgress) return {};
|
|
||||||
return {
|
|
||||||
applicationProgress: {
|
|
||||||
...state.applicationProgress,
|
|
||||||
paused: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cancelApplication(
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
set(() => ({
|
|
||||||
currentAction: 'meditate' as const,
|
|
||||||
applicationProgress: null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Disenchanting Actions ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function disenchantEquipment(
|
|
||||||
instanceId: string,
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
const state = get();
|
|
||||||
const instance = state.equipmentInstances[instanceId];
|
|
||||||
if (!instance || instance.enchantments.length === 0) return;
|
|
||||||
|
|
||||||
const disenchantLevel = 0;
|
|
||||||
const recoveryRate = 0.1 + disenchantLevel * 0.2;
|
|
||||||
|
|
||||||
let totalRecovered = 0;
|
|
||||||
for (const ench of instance.enchantments) {
|
|
||||||
totalRecovered += Math.floor(ench.actualCost * recoveryRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
set((state) => ({
|
|
||||||
rawMana: state.rawMana + totalRecovered,
|
|
||||||
equipmentInstances: {
|
|
||||||
...state.equipmentInstances,
|
|
||||||
[instanceId]: {
|
|
||||||
...instance,
|
|
||||||
enchantments: [],
|
|
||||||
usedCapacity: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
log: [`✨ Disenchanted ${instance.name}, recovered ${totalRecovered} mana.`, ...state.log.slice(0, 49)],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Equipment Crafting Actions ────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function startCraftingEquipment(
|
|
||||||
blueprintId: string,
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
): boolean {
|
|
||||||
const state = get();
|
|
||||||
|
|
||||||
const check = CraftingEquipment.canStartEquipmentCrafting(
|
|
||||||
blueprintId,
|
|
||||||
state.lootInventory.blueprints.includes(blueprintId),
|
|
||||||
state.lootInventory.materials,
|
|
||||||
state.rawMana,
|
|
||||||
state.currentAction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!check.canCraft) return false;
|
|
||||||
|
|
||||||
const result = CraftingEquipment.initializeEquipmentCrafting(
|
|
||||||
blueprintId,
|
|
||||||
state.lootInventory.materials,
|
|
||||||
state.rawMana
|
|
||||||
);
|
|
||||||
|
|
||||||
set((state) => ({
|
|
||||||
lootInventory: {
|
|
||||||
...state.lootInventory,
|
|
||||||
materials: result.newMaterials,
|
|
||||||
},
|
|
||||||
rawMana: state.rawMana - result.manaCost,
|
|
||||||
currentAction: 'craft' as const,
|
|
||||||
equipmentCraftingProgress: result.progress,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cancelEquipmentCrafting(
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
set((state) => {
|
|
||||||
const progress = state.equipmentCraftingProgress;
|
|
||||||
if (!progress) return { currentAction: 'meditate' as const, equipmentCraftingProgress: null };
|
|
||||||
|
|
||||||
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(
|
|
||||||
progress.blueprintId,
|
|
||||||
progress.manaSpent
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentAction: 'meditate' as const,
|
|
||||||
equipmentCraftingProgress: null,
|
|
||||||
rawMana: state.rawMana + cancelResult.manaRefund,
|
|
||||||
log: [cancelResult.logMessage, ...state.log.slice(0, 49)],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteMaterial(
|
|
||||||
materialId: string,
|
|
||||||
amount: number,
|
|
||||||
get: () => GameState,
|
|
||||||
set: (fn: (state: GameState) => Partial<GameState>) => void
|
|
||||||
) {
|
|
||||||
set((state) => {
|
|
||||||
const newMaterials = { ...state.lootInventory.materials };
|
|
||||||
const currentAmount = newMaterials[materialId] || 0;
|
|
||||||
const newAmount = Math.max(0, currentAmount - amount);
|
|
||||||
|
|
||||||
if (newAmount <= 0) {
|
|
||||||
delete newMaterials[materialId];
|
|
||||||
} else {
|
|
||||||
newMaterials[materialId] = newAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
lootInventory: {
|
|
||||||
...state.lootInventory,
|
|
||||||
materials: newMaterials,
|
|
||||||
},
|
|
||||||
log: [`🗑️ Deleted ${amount}x ${materialId}.`, ...state.log.slice(0, 49)],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Computed Getters ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function getEquipmentSpells(get: () => GameState): string[] {
|
|
||||||
const state = get();
|
|
||||||
const spells: string[] = [];
|
|
||||||
|
|
||||||
for (const instanceId of Object.values(state.equippedInstances)) {
|
|
||||||
if (!instanceId) continue;
|
|
||||||
const instance = state.equipmentInstances[instanceId];
|
|
||||||
if (!instance) continue;
|
|
||||||
|
|
||||||
for (const ench of instance.enchantments) {
|
|
||||||
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
|
|
||||||
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
|
|
||||||
spells.push(effectDef.effect.spellId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...new Set(spells)];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEquipmentEffects(get: () => GameState): Record<string, number> {
|
|
||||||
const state = get();
|
|
||||||
const effects: Record<string, number> = {};
|
|
||||||
|
|
||||||
for (const instanceId of Object.values(state.equippedInstances)) {
|
|
||||||
if (!instanceId) continue;
|
|
||||||
const instance = state.equipmentInstances[instanceId];
|
|
||||||
if (!instance) continue;
|
|
||||||
|
|
||||||
for (const ench of instance.enchantments) {
|
|
||||||
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
|
|
||||||
if (!effectDef) continue;
|
|
||||||
|
|
||||||
if (effectDef.effect.type === 'bonus' && effectDef.effect.stat && effectDef.effect.value) {
|
|
||||||
effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + effectDef.effect.value * ench.stacks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return effects;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAvailableCapacity(
|
|
||||||
instanceId: string,
|
|
||||||
get: () => GameState
|
|
||||||
): number {
|
|
||||||
const state = get();
|
|
||||||
const instance = state.equipmentInstances[instanceId];
|
|
||||||
if (!instance) return 0;
|
|
||||||
return instance.totalCapacity - instance.usedCapacity;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// ─── Enchantment Application Actions ────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { GameState } from '../types';
|
||||||
|
import * as CraftingApply from '../crafting-apply';
|
||||||
|
|
||||||
|
export function startApplying(
|
||||||
|
equipmentInstanceId: string,
|
||||||
|
designId: string,
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
): boolean {
|
||||||
|
const state = get();
|
||||||
|
const instance = state.equipmentInstances[equipmentInstanceId];
|
||||||
|
const design = state.enchantmentDesigns.find(d => d.id === designId);
|
||||||
|
|
||||||
|
const validation = CraftingApply.canApplyEnchantment(
|
||||||
|
instance,
|
||||||
|
design,
|
||||||
|
state.currentAction
|
||||||
|
);
|
||||||
|
if (!validation.canApply) return false;
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
currentAction: 'enchant' as const,
|
||||||
|
applicationProgress: CraftingApply.initializeApplicationProgress(
|
||||||
|
equipmentInstanceId,
|
||||||
|
designId,
|
||||||
|
design!
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pauseApplication(
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
set((state) => {
|
||||||
|
if (!state.applicationProgress) return {};
|
||||||
|
return {
|
||||||
|
applicationProgress: {
|
||||||
|
...state.applicationProgress,
|
||||||
|
paused: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resumeApplication(
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
set((state) => {
|
||||||
|
if (!state.applicationProgress) return {};
|
||||||
|
return {
|
||||||
|
applicationProgress: {
|
||||||
|
...state.applicationProgress,
|
||||||
|
paused: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelApplication(
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
set(() => ({
|
||||||
|
currentAction: 'meditate' as const,
|
||||||
|
applicationProgress: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// ─── Computed Getters ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { GameState } from '../types';
|
||||||
|
import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects';
|
||||||
|
|
||||||
|
export function getEquipmentSpells(get: () => GameState): string[] {
|
||||||
|
const state = get();
|
||||||
|
const spells: string[] = [];
|
||||||
|
|
||||||
|
for (const instanceId of Object.values(state.equippedInstances)) {
|
||||||
|
if (!instanceId) continue;
|
||||||
|
const instance = state.equipmentInstances[instanceId];
|
||||||
|
if (!instance) continue;
|
||||||
|
|
||||||
|
for (const ench of instance.enchantments) {
|
||||||
|
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||||
|
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
|
||||||
|
spells.push(effectDef.effect.spellId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(spells)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEquipmentEffects(get: () => GameState): Record<string, number> {
|
||||||
|
const state = get();
|
||||||
|
const effects: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const instanceId of Object.values(state.equippedInstances)) {
|
||||||
|
if (!instanceId) continue;
|
||||||
|
const instance = state.equipmentInstances[instanceId];
|
||||||
|
if (!instance) continue;
|
||||||
|
|
||||||
|
for (const ench of instance.enchantments) {
|
||||||
|
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||||
|
if (!effectDef) continue;
|
||||||
|
|
||||||
|
if (effectDef.effect.type === 'bonus' && effectDef.effect.stat && effectDef.effect.value) {
|
||||||
|
effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + effectDef.effect.value * ench.stacks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return effects;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvailableCapacity(
|
||||||
|
instanceId: string,
|
||||||
|
get: () => GameState
|
||||||
|
): number {
|
||||||
|
const state = get();
|
||||||
|
const instance = state.equipmentInstances[instanceId];
|
||||||
|
if (!instance) return 0;
|
||||||
|
return instance.totalCapacity - instance.usedCapacity;
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// ─── Equipment Crafting Actions ────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { GameState } from '../types';
|
||||||
|
import * as CraftingEquipment from '../crafting-equipment';
|
||||||
|
|
||||||
|
export function startCraftingEquipment(
|
||||||
|
blueprintId: string,
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
): boolean {
|
||||||
|
const state = get();
|
||||||
|
|
||||||
|
const check = CraftingEquipment.canStartEquipmentCrafting(
|
||||||
|
blueprintId,
|
||||||
|
state.lootInventory.blueprints.includes(blueprintId),
|
||||||
|
state.lootInventory.materials,
|
||||||
|
state.rawMana,
|
||||||
|
state.currentAction
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!check.canCraft) return false;
|
||||||
|
|
||||||
|
const result = CraftingEquipment.initializeEquipmentCrafting(
|
||||||
|
blueprintId,
|
||||||
|
state.lootInventory.materials,
|
||||||
|
state.rawMana
|
||||||
|
);
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
lootInventory: {
|
||||||
|
...state.lootInventory,
|
||||||
|
materials: result.newMaterials,
|
||||||
|
},
|
||||||
|
rawMana: state.rawMana - result.manaCost,
|
||||||
|
currentAction: 'craft' as const,
|
||||||
|
equipmentCraftingProgress: result.progress,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelEquipmentCrafting(
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
set((state) => {
|
||||||
|
const progress = state.equipmentCraftingProgress;
|
||||||
|
if (!progress) return { currentAction: 'meditate' as const, equipmentCraftingProgress: null };
|
||||||
|
|
||||||
|
const cancelResult = CraftingEquipment.cancelEquipmentCrafting(
|
||||||
|
progress.blueprintId,
|
||||||
|
progress.manaSpent
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentAction: 'meditate' as const,
|
||||||
|
equipmentCraftingProgress: null,
|
||||||
|
rawMana: state.rawMana + cancelResult.manaRefund,
|
||||||
|
log: [cancelResult.logMessage, ...state.log.slice(0, 49)],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteMaterial(
|
||||||
|
materialId: string,
|
||||||
|
amount: number,
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
set((state) => {
|
||||||
|
const newMaterials = { ...state.lootInventory.materials };
|
||||||
|
const currentAmount = newMaterials[materialId] || 0;
|
||||||
|
const newAmount = Math.max(0, currentAmount - amount);
|
||||||
|
|
||||||
|
if (newAmount <= 0) {
|
||||||
|
delete newMaterials[materialId];
|
||||||
|
} else {
|
||||||
|
newMaterials[materialId] = newAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lootInventory: {
|
||||||
|
...state.lootInventory,
|
||||||
|
materials: newMaterials,
|
||||||
|
},
|
||||||
|
log: [`🗑️ Deleted ${amount}x ${materialId}.`, ...state.log.slice(0, 49)],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// ─── Enchantment Design Actions ────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { GameState, EnchantmentDesign, DesignEffect } from '../types';
|
||||||
|
import * as CraftingUtils from '../crafting-utils';
|
||||||
|
import * as CraftingDesign from '../crafting-design';
|
||||||
|
import { computeEffects } from '../upgrade-effects';
|
||||||
|
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
|
||||||
|
|
||||||
|
export function startDesigningEnchantment(
|
||||||
|
name: string,
|
||||||
|
equipmentTypeId: string,
|
||||||
|
effects: DesignEffect[],
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
): boolean {
|
||||||
|
const state = get();
|
||||||
|
const enchantingLevel = state.skills.enchanting || 0;
|
||||||
|
const validation = CraftingDesign.validateDesignEffects(
|
||||||
|
effects,
|
||||||
|
equipmentTypeId,
|
||||||
|
enchantingLevel
|
||||||
|
);
|
||||||
|
if (!validation.valid) return false;
|
||||||
|
|
||||||
|
const equipType = CraftingUtils.getEquipmentType(equipmentTypeId);
|
||||||
|
if (!equipType) return false;
|
||||||
|
|
||||||
|
const efficiencyBonus = ((state.skillUpgrades || {})['efficientEnchant'] || [])?.length * 0.05 || 0;
|
||||||
|
const totalCapacityCost = CraftingDesign.calculateDesignCapacityCost(effects, efficiencyBonus);
|
||||||
|
|
||||||
|
if (totalCapacityCost > equipType.baseCapacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||||
|
const hasEnchantMastery = hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_MASTERY);
|
||||||
|
|
||||||
|
let updates: any = {};
|
||||||
|
|
||||||
|
if (!state.designProgress) {
|
||||||
|
updates = {
|
||||||
|
currentAction: 'design' as const,
|
||||||
|
designProgress: {
|
||||||
|
designId: CraftingUtils.generateDesignId(),
|
||||||
|
progress: 0,
|
||||||
|
required: CraftingDesign.calculateDesignTime(effects),
|
||||||
|
name,
|
||||||
|
equipmentType: equipmentTypeId,
|
||||||
|
effects,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (hasEnchantMastery && !state.designProgress2) {
|
||||||
|
updates = {
|
||||||
|
designProgress2: {
|
||||||
|
designId: CraftingUtils.generateDesignId(),
|
||||||
|
progress: 0,
|
||||||
|
required: CraftingDesign.calculateDesignTime(effects),
|
||||||
|
name,
|
||||||
|
equipmentType: equipmentTypeId,
|
||||||
|
effects,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(() => updates);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelDesign(
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
const state = get();
|
||||||
|
if (state.designProgress2 && !state.designProgress) {
|
||||||
|
set(() => ({ designProgress2: null }));
|
||||||
|
} else {
|
||||||
|
set(() => ({
|
||||||
|
currentAction: 'meditate' as const,
|
||||||
|
designProgress: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveDesign(
|
||||||
|
design: EnchantmentDesign,
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
const state = get();
|
||||||
|
if (state.designProgress2 && state.designProgress2.designId === design.id) {
|
||||||
|
set((state) => ({
|
||||||
|
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
||||||
|
designProgress2: null,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
set((state) => ({
|
||||||
|
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
||||||
|
designProgress: null,
|
||||||
|
currentAction: 'meditate' as const,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDesign(
|
||||||
|
designId: string,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
set((state) => ({
|
||||||
|
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// ─── Disenchanting Actions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { GameState, EquipmentInstance } from '../types';
|
||||||
|
|
||||||
|
export function disenchantEquipment(
|
||||||
|
instanceId: string,
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
const state = get();
|
||||||
|
const instance = state.equipmentInstances[instanceId];
|
||||||
|
if (!instance || instance.enchantments.length === 0) return;
|
||||||
|
|
||||||
|
const disenchantLevel = 0;
|
||||||
|
const recoveryRate = 0.1 + disenchantLevel * 0.2;
|
||||||
|
|
||||||
|
let totalRecovered = 0;
|
||||||
|
for (const ench of instance.enchantments) {
|
||||||
|
totalRecovered += Math.floor(ench.actualCost * recoveryRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
rawMana: state.rawMana + totalRecovered,
|
||||||
|
equipmentInstances: {
|
||||||
|
...state.equipmentInstances,
|
||||||
|
[instanceId]: {
|
||||||
|
...instance,
|
||||||
|
enchantments: [],
|
||||||
|
usedCapacity: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
log: [`✨ Disenchanted ${instance.name}, recovered ${totalRecovered} mana.`, ...state.log.slice(0, 49)],
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// ─── Equipment Management Actions ────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { GameState, EquipmentInstance, EquipmentSlot } from '../types';
|
||||||
|
import * as CraftingUtils from '../crafting-utils';
|
||||||
|
|
||||||
|
// Create equipment instance
|
||||||
|
export function createEquipmentInstance(
|
||||||
|
typeId: string,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
): string | null {
|
||||||
|
const type = CraftingUtils.getEquipmentType(typeId);
|
||||||
|
if (!type) return null;
|
||||||
|
|
||||||
|
const instanceId = CraftingUtils.generateInstanceId();
|
||||||
|
const instance: EquipmentInstance = {
|
||||||
|
instanceId,
|
||||||
|
typeId,
|
||||||
|
name: type.name,
|
||||||
|
enchantments: [],
|
||||||
|
usedCapacity: 0,
|
||||||
|
totalCapacity: type.baseCapacity,
|
||||||
|
rarity: 'common',
|
||||||
|
quality: 100,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
equipmentInstances: {
|
||||||
|
...state.equipmentInstances,
|
||||||
|
[instanceId]: instance,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equip item
|
||||||
|
export function equipItem(
|
||||||
|
instanceId: string,
|
||||||
|
slot: EquipmentSlot,
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
): boolean {
|
||||||
|
const state = get();
|
||||||
|
const instance = state.equipmentInstances[instanceId];
|
||||||
|
if (!instance) return false;
|
||||||
|
|
||||||
|
if (!CraftingUtils.canEquipInSlot(instance, slot, state.equippedInstances)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newEquipped = { ...state.equippedInstances };
|
||||||
|
for (const [s, id] of Object.entries(newEquipped)) {
|
||||||
|
if (id === instanceId) {
|
||||||
|
newEquipped[s as EquipmentSlot] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newEquipped[slot] = instanceId;
|
||||||
|
|
||||||
|
if (CraftingUtils.isTwoHanded(instance.typeId) && slot === 'mainHand') {
|
||||||
|
newEquipped.offHand = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(() => ({ equippedInstances: newEquipped }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unequip item
|
||||||
|
export function unequipItem(
|
||||||
|
slot: EquipmentSlot,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
set((state) => ({
|
||||||
|
equippedInstances: {
|
||||||
|
...state.equippedInstances,
|
||||||
|
[slot]: null,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete equipment instance
|
||||||
|
export function deleteEquipmentInstance(
|
||||||
|
instanceId: string,
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
const state = get();
|
||||||
|
let newEquipped = { ...state.equippedInstances };
|
||||||
|
for (const [slot, id] of Object.entries(newEquipped)) {
|
||||||
|
if (id === instanceId) {
|
||||||
|
newEquipped[slot as EquipmentSlot] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newInstances = { ...state.equipmentInstances };
|
||||||
|
delete newInstances[instanceId];
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
equippedInstances: newEquipped,
|
||||||
|
equipmentInstances: newInstances,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// ─── Crafting Action Implementations ──────────────────────────────────────────
|
||||||
|
// Modular structure for crafting actions
|
||||||
|
// Re-exports from the split modules
|
||||||
|
|
||||||
|
export { createEquipmentInstance, equipItem, unequipItem, deleteEquipmentInstance } from './equipment-actions';
|
||||||
|
export { startDesigningEnchantment, cancelDesign, saveDesign, deleteDesign } from './design-actions';
|
||||||
|
export { startPreparing, cancelPreparation } from './preparation-actions';
|
||||||
|
export { startApplying, pauseApplication, resumeApplication, cancelApplication } from './application-actions';
|
||||||
|
export { disenchantEquipment } from './disenchant-actions';
|
||||||
|
export { startCraftingEquipment, cancelEquipmentCrafting, deleteMaterial } from './crafting-equipment-actions';
|
||||||
|
export { getEquipmentSpells, getEquipmentEffects, getAvailableCapacity } from './computed-getters';
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// ─── Enchantment Preparation Actions ────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { GameState } from '../types';
|
||||||
|
import * as CraftingPrep from '../crafting-prep';
|
||||||
|
|
||||||
|
export function startPreparing(
|
||||||
|
equipmentInstanceId: string,
|
||||||
|
get: () => GameState,
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
): boolean {
|
||||||
|
const state = get();
|
||||||
|
const instance = state.equipmentInstances[equipmentInstanceId];
|
||||||
|
|
||||||
|
const validation = CraftingPrep.canPrepareEquipment(
|
||||||
|
instance,
|
||||||
|
instance?.tags || []
|
||||||
|
);
|
||||||
|
if (!validation.canPrepare) return false;
|
||||||
|
|
||||||
|
if (!instance) return false;
|
||||||
|
|
||||||
|
const costs = CraftingPrep.calculatePreparationCosts(instance.totalCapacity);
|
||||||
|
|
||||||
|
if (state.rawMana < costs.manaTotal) return false;
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
currentAction: 'prepare' as const,
|
||||||
|
preparationProgress: CraftingPrep.initializePreparationProgress(
|
||||||
|
equipmentInstanceId,
|
||||||
|
instance.totalCapacity
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelPreparation(
|
||||||
|
set: (fn: (state: GameState) => Partial<GameState>) => void
|
||||||
|
) {
|
||||||
|
set(() => ({
|
||||||
|
currentAction: 'meditate' as const,
|
||||||
|
preparationProgress: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
// ─── Spell Enchantment Effects ────────────────────────────────────────────────
|
|
||||||
// All spell-related enchantment effects that can be applied to equipment
|
|
||||||
|
|
||||||
import type { EquipmentCategory } from '../equipment'
|
|
||||||
import type { EnchantmentEffectDef } from '../enchantment-types'
|
|
||||||
|
|
||||||
// Helper to define allowed equipment categories for each effect type
|
|
||||||
const ALL_CASTER: EquipmentCategory[] = ['caster']
|
|
||||||
|
|
||||||
export const SPELL_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// SPELL EFFECTS - Only for CASTER equipment (staves, wands, rods, orbs)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Tier 0 - Basic Spells
|
|
||||||
spell_manaBolt: {
|
|
||||||
id: 'spell_manaBolt',
|
|
||||||
name: 'Mana Bolt',
|
|
||||||
description: 'Grants the ability to cast Mana Bolt (5 base damage, raw mana cost)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 50,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'manaBolt' }
|
|
||||||
},
|
|
||||||
spell_manaStrike: {
|
|
||||||
id: 'spell_manaStrike',
|
|
||||||
name: 'Mana Strike',
|
|
||||||
description: 'Grants the ability to cast Mana Strike (8 base damage, raw mana cost)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 40,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'manaStrike' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 1 - Basic Elemental Spells
|
|
||||||
spell_fireball: {
|
|
||||||
id: 'spell_fireball',
|
|
||||||
name: 'Fireball',
|
|
||||||
description: 'Grants the ability to cast Fireball (15 fire damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 80,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'fireball' }
|
|
||||||
},
|
|
||||||
spell_emberShot: {
|
|
||||||
id: 'spell_emberShot',
|
|
||||||
name: 'Ember Shot',
|
|
||||||
description: 'Grants the ability to cast Ember Shot (10 fire damage, fast cast)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 60,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'emberShot' }
|
|
||||||
},
|
|
||||||
spell_waterJet: {
|
|
||||||
id: 'spell_waterJet',
|
|
||||||
name: 'Water Jet',
|
|
||||||
description: 'Grants the ability to cast Water Jet (12 water damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 70,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'waterJet' }
|
|
||||||
},
|
|
||||||
spell_iceShard: {
|
|
||||||
id: 'spell_iceShard',
|
|
||||||
name: 'Ice Shard',
|
|
||||||
description: 'Grants the ability to cast Ice Shard (14 water damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 75,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'iceShard' }
|
|
||||||
},
|
|
||||||
spell_gust: {
|
|
||||||
id: 'spell_gust',
|
|
||||||
name: 'Gust',
|
|
||||||
description: 'Grants the ability to cast Gust (10 air damage, fast cast)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 60,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'gust' }
|
|
||||||
},
|
|
||||||
spell_stoneBullet: {
|
|
||||||
id: 'spell_stoneBullet',
|
|
||||||
name: 'Stone Bullet',
|
|
||||||
description: 'Grants the ability to cast Stone Bullet (16 earth damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 80,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'stoneBullet' }
|
|
||||||
},
|
|
||||||
spell_lightLance: {
|
|
||||||
id: 'spell_lightLance',
|
|
||||||
name: 'Light Lance',
|
|
||||||
description: 'Grants the ability to cast Light Lance (18 light damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 95,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'lightLance' }
|
|
||||||
},
|
|
||||||
spell_shadowBolt: {
|
|
||||||
id: 'spell_shadowBolt',
|
|
||||||
name: 'Shadow Bolt',
|
|
||||||
description: 'Grants the ability to cast Shadow Bolt (16 dark damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 95,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'shadowBolt' }
|
|
||||||
},
|
|
||||||
spell_drain: {
|
|
||||||
id: 'spell_drain',
|
|
||||||
name: 'Drain',
|
|
||||||
description: 'Grants the ability to cast Drain (10 death damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 85,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'drain' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 2 - Advanced Spells
|
|
||||||
spell_inferno: {
|
|
||||||
id: 'spell_inferno',
|
|
||||||
name: 'Inferno',
|
|
||||||
description: 'Grants the ability to cast Inferno (60 fire damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 180,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'inferno' }
|
|
||||||
},
|
|
||||||
spell_tidalWave: {
|
|
||||||
id: 'spell_tidalWave',
|
|
||||||
name: 'Tidal Wave',
|
|
||||||
description: 'Grants the ability to cast Tidal Wave (55 water damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 175,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'tidalWave' }
|
|
||||||
},
|
|
||||||
spell_hurricane: {
|
|
||||||
id: 'spell_hurricane',
|
|
||||||
name: 'Hurricane',
|
|
||||||
description: 'Grants the ability to cast Hurricane (50 air damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 170,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'hurricane' }
|
|
||||||
},
|
|
||||||
spell_earthquake: {
|
|
||||||
id: 'spell_earthquake',
|
|
||||||
name: 'Earthquake',
|
|
||||||
description: 'Grants the ability to cast Earthquake (70 earth damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 200,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'earthquake' }
|
|
||||||
},
|
|
||||||
spell_solarFlare: {
|
|
||||||
id: 'spell_solarFlare',
|
|
||||||
name: 'Solar Flare',
|
|
||||||
description: 'Grants the ability to cast Solar Flare (65 light damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 190,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'solarFlare' }
|
|
||||||
},
|
|
||||||
spell_voidRift: {
|
|
||||||
id: 'spell_voidRift',
|
|
||||||
name: 'Void Rift',
|
|
||||||
description: 'Grants the ability to cast Void Rift (55 dark damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 175,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'voidRift' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// Additional Tier 1 Spells
|
|
||||||
spell_windSlash: {
|
|
||||||
id: 'spell_windSlash',
|
|
||||||
name: 'Wind Slash',
|
|
||||||
description: 'Grants the ability to cast Wind Slash (12 air damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 72,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'windSlash' }
|
|
||||||
},
|
|
||||||
spell_rockSpike: {
|
|
||||||
id: 'spell_rockSpike',
|
|
||||||
name: 'Rock Spike',
|
|
||||||
description: 'Grants the ability to cast Rock Spike (18 earth damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 88,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'rockSpike' }
|
|
||||||
},
|
|
||||||
spell_radiance: {
|
|
||||||
id: 'spell_radiance',
|
|
||||||
name: 'Radiance',
|
|
||||||
description: 'Grants the ability to cast Radiance (14 light damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 80,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'radiance' }
|
|
||||||
},
|
|
||||||
spell_darkPulse: {
|
|
||||||
id: 'spell_darkPulse',
|
|
||||||
name: 'Dark Pulse',
|
|
||||||
description: 'Grants the ability to cast Dark Pulse (12 dark damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 68,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'darkPulse' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// Additional Tier 2 Spells
|
|
||||||
spell_flameWave: {
|
|
||||||
id: 'spell_flameWave',
|
|
||||||
name: 'Flame Wave',
|
|
||||||
description: 'Grants the ability to cast Flame Wave (45 fire damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 165,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'flameWave' }
|
|
||||||
},
|
|
||||||
spell_iceStorm: {
|
|
||||||
id: 'spell_iceStorm',
|
|
||||||
name: 'Ice Storm',
|
|
||||||
description: 'Grants the ability to cast Ice Storm (50 water damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 170,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'iceStorm' }
|
|
||||||
},
|
|
||||||
spell_windBlade: {
|
|
||||||
id: 'spell_windBlade',
|
|
||||||
name: 'Wind Blade',
|
|
||||||
description: 'Grants the ability to cast Wind Blade (40 air damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 155,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'windBlade' }
|
|
||||||
},
|
|
||||||
spell_stoneBarrage: {
|
|
||||||
id: 'spell_stoneBarrage',
|
|
||||||
name: 'Stone Barrage',
|
|
||||||
description: 'Grants the ability to cast Stone Barrage (55 earth damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 175,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'stoneBarrage' }
|
|
||||||
},
|
|
||||||
spell_divineSmite: {
|
|
||||||
id: 'spell_divineSmite',
|
|
||||||
name: 'Divine Smite',
|
|
||||||
description: 'Grants the ability to cast Divine Smite (55 light damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 175,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'divineSmite' }
|
|
||||||
},
|
|
||||||
spell_shadowStorm: {
|
|
||||||
id: 'spell_shadowStorm',
|
|
||||||
name: 'Shadow Storm',
|
|
||||||
description: 'Grants the ability to cast Shadow Storm (48 dark damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 168,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'shadowStorm' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// Tier 3 - Master Spells
|
|
||||||
spell_pyroclasm: {
|
|
||||||
id: 'spell_pyroclasm',
|
|
||||||
name: 'Pyroclasm',
|
|
||||||
description: 'Grants the ability to cast Pyroclasm (250 fire damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 400,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'pyroclasm' }
|
|
||||||
},
|
|
||||||
spell_tsunami: {
|
|
||||||
id: 'spell_tsunami',
|
|
||||||
name: 'Tsunami',
|
|
||||||
description: 'Grants the ability to cast Tsunami (220 water damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 380,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'tsunami' }
|
|
||||||
},
|
|
||||||
spell_meteorStrike: {
|
|
||||||
id: 'spell_meteorStrike',
|
|
||||||
name: 'Meteor Strike',
|
|
||||||
description: 'Grants the ability to cast Meteor Strike (280 earth damage)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 420,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'meteorStrike' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// LIGHTNING SPELL EFFECTS - Fast, armor-piercing, harder to dodge
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
spell_spark: {
|
|
||||||
id: 'spell_spark',
|
|
||||||
name: 'Spark',
|
|
||||||
description: 'Grants the ability to cast Spark (8 lightning damage, very fast, armor pierce)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 70,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'spark' }
|
|
||||||
},
|
|
||||||
spell_lightningBolt: {
|
|
||||||
id: 'spell_lightningBolt',
|
|
||||||
name: 'Lightning Bolt',
|
|
||||||
description: 'Grants the ability to cast Lightning Bolt (14 lightning damage, armor pierce)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 90,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'lightningBolt' }
|
|
||||||
},
|
|
||||||
spell_chainLightning: {
|
|
||||||
id: 'spell_chainLightning',
|
|
||||||
name: 'Chain Lightning',
|
|
||||||
description: 'Grants the ability to cast Chain Lightning (25 lightning damage, hits 3 targets)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 160,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'chainLightning' }
|
|
||||||
},
|
|
||||||
spell_stormCall: {
|
|
||||||
id: 'spell_stormCall',
|
|
||||||
name: 'Storm Call',
|
|
||||||
description: 'Grants the ability to cast Storm Call (40 lightning damage, hits 2 targets)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 190,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'stormCall' }
|
|
||||||
},
|
|
||||||
spell_thunderStrike: {
|
|
||||||
id: 'spell_thunderStrike',
|
|
||||||
name: 'Thunder Strike',
|
|
||||||
description: 'Grants the ability to cast Thunder Strike (150 lightning damage, 50% armor pierce)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 350,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'thunderStrike' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// METAL SPELL EFFECTS - Fire + Earth compound, armor pierce focus
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
spell_metalShard: {
|
|
||||||
id: 'spell_metalShard',
|
|
||||||
name: 'Metal Shard',
|
|
||||||
description: 'Grants the ability to cast Metal Shard (16 metal damage, 25% armor pierce)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 85,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'metalShard' }
|
|
||||||
},
|
|
||||||
spell_ironFist: {
|
|
||||||
id: 'spell_ironFist',
|
|
||||||
name: 'Iron Fist',
|
|
||||||
description: 'Grants the ability to cast Iron Fist (28 metal damage, 35% armor pierce)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 120,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'ironFist' }
|
|
||||||
},
|
|
||||||
spell_steelTempest: {
|
|
||||||
id: 'spell_steelTempest',
|
|
||||||
name: 'Steel Tempest',
|
|
||||||
description: 'Grants the ability to cast Steel Tempest (55 metal damage, 45% armor pierce)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 190,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'steelTempest' }
|
|
||||||
},
|
|
||||||
spell_furnaceBlast: {
|
|
||||||
id: 'spell_furnaceBlast',
|
|
||||||
name: 'Furnace Blast',
|
|
||||||
description: 'Grants the ability to cast Furnace Blast (200 metal damage, 60% armor pierce)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 400,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'furnaceBlast' }
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// SAND SPELL EFFECTS - Earth + Water compound, AOE focus
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
spell_sandBlast: {
|
|
||||||
id: 'spell_sandBlast',
|
|
||||||
name: 'Sand Blast',
|
|
||||||
description: 'Grants the ability to cast Sand Blast (11 sand damage, very fast)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 72,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'sandBlast' }
|
|
||||||
},
|
|
||||||
spell_sandstorm: {
|
|
||||||
id: 'spell_sandstorm',
|
|
||||||
name: 'Sandstorm',
|
|
||||||
description: 'Grants the ability to cast Sandstorm (22 sand damage, hits 2 enemies)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 100,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'sandstorm' }
|
|
||||||
},
|
|
||||||
spell_desertWind: {
|
|
||||||
id: 'spell_desertWind',
|
|
||||||
name: 'Desert Wind',
|
|
||||||
description: 'Grants the ability to cast Desert Wind (38 sand damage, hits 3 enemies)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 155,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'desertWind' }
|
|
||||||
},
|
|
||||||
spell_duneCollapse: {
|
|
||||||
id: 'spell_duneCollapse',
|
|
||||||
name: 'Dune Collapse',
|
|
||||||
description: 'Grants the ability to cast Dune Collapse (100 sand damage, hits 5 enemies)',
|
|
||||||
category: 'spell',
|
|
||||||
baseCapacityCost: 300,
|
|
||||||
maxStacks: 1,
|
|
||||||
allowedEquipmentCategories: ALL_CASTER,
|
|
||||||
effect: { type: 'spell', spellId: 'duneCollapse' }
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// ─── Tier 0 & 1 Basic Spells ───────────────────────────────────
|
||||||
|
|
||||||
|
import type { EnchantmentEffectDef } from './types';
|
||||||
|
import { ALL_CASTER } from './types';
|
||||||
|
|
||||||
|
export const BASIC_SPELL_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
||||||
|
// Tier 0 - Basic Spells
|
||||||
|
spell_manaBolt: {
|
||||||
|
id: 'spell_manaBolt',
|
||||||
|
name: 'Mana Bolt',
|
||||||
|
description: 'Grants the ability to cast Mana Bolt (5 base damage, raw mana cost)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 50,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'manaBolt' }
|
||||||
|
},
|
||||||
|
spell_manaStrike: {
|
||||||
|
id: 'spell_manaStrike',
|
||||||
|
name: 'Mana Strike',
|
||||||
|
description: 'Grants the ability to cast Mana Strike (8 base damage, raw mana cost)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 40,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'manaStrike' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tier 1 - Basic Elemental Spells
|
||||||
|
spell_fireball: {
|
||||||
|
id: 'spell_fireball',
|
||||||
|
name: 'Fireball',
|
||||||
|
description: 'Grants the ability to cast Fireball (15 fire damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 80,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'fireball' }
|
||||||
|
},
|
||||||
|
spell_emberShot: {
|
||||||
|
id: 'spell_emberShot',
|
||||||
|
name: 'Ember Shot',
|
||||||
|
description: 'Grants the ability to cast Ember Shot (10 fire damage, fast cast)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 60,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'emberShot' }
|
||||||
|
},
|
||||||
|
spell_waterJet: {
|
||||||
|
id: 'spell_waterJet',
|
||||||
|
name: 'Water Jet',
|
||||||
|
description: 'Grants the ability to cast Water Jet (12 water damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 70,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'waterJet' }
|
||||||
|
},
|
||||||
|
spell_iceShard: {
|
||||||
|
id: 'spell_iceShard',
|
||||||
|
name: 'Ice Shard',
|
||||||
|
description: 'Grants the ability to cast Ice Shard (14 water damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 75,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'iceShard' }
|
||||||
|
},
|
||||||
|
spell_gust: {
|
||||||
|
id: 'spell_gust',
|
||||||
|
name: 'Gust',
|
||||||
|
description: 'Grants the ability to cast Gust (10 air damage, fast cast)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 60,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'gust' }
|
||||||
|
},
|
||||||
|
spell_stoneBullet: {
|
||||||
|
id: 'spell_stoneBullet',
|
||||||
|
name: 'Stone Bullet',
|
||||||
|
description: 'Grants the ability to cast Stone Bullet (16 earth damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 80,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'stoneBullet' }
|
||||||
|
},
|
||||||
|
spell_lightLance: {
|
||||||
|
id: 'spell_lightLance',
|
||||||
|
name: 'Light Lance',
|
||||||
|
description: 'Grants the ability to cast Light Lance (18 light damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 95,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'lightLance' }
|
||||||
|
},
|
||||||
|
spell_shadowBolt: {
|
||||||
|
id: 'spell_shadowBolt',
|
||||||
|
name: 'Shadow Bolt',
|
||||||
|
description: 'Grants the ability to cast Shadow Bolt (16 dark damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 95,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'shadowBolt' }
|
||||||
|
},
|
||||||
|
spell_drain: {
|
||||||
|
id: 'spell_drain',
|
||||||
|
name: 'Drain',
|
||||||
|
description: 'Grants the ability to cast Drain (10 death damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 85,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'drain' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Additional Tier 1 Spells
|
||||||
|
spell_windSlash: {
|
||||||
|
id: 'spell_windSlash',
|
||||||
|
name: 'Wind Slash',
|
||||||
|
description: 'Grants the ability to cast Wind Slash (12 air damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 72,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'windSlash' }
|
||||||
|
},
|
||||||
|
spell_rockSpike: {
|
||||||
|
id: 'spell_rockSpike',
|
||||||
|
name: 'Rock Spike',
|
||||||
|
description: 'Grants the ability to cast Rock Spike (18 earth damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 88,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'rockSpike' }
|
||||||
|
},
|
||||||
|
spell_radiance: {
|
||||||
|
id: 'spell_radiance',
|
||||||
|
name: 'Radiance',
|
||||||
|
description: 'Grants the ability to cast Radiance (14 light damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 80,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'radiance' }
|
||||||
|
},
|
||||||
|
spell_darkPulse: {
|
||||||
|
id: 'spell_darkPulse',
|
||||||
|
name: 'Dark Pulse',
|
||||||
|
description: 'Grants the ability to cast Dark Pulse (12 dark damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 68,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'darkPulse' }
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// ─── Spell Enchantment Effects Index ───────────────────────────────
|
||||||
|
// Re-exports all spell effects from modular files
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export type { EnchantmentEffectDef, ALL_CASTER } from './types';
|
||||||
|
|
||||||
|
// Re-export data
|
||||||
|
export { SPELL_EFFECTS } from './data';
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// ─── Lightning Spell Effects ──────────────────────────────────
|
||||||
|
// Lightning spells - Fast, armor-piercing, harder to dodge
|
||||||
|
|
||||||
|
import type { EnchantmentEffectDef } from './types';
|
||||||
|
import { ALL_CASTER } from './types';
|
||||||
|
|
||||||
|
export const LIGHTNING_SPELL_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
||||||
|
spell_spark: {
|
||||||
|
id: 'spell_spark',
|
||||||
|
name: 'Spark',
|
||||||
|
description: 'Grants the ability to cast Spark (8 lightning damage, very fast, armor pierce)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 70,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'spark' }
|
||||||
|
},
|
||||||
|
spell_lightningBolt: {
|
||||||
|
id: 'spell_lightningBolt',
|
||||||
|
name: 'Lightning Bolt',
|
||||||
|
description: 'Grants the ability to cast Lightning Bolt (14 lightning damage, armor pierce)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 90,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'lightningBolt' }
|
||||||
|
},
|
||||||
|
spell_chainLightning: {
|
||||||
|
id: 'spell_chainLightning',
|
||||||
|
name: 'Chain Lightning',
|
||||||
|
description: 'Grants the ability to cast Chain Lightning (25 lightning damage, hits 3 targets)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 160,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'chainLightning' }
|
||||||
|
},
|
||||||
|
spell_stormCall: {
|
||||||
|
id: 'spell_stormCall',
|
||||||
|
name: 'Storm Call',
|
||||||
|
description: 'Grants the ability to cast Storm Call (40 lightning damage, hits 2 targets)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 190,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'stormCall' }
|
||||||
|
},
|
||||||
|
spell_thunderStrike: {
|
||||||
|
id: 'spell_thunderStrike',
|
||||||
|
name: 'Thunder Strike',
|
||||||
|
description: 'Grants the ability to cast Thunder Strike (150 lightning damage, 50% armor pierce)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 350,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'thunderStrike' }
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// ─── Metal Spell Effects ──────────────────────────────────────
|
||||||
|
// Metal spells - Fire + Earth compound, armor pierce focus
|
||||||
|
|
||||||
|
import type { EnchantmentEffectDef } from './types';
|
||||||
|
import { ALL_CASTER } from './types';
|
||||||
|
|
||||||
|
export const METAL_SPELL_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
||||||
|
spell_metalShard: {
|
||||||
|
id: 'spell_metalShard',
|
||||||
|
name: 'Metal Shard',
|
||||||
|
description: 'Grants the ability to cast Metal Shard (16 metal damage, 25% armor pierce)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 85,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'metalShard' }
|
||||||
|
},
|
||||||
|
spell_ironFist: {
|
||||||
|
id: 'spell_ironFist',
|
||||||
|
name: 'Iron Fist',
|
||||||
|
description: 'Grants the ability to cast Iron Fist (28 metal damage, 35% armor pierce)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 120,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'ironFist' }
|
||||||
|
},
|
||||||
|
spell_steelTempest: {
|
||||||
|
id: 'spell_steelTempest',
|
||||||
|
name: 'Steel Tempest',
|
||||||
|
description: 'Grants the ability to cast Steel Tempest (55 metal damage, 45% armor pierce)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 190,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'steelTempest' }
|
||||||
|
},
|
||||||
|
spell_furnaceBlast: {
|
||||||
|
id: 'spell_furnaceBlast',
|
||||||
|
name: 'Furnace Blast',
|
||||||
|
description: 'Grants the ability to cast Furnace Blast (200 metal damage, 60% armor pierce)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 400,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'furnaceBlast' }
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// ─── Sand Spell Effects ───────────────────────────────────────
|
||||||
|
// Sand spells - Earth + Water compound, AOE focus
|
||||||
|
|
||||||
|
import type { EnchantmentEffectDef } from './types';
|
||||||
|
import { ALL_CASTER } from './types';
|
||||||
|
|
||||||
|
export const SAND_SPELL_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
||||||
|
spell_sandBlast: {
|
||||||
|
id: 'spell_sandBlast',
|
||||||
|
name: 'Sand Blast',
|
||||||
|
description: 'Grants the ability to cast Sand Blast (11 sand damage, very fast)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 72,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'sandBlast' }
|
||||||
|
},
|
||||||
|
spell_sandstorm: {
|
||||||
|
id: 'spell_sandstorm',
|
||||||
|
name: 'Sandstorm',
|
||||||
|
description: 'Grants the ability to cast Sandstorm (22 sand damage, hits 2 enemies)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 100,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'sandstorm' }
|
||||||
|
},
|
||||||
|
spell_desertWind: {
|
||||||
|
id: 'spell_desertWind',
|
||||||
|
name: 'Desert Wind',
|
||||||
|
description: 'Grants the ability to cast Desert Wind (38 sand damage, hits 3 enemies)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 155,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'desertWind' }
|
||||||
|
},
|
||||||
|
spell_duneCollapse: {
|
||||||
|
id: 'spell_duneCollapse',
|
||||||
|
name: 'Dune Collapse',
|
||||||
|
description: 'Grants the ability to cast Dune Collapse (100 sand damage, hits 5 enemies)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 300,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'duneCollapse' }
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
// ─── Tier 2 Advanced Spells ───────────────────────────────────
|
||||||
|
|
||||||
|
import type { EnchantmentEffectDef } from './types';
|
||||||
|
import { ALL_CASTER } from './types';
|
||||||
|
|
||||||
|
export const TIER2_SPELL_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
||||||
|
spell_inferno: {
|
||||||
|
id: 'spell_inferno',
|
||||||
|
name: 'Inferno',
|
||||||
|
description: 'Grants the ability to cast Inferno (60 fire damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 180,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'inferno' }
|
||||||
|
},
|
||||||
|
spell_tidalWave: {
|
||||||
|
id: 'spell_tidalWave',
|
||||||
|
name: 'Tidal Wave',
|
||||||
|
description: 'Grants the ability to cast Tidal Wave (55 water damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 175,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'tidalWave' }
|
||||||
|
},
|
||||||
|
spell_hurricane: {
|
||||||
|
id: 'spell_hurricane',
|
||||||
|
name: 'Hurricane',
|
||||||
|
description: 'Grants the ability to cast Hurricane (50 air damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 170,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'hurricane' }
|
||||||
|
},
|
||||||
|
spell_earthquake: {
|
||||||
|
id: 'spell_earthquake',
|
||||||
|
name: 'Earthquake',
|
||||||
|
description: 'Grants the ability to cast Earthquake (70 earth damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 200,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'earthquake' }
|
||||||
|
},
|
||||||
|
spell_solarFlare: {
|
||||||
|
id: 'spell_solarFlare',
|
||||||
|
name: 'Solar Flare',
|
||||||
|
description: 'Grants the ability to cast Solar Flare (65 light damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 190,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'solarFlare' }
|
||||||
|
},
|
||||||
|
spell_voidRift: {
|
||||||
|
id: 'spell_voidRift',
|
||||||
|
name: 'Void Rift',
|
||||||
|
description: 'Grants the ability to cast Void Rift (55 dark damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 175,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'voidRift' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Additional Tier 2 Spells
|
||||||
|
spell_flameWave: {
|
||||||
|
id: 'spell_flameWave',
|
||||||
|
name: 'Flame Wave',
|
||||||
|
description: 'Grants the ability to cast Flame Wave (45 fire damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 165,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'flameWave' }
|
||||||
|
},
|
||||||
|
spell_iceStorm: {
|
||||||
|
id: 'spell_iceStorm',
|
||||||
|
name: 'Ice Storm',
|
||||||
|
description: 'Grants the ability to cast Ice Storm (50 water damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 170,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'iceStorm' }
|
||||||
|
},
|
||||||
|
spell_windBlade: {
|
||||||
|
id: 'spell_windBlade',
|
||||||
|
name: 'Wind Blade',
|
||||||
|
description: 'Grants the ability to cast Wind Blade (40 air damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 155,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'windBlade' }
|
||||||
|
},
|
||||||
|
spell_stoneBarrage: {
|
||||||
|
id: 'spell_stoneBarrage',
|
||||||
|
name: 'Stone Barrage',
|
||||||
|
description: 'Grants the ability to cast Stone Barrage (55 earth damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 175,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'stoneBarrage' }
|
||||||
|
},
|
||||||
|
spell_divineSmite: {
|
||||||
|
id: 'spell_divineSmite',
|
||||||
|
name: 'Divine Smite',
|
||||||
|
description: 'Grants the ability to cast Divine Smite (55 light damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 175,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'divineSmite' }
|
||||||
|
},
|
||||||
|
spell_shadowStorm: {
|
||||||
|
id: 'spell_shadowStorm',
|
||||||
|
name: 'Shadow Storm',
|
||||||
|
description: 'Grants the ability to cast Shadow Storm (48 dark damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 168,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'shadowStorm' }
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// ─── Tier 3 Master Spells ─────────────────────────────────────
|
||||||
|
|
||||||
|
import type { EnchantmentEffectDef } from './types';
|
||||||
|
import { ALL_CASTER } from './types';
|
||||||
|
|
||||||
|
export const TIER3_SPELL_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
||||||
|
spell_pyroclasm: {
|
||||||
|
id: 'spell_pyroclasm',
|
||||||
|
name: 'Pyroclasm',
|
||||||
|
description: 'Grants the ability to cast Pyroclasm (250 fire damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 400,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'pyroclasm' }
|
||||||
|
},
|
||||||
|
spell_tsunami: {
|
||||||
|
id: 'spell_tsunami',
|
||||||
|
name: 'Tsunami',
|
||||||
|
description: 'Grants the ability to cast Tsunami (220 water damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 380,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'tsunami' }
|
||||||
|
},
|
||||||
|
spell_meteorStrike: {
|
||||||
|
id: 'spell_meteorStrike',
|
||||||
|
name: 'Meteor Strike',
|
||||||
|
description: 'Grants the ability to cast Meteor Strike (280 earth damage)',
|
||||||
|
category: 'spell',
|
||||||
|
baseCapacityCost: 420,
|
||||||
|
maxStacks: 1,
|
||||||
|
allowedEquipmentCategories: ALL_CASTER,
|
||||||
|
effect: { type: 'spell', spellId: 'meteorStrike' }
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// ─── Spell Enchantment Effects Types ─────────────────
|
||||||
|
|
||||||
|
export interface EnchantmentEffectDef {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
baseCapacityCost: number;
|
||||||
|
maxStacks: number;
|
||||||
|
allowedEquipmentCategories: string[];
|
||||||
|
effect: {
|
||||||
|
type: string;
|
||||||
|
spellId?: string;
|
||||||
|
stat?: string;
|
||||||
|
value?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to define allowed equipment categories for each effect type
|
||||||
|
export const ALL_CASTER: string[] = ['caster']
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
// ─── Equipment Types ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export type EquipmentSlot = 'mainHand' | 'offHand' | 'head' | 'body' | 'hands' | 'feet' | 'accessory1' | 'accessory2';
|
|
||||||
export type EquipmentCategory = 'caster' | 'shield' | 'catalyst' | 'sword' | 'head' | 'body' | 'hands' | 'feet' | 'accessory';
|
|
||||||
|
|
||||||
// All equipment slots in order
|
|
||||||
export const EQUIPMENT_SLOTS: EquipmentSlot[] = ['mainHand', 'offHand', 'head', 'body', 'hands', 'feet', 'accessory1', 'accessory2'];
|
|
||||||
|
|
||||||
// Human-readable names for equipment slots
|
|
||||||
export const SLOT_NAMES: Record<EquipmentSlot, string> = {
|
|
||||||
mainHand: 'Main Hand',
|
|
||||||
offHand: 'Off Hand',
|
|
||||||
head: 'Head',
|
|
||||||
body: 'Body',
|
|
||||||
hands: 'Hands',
|
|
||||||
feet: 'Feet',
|
|
||||||
accessory1: 'Accessory 1',
|
|
||||||
accessory2: 'Accessory 2',
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface EquipmentType {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
category: EquipmentCategory;
|
|
||||||
slot: EquipmentSlot;
|
|
||||||
baseCapacity: number;
|
|
||||||
description: string;
|
|
||||||
baseDamage?: number; // For swords
|
|
||||||
baseCastSpeed?: number; // For swords (higher = faster)
|
|
||||||
twoHanded?: boolean; // If true, weapon occupies both main hand and offhand slots
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Equipment Types Definition ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const EQUIPMENT_TYPES: Record<string, EquipmentType> = {
|
|
||||||
// ─── Main Hand - Casters ─────────────────────────────────────────────────
|
|
||||||
basicStaff: {
|
|
||||||
id: 'basicStaff',
|
|
||||||
name: 'Basic Staff',
|
|
||||||
category: 'caster',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 50,
|
|
||||||
description: 'A simple wooden staff, basic but reliable for channeling mana.',
|
|
||||||
twoHanded: true,
|
|
||||||
},
|
|
||||||
apprenticeWand: {
|
|
||||||
id: 'apprenticeWand',
|
|
||||||
name: 'Apprentice Wand',
|
|
||||||
category: 'caster',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 35,
|
|
||||||
description: 'A lightweight wand favored by apprentices. Lower capacity but faster to prepare.',
|
|
||||||
},
|
|
||||||
oakStaff: {
|
|
||||||
id: 'oakStaff',
|
|
||||||
name: 'Oak Staff',
|
|
||||||
category: 'caster',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 65,
|
|
||||||
description: 'A sturdy oak staff with decent mana capacity.',
|
|
||||||
twoHanded: true,
|
|
||||||
},
|
|
||||||
crystalWand: {
|
|
||||||
id: 'crystalWand',
|
|
||||||
name: 'Crystal Wand',
|
|
||||||
category: 'caster',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 45,
|
|
||||||
description: 'A wand tipped with a small crystal. Excellent for elemental enchantments.',
|
|
||||||
},
|
|
||||||
arcanistStaff: {
|
|
||||||
id: 'arcanistStaff',
|
|
||||||
name: 'Arcanist Staff',
|
|
||||||
category: 'caster',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 80,
|
|
||||||
description: 'A staff designed for advanced spellcasters. High capacity for complex enchantments.',
|
|
||||||
twoHanded: true,
|
|
||||||
},
|
|
||||||
battlestaff: {
|
|
||||||
id: 'battlestaff',
|
|
||||||
name: 'Battlestaff',
|
|
||||||
category: 'caster',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 70,
|
|
||||||
description: 'A reinforced staff suitable for both casting and combat.',
|
|
||||||
twoHanded: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── Main Hand - Catalysts ────────────────────────────────────────────────
|
|
||||||
basicCatalyst: {
|
|
||||||
id: 'basicCatalyst',
|
|
||||||
name: 'Basic Catalyst',
|
|
||||||
category: 'catalyst',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 40,
|
|
||||||
description: 'A simple catalyst for amplifying magical effects.',
|
|
||||||
},
|
|
||||||
fireCatalyst: {
|
|
||||||
id: 'fireCatalyst',
|
|
||||||
name: 'Fire Catalyst',
|
|
||||||
category: 'catalyst',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 55,
|
|
||||||
description: 'A catalyst attuned to fire magic. Enhances fire enchantments.',
|
|
||||||
},
|
|
||||||
voidCatalyst: {
|
|
||||||
id: 'voidCatalyst',
|
|
||||||
name: 'Void Catalyst',
|
|
||||||
category: 'catalyst',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 75,
|
|
||||||
description: 'A rare catalyst touched by void energy. High capacity but volatile.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── Main Hand - Magic Swords ─────────────────────────────────────────────
|
|
||||||
// Magic swords have low base damage but high cast speed
|
|
||||||
// They can be enchanted with elemental effects that use mana over time
|
|
||||||
ironBlade: {
|
|
||||||
id: 'ironBlade',
|
|
||||||
name: 'Iron Blade',
|
|
||||||
category: 'sword',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 30,
|
|
||||||
baseDamage: 3,
|
|
||||||
baseCastSpeed: 4,
|
|
||||||
description: 'A simple iron sword. Can be enchanted with elemental effects.',
|
|
||||||
},
|
|
||||||
steelBlade: {
|
|
||||||
id: 'steelBlade',
|
|
||||||
name: 'Steel Blade',
|
|
||||||
category: 'sword',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 40,
|
|
||||||
baseDamage: 4,
|
|
||||||
baseCastSpeed: 4,
|
|
||||||
description: 'A well-crafted steel sword. Balanced for combat and enchanting.',
|
|
||||||
},
|
|
||||||
crystalBlade: {
|
|
||||||
id: 'crystalBlade',
|
|
||||||
name: 'Crystal Blade',
|
|
||||||
category: 'sword',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 55,
|
|
||||||
baseDamage: 3,
|
|
||||||
baseCastSpeed: 5,
|
|
||||||
description: 'A blade made of crystallized mana. Excellent for elemental enchantments.',
|
|
||||||
},
|
|
||||||
arcanistBlade: {
|
|
||||||
id: 'arcanistBlade',
|
|
||||||
name: 'Arcanist Blade',
|
|
||||||
category: 'sword',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 65,
|
|
||||||
baseDamage: 5,
|
|
||||||
baseCastSpeed: 4,
|
|
||||||
description: 'A sword forged for battle mages. High capacity for powerful enchantments.',
|
|
||||||
},
|
|
||||||
voidBlade: {
|
|
||||||
id: 'voidBlade',
|
|
||||||
name: 'Void-Touched Blade',
|
|
||||||
category: 'sword',
|
|
||||||
slot: 'mainHand',
|
|
||||||
baseCapacity: 50,
|
|
||||||
baseDamage: 6,
|
|
||||||
baseCastSpeed: 3,
|
|
||||||
description: 'A blade corrupted by void energy. Powerful but consumes more mana.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── Off Hand - Shields ───────────────────────────────────────────────────
|
|
||||||
basicShield: {
|
|
||||||
id: 'basicShield',
|
|
||||||
name: 'Basic Shield',
|
|
||||||
category: 'shield',
|
|
||||||
slot: 'offHand',
|
|
||||||
baseCapacity: 40,
|
|
||||||
description: 'A simple wooden shield. Provides basic protection.',
|
|
||||||
},
|
|
||||||
reinforcedShield: {
|
|
||||||
id: 'reinforcedShield',
|
|
||||||
name: 'Reinforced Shield',
|
|
||||||
category: 'shield',
|
|
||||||
slot: 'offHand',
|
|
||||||
baseCapacity: 55,
|
|
||||||
description: 'A metal-reinforced shield with enhanced durability and capacity.',
|
|
||||||
},
|
|
||||||
runicShield: {
|
|
||||||
id: 'runicShield',
|
|
||||||
name: 'Runic Shield',
|
|
||||||
category: 'shield',
|
|
||||||
slot: 'offHand',
|
|
||||||
baseCapacity: 70,
|
|
||||||
description: 'A shield engraved with protective runes. Excellent for defensive enchantments.',
|
|
||||||
},
|
|
||||||
manaShield: {
|
|
||||||
id: 'manaShield',
|
|
||||||
name: 'Mana Shield',
|
|
||||||
category: 'shield',
|
|
||||||
slot: 'offHand',
|
|
||||||
baseCapacity: 60,
|
|
||||||
description: 'A crystalline shield that can store and reflect mana.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── Head ─────────────────────────────────────────────────────────────────
|
|
||||||
clothHood: {
|
|
||||||
id: 'clothHood',
|
|
||||||
name: 'Cloth Hood',
|
|
||||||
category: 'head',
|
|
||||||
slot: 'head',
|
|
||||||
baseCapacity: 25,
|
|
||||||
description: 'A simple cloth hood. Minimal protection but comfortable.',
|
|
||||||
},
|
|
||||||
apprenticeCap: {
|
|
||||||
id: 'apprenticeCap',
|
|
||||||
name: 'Apprentice Cap',
|
|
||||||
category: 'head',
|
|
||||||
slot: 'head',
|
|
||||||
baseCapacity: 30,
|
|
||||||
description: 'The traditional cap of magic apprentices.',
|
|
||||||
},
|
|
||||||
wizardHat: {
|
|
||||||
id: 'wizardHat',
|
|
||||||
name: 'Wizard Hat',
|
|
||||||
category: 'head',
|
|
||||||
slot: 'head',
|
|
||||||
baseCapacity: 45,
|
|
||||||
description: 'A classic pointed wizard hat. Decent capacity for headwear.',
|
|
||||||
},
|
|
||||||
arcanistCirclet: {
|
|
||||||
id: 'arcanistCirclet',
|
|
||||||
name: 'Arcanist Circlet',
|
|
||||||
category: 'head',
|
|
||||||
slot: 'head',
|
|
||||||
baseCapacity: 40,
|
|
||||||
description: 'A silver circlet worn by accomplished arcanists.',
|
|
||||||
},
|
|
||||||
battleHelm: {
|
|
||||||
id: 'battleHelm',
|
|
||||||
name: 'Battle Helm',
|
|
||||||
category: 'head',
|
|
||||||
slot: 'head',
|
|
||||||
baseCapacity: 50,
|
|
||||||
description: 'A sturdy helm for battle mages.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── Body ────────────────────────────────────────────────────────────────
|
|
||||||
civilianShirt: {
|
|
||||||
id: 'civilianShirt',
|
|
||||||
name: 'Civilian Shirt',
|
|
||||||
category: 'body',
|
|
||||||
slot: 'body',
|
|
||||||
baseCapacity: 30,
|
|
||||||
description: 'A plain shirt with minimal magical properties.',
|
|
||||||
},
|
|
||||||
apprenticeRobe: {
|
|
||||||
id: 'apprenticeRobe',
|
|
||||||
name: 'Apprentice Robe',
|
|
||||||
category: 'body',
|
|
||||||
slot: 'body',
|
|
||||||
baseCapacity: 45,
|
|
||||||
description: 'The standard robe for magic apprentices.',
|
|
||||||
},
|
|
||||||
scholarRobe: {
|
|
||||||
id: 'scholarRobe',
|
|
||||||
name: 'Scholar Robe',
|
|
||||||
category: 'body',
|
|
||||||
slot: 'body',
|
|
||||||
baseCapacity: 55,
|
|
||||||
description: 'A robe worn by scholars and researchers.',
|
|
||||||
},
|
|
||||||
battleRobe: {
|
|
||||||
id: 'battleRobe',
|
|
||||||
name: 'Battle Robe',
|
|
||||||
category: 'body',
|
|
||||||
slot: 'body',
|
|
||||||
baseCapacity: 65,
|
|
||||||
description: 'A reinforced robe designed for combat mages.',
|
|
||||||
},
|
|
||||||
arcanistRobe: {
|
|
||||||
id: 'arcanistRobe',
|
|
||||||
name: 'Arcanist Robe',
|
|
||||||
category: 'body',
|
|
||||||
slot: 'body',
|
|
||||||
baseCapacity: 80,
|
|
||||||
description: 'An ornate robe for master arcanists. High capacity for body armor.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── Hands ───────────────────────────────────────────────────────────────
|
|
||||||
civilianGloves: {
|
|
||||||
id: 'civilianGloves',
|
|
||||||
name: 'Civilian Gloves',
|
|
||||||
category: 'hands',
|
|
||||||
slot: 'hands',
|
|
||||||
baseCapacity: 20,
|
|
||||||
description: 'Simple cloth gloves. Minimal magical capacity.',
|
|
||||||
},
|
|
||||||
apprenticeGloves: {
|
|
||||||
id: 'apprenticeGloves',
|
|
||||||
name: 'Apprentice Gloves',
|
|
||||||
category: 'hands',
|
|
||||||
slot: 'hands',
|
|
||||||
baseCapacity: 30,
|
|
||||||
description: 'Basic gloves for handling magical components.',
|
|
||||||
},
|
|
||||||
spellweaveGloves: {
|
|
||||||
id: 'spellweaveGloves',
|
|
||||||
name: 'Spellweave Gloves',
|
|
||||||
category: 'hands',
|
|
||||||
slot: 'hands',
|
|
||||||
baseCapacity: 40,
|
|
||||||
description: 'Gloves woven with mana-conductive threads.',
|
|
||||||
},
|
|
||||||
combatGauntlets: {
|
|
||||||
id: 'combatGauntlets',
|
|
||||||
name: 'Combat Gauntlets',
|
|
||||||
category: 'hands',
|
|
||||||
slot: 'hands',
|
|
||||||
baseCapacity: 35,
|
|
||||||
description: 'Armored gauntlets for battle mages.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── Feet ────────────────────────────────────────────────────────────────
|
|
||||||
civilianShoes: {
|
|
||||||
id: 'civilianShoes',
|
|
||||||
name: 'Civilian Shoes',
|
|
||||||
category: 'feet',
|
|
||||||
slot: 'feet',
|
|
||||||
baseCapacity: 15,
|
|
||||||
description: 'Simple leather shoes. No special properties.',
|
|
||||||
},
|
|
||||||
apprenticeBoots: {
|
|
||||||
id: 'apprenticeBoots',
|
|
||||||
name: 'Apprentice Boots',
|
|
||||||
category: 'feet',
|
|
||||||
slot: 'feet',
|
|
||||||
baseCapacity: 25,
|
|
||||||
description: 'Basic boots for magic students.',
|
|
||||||
},
|
|
||||||
travelerBoots: {
|
|
||||||
id: 'travelerBoots',
|
|
||||||
name: 'Traveler Boots',
|
|
||||||
category: 'feet',
|
|
||||||
slot: 'feet',
|
|
||||||
baseCapacity: 30,
|
|
||||||
description: 'Comfortable boots for long journeys.',
|
|
||||||
},
|
|
||||||
battleBoots: {
|
|
||||||
id: 'battleBoots',
|
|
||||||
name: 'Battle Boots',
|
|
||||||
category: 'feet',
|
|
||||||
slot: 'feet',
|
|
||||||
baseCapacity: 35,
|
|
||||||
description: 'Sturdy boots for combat situations.',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── Accessories ────────────────────────────────────────────────────────
|
|
||||||
copperRing: {
|
|
||||||
id: 'copperRing',
|
|
||||||
name: 'Copper Ring',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 15,
|
|
||||||
description: 'A simple copper ring. Basic capacity for accessories.',
|
|
||||||
},
|
|
||||||
silverRing: {
|
|
||||||
id: 'silverRing',
|
|
||||||
name: 'Silver Ring',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 25,
|
|
||||||
description: 'A silver ring with decent magical conductivity.',
|
|
||||||
},
|
|
||||||
goldRing: {
|
|
||||||
id: 'goldRing',
|
|
||||||
name: 'Gold Ring',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 35,
|
|
||||||
description: 'A gold ring with excellent magical properties.',
|
|
||||||
},
|
|
||||||
signetRing: {
|
|
||||||
id: 'signetRing',
|
|
||||||
name: 'Signet Ring',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 30,
|
|
||||||
description: 'A ring bearing a magical sigil.',
|
|
||||||
},
|
|
||||||
copperAmulet: {
|
|
||||||
id: 'copperAmulet',
|
|
||||||
name: 'Copper Amulet',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 20,
|
|
||||||
description: 'A simple copper amulet on a leather cord.',
|
|
||||||
},
|
|
||||||
silverAmulet: {
|
|
||||||
id: 'silverAmulet',
|
|
||||||
name: 'Silver Amulet',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 30,
|
|
||||||
description: 'A silver amulet with a small gem.',
|
|
||||||
},
|
|
||||||
crystalPendant: {
|
|
||||||
id: 'crystalPendant',
|
|
||||||
name: 'Crystal Pendant',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 45,
|
|
||||||
description: 'A pendant with a mana-infused crystal.',
|
|
||||||
},
|
|
||||||
manaBrooch: {
|
|
||||||
id: 'manaBrooch',
|
|
||||||
name: 'Mana Brooch',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 40,
|
|
||||||
description: 'A decorative brooch that can hold enchantments.',
|
|
||||||
},
|
|
||||||
arcanistPendant: {
|
|
||||||
id: 'arcanistPendant',
|
|
||||||
name: 'Arcanist Pendant',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 55,
|
|
||||||
description: 'A powerful pendant worn by master arcanists.',
|
|
||||||
},
|
|
||||||
voidTouchedRing: {
|
|
||||||
id: 'voidTouchedRing',
|
|
||||||
name: 'Void-Touched Ring',
|
|
||||||
category: 'accessory',
|
|
||||||
slot: 'accessory1',
|
|
||||||
baseCapacity: 50,
|
|
||||||
description: 'A ring corrupted by void energy. High capacity but risky.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Helper Functions ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function getEquipmentType(id: string): EquipmentType | undefined {
|
|
||||||
return EQUIPMENT_TYPES[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEquipmentByCategory(category: EquipmentCategory): EquipmentType[] {
|
|
||||||
return Object.values(EQUIPMENT_TYPES).filter(e => e.category === category);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEquipmentBySlot(slot: EquipmentSlot): EquipmentType[] {
|
|
||||||
return Object.values(EQUIPMENT_TYPES).filter(e => e.slot === slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAllEquipmentTypes(): EquipmentType[] {
|
|
||||||
return Object.values(EQUIPMENT_TYPES);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get valid slots for a category
|
|
||||||
// Note: For 2-handed weapons, use getValidSlotsForEquipmentType instead
|
|
||||||
export function getValidSlotsForCategory(category: EquipmentCategory): EquipmentSlot[] {
|
|
||||||
switch (category) {
|
|
||||||
case 'caster':
|
|
||||||
case 'catalyst':
|
|
||||||
case 'sword':
|
|
||||||
return ['mainHand'];
|
|
||||||
case 'shield':
|
|
||||||
return ['offHand'];
|
|
||||||
case 'head':
|
|
||||||
return ['head'];
|
|
||||||
case 'body':
|
|
||||||
return ['body'];
|
|
||||||
case 'hands':
|
|
||||||
return ['hands'];
|
|
||||||
case 'feet':
|
|
||||||
return ['feet'];
|
|
||||||
case 'accessory':
|
|
||||||
return ['accessory1', 'accessory2'];
|
|
||||||
default:
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get valid slots for a specific equipment type (considers 2-handed weapons)
|
|
||||||
export function getValidSlotsForEquipmentType(equipType: EquipmentType): EquipmentSlot[] {
|
|
||||||
// 2-handed weapons occupy both main hand and offhand
|
|
||||||
if (equipType.twoHanded) {
|
|
||||||
return ['mainHand', 'offHand'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise use category-based slots
|
|
||||||
return getValidSlotsForCategory(equipType.category);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if an equipment type can be equipped in a specific slot
|
|
||||||
export function canEquipInSlot(equipmentType: EquipmentType, slot: EquipmentSlot): boolean {
|
|
||||||
const validSlots = getValidSlotsForCategory(equipmentType.category);
|
|
||||||
return validSlots.includes(slot);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// ─── Accessories Equipment Types ──────────────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType } from './types';
|
||||||
|
|
||||||
|
export const ACCESSORIES_EQUIPMENT: Record<string, EquipmentType> = {
|
||||||
|
// ─── Accessories ────────────────────────────────────────────────
|
||||||
|
copperRing: {
|
||||||
|
id: 'copperRing',
|
||||||
|
name: 'Copper Ring',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 15,
|
||||||
|
description: 'A simple copper ring. Basic capacity for accessories.',
|
||||||
|
},
|
||||||
|
silverRing: {
|
||||||
|
id: 'silverRing',
|
||||||
|
name: 'Silver Ring',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 25,
|
||||||
|
description: 'A silver ring with decent magical conductivity.',
|
||||||
|
},
|
||||||
|
goldRing: {
|
||||||
|
id: 'goldRing',
|
||||||
|
name: 'Gold Ring',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 35,
|
||||||
|
description: 'A gold ring with excellent magical properties.',
|
||||||
|
},
|
||||||
|
signetRing: {
|
||||||
|
id: 'signetRing',
|
||||||
|
name: 'Signet Ring',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 30,
|
||||||
|
description: 'A ring bearing a magical sigil.',
|
||||||
|
},
|
||||||
|
copperAmulet: {
|
||||||
|
id: 'copperAmulet',
|
||||||
|
name: 'Copper Amulet',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 20,
|
||||||
|
description: 'A simple copper amulet on a leather cord.',
|
||||||
|
},
|
||||||
|
silverAmulet: {
|
||||||
|
id: 'silverAmulet',
|
||||||
|
name: 'Silver Amulet',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 30,
|
||||||
|
description: 'A silver amulet with a small gem.',
|
||||||
|
},
|
||||||
|
crystalPendant: {
|
||||||
|
id: 'crystalPendant',
|
||||||
|
name: 'Crystal Pendant',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 45,
|
||||||
|
description: 'A pendant with a mana-infused crystal.',
|
||||||
|
},
|
||||||
|
manaBrooch: {
|
||||||
|
id: 'manaBrooch',
|
||||||
|
name: 'Mana Brooch',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 40,
|
||||||
|
description: 'A decorative brooch that can hold enchantments.',
|
||||||
|
},
|
||||||
|
arcanistPendant: {
|
||||||
|
id: 'arcanistPendant',
|
||||||
|
name: 'Arcanist Pendant',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 55,
|
||||||
|
description: 'A powerful pendant worn by master arcanists.',
|
||||||
|
},
|
||||||
|
voidTouchedRing: {
|
||||||
|
id: 'voidTouchedRing',
|
||||||
|
name: 'Void-Touched Ring',
|
||||||
|
category: 'accessory',
|
||||||
|
slot: 'accessory1',
|
||||||
|
baseCapacity: 50,
|
||||||
|
description: 'A ring corrupted by void energy. High capacity but risky.',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// ─── Body Equipment Types ─────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType } from './types';
|
||||||
|
|
||||||
|
export const BODY_EQUIPMENT: Record<string, EquipmentType> = {
|
||||||
|
// ─── Body ────────────────────────────────────────────────────────
|
||||||
|
civilianShirt: {
|
||||||
|
id: 'civilianShirt',
|
||||||
|
name: 'Civilian Shirt',
|
||||||
|
category: 'body',
|
||||||
|
slot: 'body',
|
||||||
|
baseCapacity: 30,
|
||||||
|
description: 'A plain shirt with minimal magical properties.',
|
||||||
|
},
|
||||||
|
apprenticeRobe: {
|
||||||
|
id: 'apprenticeRobe',
|
||||||
|
name: 'Apprentice Robe',
|
||||||
|
category: 'body',
|
||||||
|
slot: 'body',
|
||||||
|
baseCapacity: 45,
|
||||||
|
description: 'The standard robe for magic apprentices.',
|
||||||
|
},
|
||||||
|
scholarRobe: {
|
||||||
|
id: 'scholarRobe',
|
||||||
|
name: 'Scholar Robe',
|
||||||
|
category: 'body',
|
||||||
|
slot: 'body',
|
||||||
|
baseCapacity: 55,
|
||||||
|
description: 'A robe worn by scholars and researchers.',
|
||||||
|
},
|
||||||
|
battleRobe: {
|
||||||
|
id: 'battleRobe',
|
||||||
|
name: 'Battle Robe',
|
||||||
|
category: 'body',
|
||||||
|
slot: 'body',
|
||||||
|
baseCapacity: 65,
|
||||||
|
description: 'A reinforced robe designed for combat mages.',
|
||||||
|
},
|
||||||
|
arcanistRobe: {
|
||||||
|
id: 'arcanistRobe',
|
||||||
|
name: 'Arcanist Robe',
|
||||||
|
category: 'body',
|
||||||
|
slot: 'body',
|
||||||
|
baseCapacity: 80,
|
||||||
|
description: 'An ornate robe for master arcanists. High capacity for body armor.',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// ─── Caster Equipment Types ────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType } from './types';
|
||||||
|
|
||||||
|
export const CASTER_EQUIPMENT: Record<string, EquipmentType> = {
|
||||||
|
// ─── Main Hand - Casters ─────────────────────────────────────────────────
|
||||||
|
basicStaff: {
|
||||||
|
id: 'basicStaff',
|
||||||
|
name: 'Basic Staff',
|
||||||
|
category: 'caster',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 50,
|
||||||
|
description: 'A simple wooden staff, basic but reliable for channeling mana.',
|
||||||
|
twoHanded: true,
|
||||||
|
},
|
||||||
|
apprenticeWand: {
|
||||||
|
id: 'apprenticeWand',
|
||||||
|
name: 'Apprentice Wand',
|
||||||
|
category: 'caster',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 35,
|
||||||
|
description: 'A lightweight wand favored by apprentices. Lower capacity but faster to prepare.',
|
||||||
|
},
|
||||||
|
oakStaff: {
|
||||||
|
id: 'oakStaff',
|
||||||
|
name: 'Oak Staff',
|
||||||
|
category: 'caster',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 65,
|
||||||
|
description: 'A sturdy oak staff with decent mana capacity.',
|
||||||
|
twoHanded: true,
|
||||||
|
},
|
||||||
|
crystalWand: {
|
||||||
|
id: 'crystalWand',
|
||||||
|
name: 'Crystal Wand',
|
||||||
|
category: 'caster',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 45,
|
||||||
|
description: 'A wand tipped with a small crystal. Excellent for elemental enchantments.',
|
||||||
|
},
|
||||||
|
arcanistStaff: {
|
||||||
|
id: 'arcanistStaff',
|
||||||
|
name: 'Arcanist Staff',
|
||||||
|
category: 'caster',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 80,
|
||||||
|
description: 'A staff designed for advanced spellcasters. High capacity for complex enchantments.',
|
||||||
|
twoHanded: true,
|
||||||
|
},
|
||||||
|
battlestaff: {
|
||||||
|
id: 'battlestaff',
|
||||||
|
name: 'Battlestaff',
|
||||||
|
category: 'caster',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 70,
|
||||||
|
description: 'A reinforced staff suitable for both casting and combat.',
|
||||||
|
twoHanded: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// ─── Catalyst Equipment Types ───────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType } from './types';
|
||||||
|
|
||||||
|
export const CATALYST_EQUIPMENT: Record<string, EquipmentType> = {
|
||||||
|
// ─── Main Hand - Catalysts ────────────────────────────────────────────────
|
||||||
|
basicCatalyst: {
|
||||||
|
id: 'basicCatalyst',
|
||||||
|
name: 'Basic Catalyst',
|
||||||
|
category: 'catalyst',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 40,
|
||||||
|
description: 'A simple catalyst for amplifying magical effects.',
|
||||||
|
},
|
||||||
|
fireCatalyst: {
|
||||||
|
id: 'fireCatalyst',
|
||||||
|
name: 'Fire Catalyst',
|
||||||
|
category: 'catalyst',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 55,
|
||||||
|
description: 'A catalyst attuned to fire magic. Enhances fire enchantments.',
|
||||||
|
},
|
||||||
|
voidCatalyst: {
|
||||||
|
id: 'voidCatalyst',
|
||||||
|
name: 'Void Catalyst',
|
||||||
|
category: 'catalyst',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 75,
|
||||||
|
description: 'A rare catalyst touched by void energy. High capacity but volatile.',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// ─── Feet Equipment Types ─────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType } from './types';
|
||||||
|
|
||||||
|
export const FEET_EQUIPMENT: Record<string, EquipmentType> = {
|
||||||
|
// ─── Feet ────────────────────────────────────────────────────────
|
||||||
|
civilianShoes: {
|
||||||
|
id: 'civilianShoes',
|
||||||
|
name: 'Civilian Shoes',
|
||||||
|
category: 'feet',
|
||||||
|
slot: 'feet',
|
||||||
|
baseCapacity: 15,
|
||||||
|
description: 'Simple leather shoes. No special properties.',
|
||||||
|
},
|
||||||
|
apprenticeBoots: {
|
||||||
|
id: 'apprenticeBoots',
|
||||||
|
name: 'Apprentice Boots',
|
||||||
|
category: 'feet',
|
||||||
|
slot: 'feet',
|
||||||
|
baseCapacity: 25,
|
||||||
|
description: 'Basic boots for magic students.',
|
||||||
|
},
|
||||||
|
travelerBoots: {
|
||||||
|
id: 'travelerBoots',
|
||||||
|
name: 'Traveler Boots',
|
||||||
|
category: 'feet',
|
||||||
|
slot: 'feet',
|
||||||
|
baseCapacity: 30,
|
||||||
|
description: 'Comfortable boots for long journeys.',
|
||||||
|
},
|
||||||
|
battleBoots: {
|
||||||
|
id: 'battleBoots',
|
||||||
|
name: 'Battle Boots',
|
||||||
|
category: 'feet',
|
||||||
|
slot: 'feet',
|
||||||
|
baseCapacity: 35,
|
||||||
|
description: 'Sturdy boots for combat situations.',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// ─── Hands Equipment Types ─────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType } from './types';
|
||||||
|
|
||||||
|
export const HANDS_EQUIPMENT: Record<string, EquipmentType> = {
|
||||||
|
// ─── Hands ───────────────────────────────────────────────────────
|
||||||
|
civilianGloves: {
|
||||||
|
id: 'civilianGloves',
|
||||||
|
name: 'Civilian Gloves',
|
||||||
|
category: 'hands',
|
||||||
|
slot: 'hands',
|
||||||
|
baseCapacity: 20,
|
||||||
|
description: 'Simple cloth gloves. Minimal magical capacity.',
|
||||||
|
},
|
||||||
|
apprenticeGloves: {
|
||||||
|
id: 'apprenticeGloves',
|
||||||
|
name: 'Apprentice Gloves',
|
||||||
|
category: 'hands',
|
||||||
|
slot: 'hands',
|
||||||
|
baseCapacity: 30,
|
||||||
|
description: 'Basic gloves for handling magical components.',
|
||||||
|
},
|
||||||
|
spellweaveGloves: {
|
||||||
|
id: 'spellweaveGloves',
|
||||||
|
name: 'Spellweave Gloves',
|
||||||
|
category: 'hands',
|
||||||
|
slot: 'hands',
|
||||||
|
baseCapacity: 40,
|
||||||
|
description: 'Gloves woven with mana-conductive threads.',
|
||||||
|
},
|
||||||
|
combatGauntlets: {
|
||||||
|
id: 'combatGauntlets',
|
||||||
|
name: 'Combat Gauntlets',
|
||||||
|
category: 'hands',
|
||||||
|
slot: 'hands',
|
||||||
|
baseCapacity: 35,
|
||||||
|
description: 'Armored gauntlets for battle mages.',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// ─── Head Equipment Types ────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType } from './types';
|
||||||
|
|
||||||
|
export const HEAD_EQUIPMENT: Record<string, EquipmentType> = {
|
||||||
|
// ─── Head ─────────────────────────────────────────────────────────
|
||||||
|
clothHood: {
|
||||||
|
id: 'clothHood',
|
||||||
|
name: 'Cloth Hood',
|
||||||
|
category: 'head',
|
||||||
|
slot: 'head',
|
||||||
|
baseCapacity: 25,
|
||||||
|
description: 'A simple cloth hood. Minimal protection but comfortable.',
|
||||||
|
},
|
||||||
|
apprenticeCap: {
|
||||||
|
id: 'apprenticeCap',
|
||||||
|
name: 'Apprentice Cap',
|
||||||
|
category: 'head',
|
||||||
|
slot: 'head',
|
||||||
|
baseCapacity: 30,
|
||||||
|
description: 'The traditional cap of magic apprentices.',
|
||||||
|
},
|
||||||
|
wizardHat: {
|
||||||
|
id: 'wizardHat',
|
||||||
|
name: 'Wizard Hat',
|
||||||
|
category: 'head',
|
||||||
|
slot: 'head',
|
||||||
|
baseCapacity: 45,
|
||||||
|
description: 'A classic pointed wizard hat. Decent capacity for headwear.',
|
||||||
|
},
|
||||||
|
arcanistCirclet: {
|
||||||
|
id: 'arcanistCirclet',
|
||||||
|
name: 'Arcanist Circlet',
|
||||||
|
category: 'head',
|
||||||
|
slot: 'head',
|
||||||
|
baseCapacity: 40,
|
||||||
|
description: 'A silver circlet worn by accomplished arcanists.',
|
||||||
|
},
|
||||||
|
battleHelm: {
|
||||||
|
id: 'battleHelm',
|
||||||
|
name: 'Battle Helm',
|
||||||
|
category: 'head',
|
||||||
|
slot: 'head',
|
||||||
|
baseCapacity: 50,
|
||||||
|
description: 'A sturdy helm for battle mages.',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// ─── Equipment Types Index ───────────────────────────────
|
||||||
|
// Re-exports from all equipment type modules
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export type {
|
||||||
|
EquipmentSlot,
|
||||||
|
EquipmentCategory,
|
||||||
|
EquipmentType
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export {
|
||||||
|
EQUIPMENT_SLOTS,
|
||||||
|
SLOT_NAMES
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// Re-export data
|
||||||
|
export { EQUIPMENT_TYPES } from './data';
|
||||||
|
|
||||||
|
// Re-export utility functions
|
||||||
|
export {
|
||||||
|
getEquipmentType,
|
||||||
|
getEquipmentByCategory,
|
||||||
|
getEquipmentBySlot,
|
||||||
|
getAllEquipmentTypes,
|
||||||
|
getValidSlotsForCategory,
|
||||||
|
getValidSlotsForEquipmentType,
|
||||||
|
canEquipInSlot,
|
||||||
|
} from './utils';
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// ─── Shield Equipment Types ───────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType } from './types';
|
||||||
|
|
||||||
|
export const SHIELD_EQUIPMENT: Record<string, EquipmentType> = {
|
||||||
|
// ─── Off Hand - Shields ───────────────────────────────────────────
|
||||||
|
basicShield: {
|
||||||
|
id: 'basicShield',
|
||||||
|
name: 'Basic Shield',
|
||||||
|
category: 'shield',
|
||||||
|
slot: 'offHand',
|
||||||
|
baseCapacity: 40,
|
||||||
|
description: 'A simple wooden shield. Provides basic protection.',
|
||||||
|
},
|
||||||
|
reinforcedShield: {
|
||||||
|
id: 'reinforcedShield',
|
||||||
|
name: 'Reinforced Shield',
|
||||||
|
category: 'shield',
|
||||||
|
slot: 'offHand',
|
||||||
|
baseCapacity: 55,
|
||||||
|
description: 'A metal-reinforced shield with enhanced durability and capacity.',
|
||||||
|
},
|
||||||
|
runicShield: {
|
||||||
|
id: 'runicShield',
|
||||||
|
name: 'Runic Shield',
|
||||||
|
category: 'shield',
|
||||||
|
slot: 'offHand',
|
||||||
|
baseCapacity: 70,
|
||||||
|
description: 'A shield engraved with protective runes. Excellent for defensive enchantments.',
|
||||||
|
},
|
||||||
|
manaShield: {
|
||||||
|
id: 'manaShield',
|
||||||
|
name: 'Mana Shield',
|
||||||
|
category: 'shield',
|
||||||
|
slot: 'offHand',
|
||||||
|
baseCapacity: 60,
|
||||||
|
description: 'A crystalline shield that can store and reflect mana.',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// ─── Sword Equipment Types ───────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType } from './types';
|
||||||
|
|
||||||
|
export const SWORD_EQUIPMENT: Record<string, EquipmentType> = {
|
||||||
|
// ─── Main Hand - Magic Swords ─────────────────────────────────────
|
||||||
|
// Magic swords have low base damage but high cast speed
|
||||||
|
// They can be enchanted with elemental effects that use mana over time
|
||||||
|
ironBlade: {
|
||||||
|
id: 'ironBlade',
|
||||||
|
name: 'Iron Blade',
|
||||||
|
category: 'sword',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 30,
|
||||||
|
baseDamage: 3,
|
||||||
|
baseCastSpeed: 4,
|
||||||
|
description: 'A simple iron sword. Can be enchanted with elemental effects.',
|
||||||
|
},
|
||||||
|
steelBlade: {
|
||||||
|
id: 'steelBlade',
|
||||||
|
name: 'Steel Blade',
|
||||||
|
category: 'sword',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 40,
|
||||||
|
baseDamage: 4,
|
||||||
|
baseCastSpeed: 4,
|
||||||
|
description: 'A well-crafted steel sword. Balanced for combat and enchanting.',
|
||||||
|
},
|
||||||
|
crystalBlade: {
|
||||||
|
id: 'crystalBlade',
|
||||||
|
name: 'Crystal Blade',
|
||||||
|
category: 'sword',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 55,
|
||||||
|
baseDamage: 3,
|
||||||
|
baseCastSpeed: 5,
|
||||||
|
description: 'A blade made of crystallized mana. Excellent for elemental enchantments.',
|
||||||
|
},
|
||||||
|
arcanistBlade: {
|
||||||
|
id: 'arcanistBlade',
|
||||||
|
name: 'Arcanist Blade',
|
||||||
|
category: 'sword',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 65,
|
||||||
|
baseDamage: 5,
|
||||||
|
baseCastSpeed: 4,
|
||||||
|
description: 'A sword forged for battle mages. High capacity for powerful enchantments.',
|
||||||
|
},
|
||||||
|
voidBlade: {
|
||||||
|
id: 'voidBlade',
|
||||||
|
name: 'Void-Touched Blade',
|
||||||
|
category: 'sword',
|
||||||
|
slot: 'mainHand',
|
||||||
|
baseCapacity: 50,
|
||||||
|
baseDamage: 6,
|
||||||
|
baseCastSpeed: 3,
|
||||||
|
description: 'A blade corrupted by void energy. Powerful but consumes more mana.',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// ─── Equipment Types ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type EquipmentSlot = 'mainHand' | 'offHand' | 'head' | 'body' | 'hands' | 'feet' | 'accessory1' | 'accessory2';
|
||||||
|
export type EquipmentCategory = 'caster' | 'shield' | 'catalyst' | 'sword' | 'head' | 'body' | 'hands' | 'feet' | 'accessory';
|
||||||
|
|
||||||
|
// All equipment slots in order
|
||||||
|
export const EQUIPMENT_SLOTS: EquipmentSlot[] = ['mainHand', 'offHand', 'head', 'body', 'hands', 'feet', 'accessory1', 'accessory2'];
|
||||||
|
|
||||||
|
// Human-readable names for equipment slots
|
||||||
|
export const SLOT_NAMES: Record<EquipmentSlot, string> = {
|
||||||
|
mainHand: 'Main Hand',
|
||||||
|
offHand: 'Off Hand',
|
||||||
|
head: 'Head',
|
||||||
|
body: 'Body',
|
||||||
|
hands: 'Hands',
|
||||||
|
feet: 'Feet',
|
||||||
|
accessory1: 'Accessory 1',
|
||||||
|
accessory2: 'Accessory 2',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface EquipmentType {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: EquipmentCategory;
|
||||||
|
slot: EquipmentSlot;
|
||||||
|
baseCapacity: number;
|
||||||
|
description: string;
|
||||||
|
baseDamage?: number; // For swords
|
||||||
|
baseCastSpeed?: number; // For swords (higher = faster)
|
||||||
|
twoHanded?: boolean; // If true, weapon occupies both main hand and offhand slots
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// ─── Equipment Helper Functions ─────────────────────────
|
||||||
|
|
||||||
|
import type { EquipmentType, EquipmentSlot, EquipmentCategory } from './types';
|
||||||
|
import { EQUIPMENT_TYPES } from './index';
|
||||||
|
|
||||||
|
export function getEquipmentType(id: string): EquipmentType | undefined {
|
||||||
|
return EQUIPMENT_TYPES[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEquipmentByCategory(category: EquipmentCategory): EquipmentType[] {
|
||||||
|
return Object.values(EQUIPMENT_TYPES).filter(e => e.category === category) as EquipmentType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEquipmentBySlot(slot: EquipmentSlot): EquipmentType[] {
|
||||||
|
return Object.values(EQUIPMENT_TYPES).filter(e => e.slot === slot) as EquipmentType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllEquipmentTypes(): EquipmentType[] {
|
||||||
|
return Object.values(EQUIPMENT_TYPES) as EquipmentType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get valid slots for a category
|
||||||
|
// Note: For 2-handed weapons, use getValidSlotsForEquipmentType instead
|
||||||
|
export function getValidSlotsForCategory(category: EquipmentCategory): EquipmentSlot[] {
|
||||||
|
switch (category) {
|
||||||
|
case 'caster':
|
||||||
|
case 'catalyst':
|
||||||
|
case 'sword':
|
||||||
|
return ['mainHand'];
|
||||||
|
case 'shield':
|
||||||
|
return ['offHand'];
|
||||||
|
case 'head':
|
||||||
|
return ['head'];
|
||||||
|
case 'body':
|
||||||
|
return ['body'];
|
||||||
|
case 'hands':
|
||||||
|
return ['hands'];
|
||||||
|
case 'feet':
|
||||||
|
return ['feet'];
|
||||||
|
case 'accessory':
|
||||||
|
return ['accessory1', 'accessory2'];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get valid slots for a specific equipment type (considers 2-handed weapons)
|
||||||
|
export function getValidSlotsForEquipmentType(equipType: EquipmentType): EquipmentSlot[] {
|
||||||
|
// 2-handed weapons occupy both main hand and offhand
|
||||||
|
if (equipType.twoHanded) {
|
||||||
|
return ['mainHand', 'offHand'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use category-based slots
|
||||||
|
return getValidSlotsForCategory(equipType.category);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if an equipment type can be equipped in a specific slot
|
||||||
|
export function canEquipInSlot(equipmentType: EquipmentType, slot: EquipmentSlot): boolean {
|
||||||
|
const validSlots = getValidSlotsForEquipmentType(equipmentType);
|
||||||
|
return validSlots.includes(slot);
|
||||||
|
}
|
||||||
@@ -1,471 +0,0 @@
|
|||||||
// ─── Golem Definitions ─────────────────────────────────────────────────────────
|
|
||||||
// Golems are magical constructs that fight alongside the player
|
|
||||||
// They cost mana to summon and maintain
|
|
||||||
|
|
||||||
import type { SpellCost } from '../types';
|
|
||||||
|
|
||||||
// Golem mana cost helper
|
|
||||||
function elemCost(element: string, amount: number): SpellCost {
|
|
||||||
return { type: 'element', element, amount };
|
|
||||||
}
|
|
||||||
|
|
||||||
function rawCost(amount: number): SpellCost {
|
|
||||||
return { type: 'raw', amount };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GolemManaCost {
|
|
||||||
type: 'raw' | 'element';
|
|
||||||
element?: string;
|
|
||||||
amount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'attunement_level' | 'mana_unlocked' | 'dual_attunement';
|
|
||||||
attunement?: string;
|
|
||||||
level?: number;
|
|
||||||
manaType?: string;
|
|
||||||
attunements?: string[];
|
|
||||||
levels?: number[];
|
|
||||||
};
|
|
||||||
tier: number; // Power tier (1-4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// All golem definitions
|
|
||||||
export const GOLEMS_DEF: Record<string, GolemDef> = {
|
|
||||||
// ─── BASE GOLEMS ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Earth Golem - Basic, available with Fabricator attunement
|
|
||||||
earthGolem: {
|
|
||||||
id: 'earthGolem',
|
|
||||||
name: 'Earth Golem',
|
|
||||||
description: 'A sturdy construct of stone and soil. Slow but powerful.',
|
|
||||||
baseManaType: 'earth',
|
|
||||||
summonCost: [elemCost('earth', 10)],
|
|
||||||
maintenanceCost: [elemCost('earth', 0.5)],
|
|
||||||
damage: 8,
|
|
||||||
attackSpeed: 1.5,
|
|
||||||
hp: 50,
|
|
||||||
armorPierce: 0.15,
|
|
||||||
isAoe: false,
|
|
||||||
aoeTargets: 1,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'attunement_level',
|
|
||||||
attunement: 'fabricator',
|
|
||||||
level: 2,
|
|
||||||
},
|
|
||||||
tier: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── ELEMENTAL VARIANT GOLEMS ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// Steel Golem - Metal mana variant
|
|
||||||
steelGolem: {
|
|
||||||
id: 'steelGolem',
|
|
||||||
name: 'Steel Golem',
|
|
||||||
description: 'Forged from metal, this golem has high armor piercing.',
|
|
||||||
baseManaType: 'metal',
|
|
||||||
summonCost: [elemCost('metal', 8), elemCost('earth', 5)],
|
|
||||||
maintenanceCost: [elemCost('metal', 0.6), elemCost('earth', 0.2)],
|
|
||||||
damage: 12,
|
|
||||||
attackSpeed: 1.2,
|
|
||||||
hp: 60,
|
|
||||||
armorPierce: 0.35,
|
|
||||||
isAoe: false,
|
|
||||||
aoeTargets: 1,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'mana_unlocked',
|
|
||||||
manaType: 'metal',
|
|
||||||
},
|
|
||||||
tier: 2,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Crystal Golem - Crystal mana variant
|
|
||||||
crystalGolem: {
|
|
||||||
id: 'crystalGolem',
|
|
||||||
name: 'Crystal Golem',
|
|
||||||
description: 'A prismatic construct that deals high damage with precision.',
|
|
||||||
baseManaType: 'crystal',
|
|
||||||
summonCost: [elemCost('crystal', 6), elemCost('earth', 3)],
|
|
||||||
maintenanceCost: [elemCost('crystal', 0.4), elemCost('earth', 0.2)],
|
|
||||||
damage: 18,
|
|
||||||
attackSpeed: 1.0,
|
|
||||||
hp: 40,
|
|
||||||
armorPierce: 0.25,
|
|
||||||
isAoe: false,
|
|
||||||
aoeTargets: 1,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'mana_unlocked',
|
|
||||||
manaType: 'crystal',
|
|
||||||
},
|
|
||||||
tier: 3,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Sand Golem - Sand mana variant
|
|
||||||
sandGolem: {
|
|
||||||
id: 'sandGolem',
|
|
||||||
name: 'Sand Golem',
|
|
||||||
description: 'A shifting construct of sand particles. Hits multiple enemies.',
|
|
||||||
baseManaType: 'sand',
|
|
||||||
summonCost: [elemCost('sand', 8), elemCost('earth', 3)],
|
|
||||||
maintenanceCost: [elemCost('sand', 0.5), elemCost('earth', 0.2)],
|
|
||||||
damage: 6,
|
|
||||||
attackSpeed: 2.0,
|
|
||||||
hp: 35,
|
|
||||||
armorPierce: 0.1,
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 2,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'mana_unlocked',
|
|
||||||
manaType: 'sand',
|
|
||||||
},
|
|
||||||
tier: 2,
|
|
||||||
},
|
|
||||||
|
|
||||||
// ─── ADVANCED HYBRID GOLEMS ──────────────────────────────────────────────────
|
|
||||||
// Require Enchanter 5 + Fabricator 5
|
|
||||||
|
|
||||||
// Lava Golem - Fire + Earth fusion
|
|
||||||
lavaGolem: {
|
|
||||||
id: 'lavaGolem',
|
|
||||||
name: 'Lava Golem',
|
|
||||||
description: 'Molten earth and fire combined. Burns enemies over time.',
|
|
||||||
baseManaType: 'earth',
|
|
||||||
summonCost: [elemCost('earth', 10), elemCost('fire', 8)],
|
|
||||||
maintenanceCost: [elemCost('earth', 0.4), elemCost('fire', 0.5)],
|
|
||||||
damage: 15,
|
|
||||||
attackSpeed: 1.0,
|
|
||||||
hp: 70,
|
|
||||||
armorPierce: 0.2,
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 2,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'dual_attunement',
|
|
||||||
attunements: ['enchanter', 'fabricator'],
|
|
||||||
levels: [5, 5],
|
|
||||||
},
|
|
||||||
tier: 3,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Galvanic Golem - Metal + Lightning fusion
|
|
||||||
galvanicGolem: {
|
|
||||||
id: 'galvanicGolem',
|
|
||||||
name: 'Galvanic Golem',
|
|
||||||
description: 'A conductive metal construct charged with lightning. Extremely fast attacks.',
|
|
||||||
baseManaType: 'metal',
|
|
||||||
summonCost: [elemCost('metal', 8), elemCost('lightning', 6)],
|
|
||||||
maintenanceCost: [elemCost('metal', 0.3), elemCost('lightning', 0.6)],
|
|
||||||
damage: 10,
|
|
||||||
attackSpeed: 3.5,
|
|
||||||
hp: 45,
|
|
||||||
armorPierce: 0.45,
|
|
||||||
isAoe: false,
|
|
||||||
aoeTargets: 1,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'dual_attunement',
|
|
||||||
attunements: ['enchanter', 'fabricator'],
|
|
||||||
levels: [5, 5],
|
|
||||||
},
|
|
||||||
tier: 3,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Obsidian Golem - Dark + Earth fusion
|
|
||||||
obsidianGolem: {
|
|
||||||
id: 'obsidianGolem',
|
|
||||||
name: 'Obsidian Golem',
|
|
||||||
description: 'Volcanic glass animated by shadow. Devastating single-target damage.',
|
|
||||||
baseManaType: 'earth',
|
|
||||||
summonCost: [elemCost('earth', 12), elemCost('dark', 6)],
|
|
||||||
maintenanceCost: [elemCost('earth', 0.3), elemCost('dark', 0.4)],
|
|
||||||
damage: 25,
|
|
||||||
attackSpeed: 0.8,
|
|
||||||
hp: 55,
|
|
||||||
armorPierce: 0.5,
|
|
||||||
isAoe: false,
|
|
||||||
aoeTargets: 1,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'dual_attunement',
|
|
||||||
attunements: ['enchanter', 'fabricator'],
|
|
||||||
levels: [5, 5],
|
|
||||||
},
|
|
||||||
tier: 4,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Prism Golem - Light + Crystal fusion
|
|
||||||
prismGolem: {
|
|
||||||
id: 'prismGolem',
|
|
||||||
name: 'Prism Golem',
|
|
||||||
description: 'A radiant crystal construct. Channels light into piercing beams.',
|
|
||||||
baseManaType: 'crystal',
|
|
||||||
summonCost: [elemCost('crystal', 10), elemCost('light', 6)],
|
|
||||||
maintenanceCost: [elemCost('crystal', 0.4), elemCost('light', 0.4)],
|
|
||||||
damage: 20,
|
|
||||||
attackSpeed: 1.5,
|
|
||||||
hp: 50,
|
|
||||||
armorPierce: 0.35,
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 3,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'dual_attunement',
|
|
||||||
attunements: ['enchanter', 'fabricator'],
|
|
||||||
levels: [5, 5],
|
|
||||||
},
|
|
||||||
tier: 4,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Quicksilver Golem - Water + Metal fusion
|
|
||||||
quicksilverGolem: {
|
|
||||||
id: 'quicksilverGolem',
|
|
||||||
name: 'Quicksilver Golem',
|
|
||||||
description: 'Liquid metal that flows around defenses. Fast and hard to dodge.',
|
|
||||||
baseManaType: 'metal',
|
|
||||||
summonCost: [elemCost('metal', 6), elemCost('water', 6)],
|
|
||||||
maintenanceCost: [elemCost('metal', 0.3), elemCost('water', 0.3)],
|
|
||||||
damage: 8,
|
|
||||||
attackSpeed: 4.0,
|
|
||||||
hp: 40,
|
|
||||||
armorPierce: 0.3,
|
|
||||||
isAoe: false,
|
|
||||||
aoeTargets: 1,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'dual_attunement',
|
|
||||||
attunements: ['enchanter', 'fabricator'],
|
|
||||||
levels: [5, 5],
|
|
||||||
},
|
|
||||||
tier: 3,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Voidstone Golem - Void + Earth fusion (ultimate)
|
|
||||||
voidstoneGolem: {
|
|
||||||
id: 'voidstoneGolem',
|
|
||||||
name: 'Voidstone Golem',
|
|
||||||
description: 'Earth infused with void energy. The ultimate golem construct.',
|
|
||||||
baseManaType: 'earth',
|
|
||||||
summonCost: [elemCost('earth', 15), elemCost('void', 8)],
|
|
||||||
maintenanceCost: [elemCost('earth', 0.3), elemCost('void', 0.6)],
|
|
||||||
damage: 40,
|
|
||||||
attackSpeed: 0.6,
|
|
||||||
hp: 100,
|
|
||||||
armorPierce: 0.6,
|
|
||||||
isAoe: true,
|
|
||||||
aoeTargets: 3,
|
|
||||||
unlockCondition: {
|
|
||||||
type: 'dual_attunement',
|
|
||||||
attunements: ['enchanter', 'fabricator'],
|
|
||||||
levels: [5, 5],
|
|
||||||
},
|
|
||||||
tier: 4,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get golem slots based on 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,
|
|
||||||
attunements: Record<string, { active: boolean; level: number }>,
|
|
||||||
unlockedElements: string[]
|
|
||||||
): 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);
|
|
||||||
|
|
||||||
case 'mana_unlocked':
|
|
||||||
return unlockedElements.includes(condition.manaType || '');
|
|
||||||
|
|
||||||
case 'dual_attunement':
|
|
||||||
if (!condition.attunements || !condition.levels) return false;
|
|
||||||
return condition.attunements.every((attId, idx) => {
|
|
||||||
const att = attunements[attId];
|
|
||||||
return att?.active && (att.level || 1) >= condition.levels![idx];
|
|
||||||
});
|
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate golem damage with skill bonuses
|
|
||||||
export function getGolemDamage(
|
|
||||||
golemId: string,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate golem attack speed with skill bonuses
|
|
||||||
export function getGolemAttackSpeed(
|
|
||||||
golemId: string,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 };
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// ─── Base Golem Definitions ───────────────────────────────────
|
||||||
|
|
||||||
|
import type { GolemDef } from './types';
|
||||||
|
import { elemCost } from './types';
|
||||||
|
|
||||||
|
export const BASE_GOLEMS: Record<string, GolemDef> = {
|
||||||
|
// ─── BASE GOLEMS ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Earth Golem - Basic, available with Fabricator attunement
|
||||||
|
earthGolem: {
|
||||||
|
id: 'earthGolem',
|
||||||
|
name: 'Earth Golem',
|
||||||
|
description: 'A sturdy construct of stone and soil. Slow but powerful.',
|
||||||
|
baseManaType: 'earth',
|
||||||
|
summonCost: [elemCost('earth', 10)],
|
||||||
|
maintenanceCost: [elemCost('earth', 0.5)],
|
||||||
|
damage: 8,
|
||||||
|
attackSpeed: 1.5,
|
||||||
|
hp: 50,
|
||||||
|
armorPierce: 0.15,
|
||||||
|
isAoe: false,
|
||||||
|
aoeTargets: 1,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'attunement_level',
|
||||||
|
attunement: 'fabricator',
|
||||||
|
level: 2,
|
||||||
|
},
|
||||||
|
tier: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// ─── Elemental Variant Golems ───────────────────────────────────
|
||||||
|
|
||||||
|
import type { GolemDef } from './types';
|
||||||
|
import { elemCost } from './types';
|
||||||
|
|
||||||
|
export const ELEMENTAL_GOLEMS: Record<string, GolemDef> = {
|
||||||
|
// ─── ELEMENTAL VARIANT GOLEMS ────────────────────────────────────────
|
||||||
|
|
||||||
|
// Steel Golem - Metal mana variant
|
||||||
|
steelGolem: {
|
||||||
|
id: 'steelGolem',
|
||||||
|
name: 'Steel Golem',
|
||||||
|
description: 'Forged from metal, this golem has high armor piercing.',
|
||||||
|
baseManaType: 'metal',
|
||||||
|
summonCost: [elemCost('metal', 8), elemCost('earth', 5)],
|
||||||
|
maintenanceCost: [elemCost('metal', 0.6), elemCost('earth', 0.2)],
|
||||||
|
damage: 12,
|
||||||
|
attackSpeed: 1.2,
|
||||||
|
hp: 60,
|
||||||
|
armorPierce: 0.35,
|
||||||
|
isAoe: false,
|
||||||
|
aoeTargets: 1,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'mana_unlocked',
|
||||||
|
manaType: 'metal',
|
||||||
|
},
|
||||||
|
tier: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Crystal Golem - Crystal mana variant
|
||||||
|
crystalGolem: {
|
||||||
|
id: 'crystalGolem',
|
||||||
|
name: 'Crystal Golem',
|
||||||
|
description: 'A prismatic construct that deals high damage with precision.',
|
||||||
|
baseManaType: 'crystal',
|
||||||
|
summonCost: [elemCost('crystal', 6), elemCost('earth', 3)],
|
||||||
|
maintenanceCost: [elemCost('crystal', 0.4), elemCost('earth', 0.2)],
|
||||||
|
damage: 18,
|
||||||
|
attackSpeed: 1.0,
|
||||||
|
hp: 40,
|
||||||
|
armorPierce: 0.25,
|
||||||
|
isAoe: false,
|
||||||
|
aoeTargets: 1,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'mana_unlocked',
|
||||||
|
manaType: 'crystal',
|
||||||
|
},
|
||||||
|
tier: 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sand Golem - Sand mana variant
|
||||||
|
sandGolem: {
|
||||||
|
id: 'sandGolem',
|
||||||
|
name: 'Sand Golem',
|
||||||
|
description: 'A shifting construct of sand particles. Hits multiple enemies.',
|
||||||
|
baseManaType: 'sand',
|
||||||
|
summonCost: [elemCost('sand', 8), elemCost('earth', 3)],
|
||||||
|
maintenanceCost: [elemCost('sand', 0.5), elemCost('earth', 0.2)],
|
||||||
|
damage: 6,
|
||||||
|
attackSpeed: 2.0,
|
||||||
|
hp: 35,
|
||||||
|
armorPierce: 0.1,
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 2,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'mana_unlocked',
|
||||||
|
manaType: 'sand',
|
||||||
|
},
|
||||||
|
tier: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// ─── Advanced Hybrid Golems ────────────────────────────────────
|
||||||
|
// Require Enchanter 5 + Fabricator 5
|
||||||
|
|
||||||
|
import type { GolemDef } from './types';
|
||||||
|
import { elemCost } from './types';
|
||||||
|
|
||||||
|
export const HYBRID_GOLEMS: Record<string, GolemDef> = {
|
||||||
|
// Lava Golem - Fire + Earth fusion
|
||||||
|
lavaGolem: {
|
||||||
|
id: 'lavaGolem',
|
||||||
|
name: 'Lava Golem',
|
||||||
|
description: 'Molten earth and fire combined. Burns enemies over time.',
|
||||||
|
baseManaType: 'earth',
|
||||||
|
summonCost: [elemCost('earth', 10), elemCost('fire', 8)],
|
||||||
|
maintenanceCost: [elemCost('earth', 0.4), elemCost('fire', 0.5)],
|
||||||
|
damage: 15,
|
||||||
|
attackSpeed: 1.0,
|
||||||
|
hp: 70,
|
||||||
|
armorPierce: 0.2,
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 2,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'dual_attunement',
|
||||||
|
attunements: ['enchanter', 'fabricator'],
|
||||||
|
levels: [5, 5],
|
||||||
|
},
|
||||||
|
tier: 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Galvanic Golem - Metal + Lightning fusion
|
||||||
|
galvanicGolem: {
|
||||||
|
id: 'galvanicGolem',
|
||||||
|
name: 'Galvanic Golem',
|
||||||
|
description: 'A conductive metal construct charged with lightning. Extremely fast attacks.',
|
||||||
|
baseManaType: 'metal',
|
||||||
|
summonCost: [elemCost('metal', 8), elemCost('lightning', 6)],
|
||||||
|
maintenanceCost: [elemCost('metal', 0.3), elemCost('lightning', 0.6)],
|
||||||
|
damage: 10,
|
||||||
|
attackSpeed: 3.5,
|
||||||
|
hp: 45,
|
||||||
|
armorPierce: 0.45,
|
||||||
|
isAoe: false,
|
||||||
|
aoeTargets: 1,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'dual_attunement',
|
||||||
|
attunements: ['enchanter', 'fabricator'],
|
||||||
|
levels: [5, 5],
|
||||||
|
},
|
||||||
|
tier: 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Obsidian Golem - Dark + Earth fusion
|
||||||
|
obsidianGolem: {
|
||||||
|
id: 'obsidianGolem',
|
||||||
|
name: 'Obsidian Golem',
|
||||||
|
description: 'Volcanic glass animated by shadow. Devastating single-target damage.',
|
||||||
|
baseManaType: 'earth',
|
||||||
|
summonCost: [elemCost('earth', 12), elemCost('dark', 6)],
|
||||||
|
maintenanceCost: [elemCost('earth', 0.3), elemCost('dark', 0.4)],
|
||||||
|
damage: 25,
|
||||||
|
attackSpeed: 0.8,
|
||||||
|
hp: 55,
|
||||||
|
armorPierce: 0.5,
|
||||||
|
isAoe: false,
|
||||||
|
aoeTargets: 1,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'dual_attunement',
|
||||||
|
attunements: ['enchanter', 'fabricator'],
|
||||||
|
levels: [5, 5],
|
||||||
|
},
|
||||||
|
tier: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Prism Golem - Light + Crystal fusion
|
||||||
|
prismGolem: {
|
||||||
|
id: 'prismGolem',
|
||||||
|
name: 'Prism Golem',
|
||||||
|
description: 'A radiant crystal construct. Channels light into piercing beams.',
|
||||||
|
baseManaType: 'crystal',
|
||||||
|
summonCost: [elemCost('crystal', 10), elemCost('light', 6)],
|
||||||
|
maintenanceCost: [elemCost('crystal', 0.4), elemCost('light', 0.4)],
|
||||||
|
damage: 20,
|
||||||
|
attackSpeed: 1.5,
|
||||||
|
hp: 50,
|
||||||
|
armorPierce: 0.35,
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 3,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'dual_attunement',
|
||||||
|
attunements: ['enchanter', 'fabricator'],
|
||||||
|
levels: [5, 5],
|
||||||
|
},
|
||||||
|
tier: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Quicksilver Golem - Water + Metal fusion
|
||||||
|
quicksilverGolem: {
|
||||||
|
id: 'quicksilverGolem',
|
||||||
|
name: 'Quicksilver Golem',
|
||||||
|
description: 'Liquid metal that flows around defenses. Fast and hard to dodge.',
|
||||||
|
baseManaType: 'metal',
|
||||||
|
summonCost: [elemCost('metal', 6), elemCost('water', 6)],
|
||||||
|
maintenanceCost: [elemCost('metal', 0.3), elemCost('water', 0.3)],
|
||||||
|
damage: 8,
|
||||||
|
attackSpeed: 4.0,
|
||||||
|
hp: 40,
|
||||||
|
armorPierce: 0.3,
|
||||||
|
isAoe: false,
|
||||||
|
aoeTargets: 1,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'dual_attunement',
|
||||||
|
attunements: ['enchanter', 'fabricator'],
|
||||||
|
levels: [5, 5],
|
||||||
|
},
|
||||||
|
tier: 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Voidstone Golem - Void + Earth fusion (ultimate)
|
||||||
|
voidstoneGolem: {
|
||||||
|
id: 'voidstoneGolem',
|
||||||
|
name: 'Voidstone Golem',
|
||||||
|
description: 'Earth infused with void energy. The ultimate golem construct.',
|
||||||
|
baseManaType: 'earth',
|
||||||
|
summonCost: [elemCost('earth', 15), elemCost('void', 8)],
|
||||||
|
maintenanceCost: [elemCost('earth', 0.3), elemCost('void', 0.6)],
|
||||||
|
damage: 40,
|
||||||
|
attackSpeed: 0.6,
|
||||||
|
hp: 100,
|
||||||
|
armorPierce: 0.6,
|
||||||
|
isAoe: true,
|
||||||
|
aoeTargets: 3,
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'dual_attunement',
|
||||||
|
attunements: ['enchanter', 'fabricator'],
|
||||||
|
levels: [5, 5],
|
||||||
|
},
|
||||||
|
tier: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// ─── Golem Definitions Index ─────────────────────────────────
|
||||||
|
// Re-exports from all golem modules
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export type { GolemDef, GolemManaCost } from './types';
|
||||||
|
|
||||||
|
// Re-export data
|
||||||
|
export { GOLEMS_DEF } from './data';
|
||||||
|
|
||||||
|
// Re-export utility functions
|
||||||
|
export {
|
||||||
|
getGolemSlots,
|
||||||
|
isGolemUnlocked,
|
||||||
|
getUnlockedGolems,
|
||||||
|
getGolemDamage,
|
||||||
|
getGolemAttackSpeed,
|
||||||
|
getGolemFloorDuration,
|
||||||
|
getGolemMaintenanceMultiplier,
|
||||||
|
canAffordGolemSummon,
|
||||||
|
deductGolemSummonCost,
|
||||||
|
canAffordGolemMaintenance,
|
||||||
|
deductGolemMaintenance,
|
||||||
|
} from './utils';
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// ─── Golem Types ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { SpellCost } from '../types';
|
||||||
|
|
||||||
|
// Golem mana cost helper
|
||||||
|
export function elemCost(element: string, amount: number): SpellCost {
|
||||||
|
return { type: 'element', element, amount };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rawCost(amount: number): SpellCost {
|
||||||
|
return { type: 'raw', amount };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GolemManaCost {
|
||||||
|
type: 'raw' | 'element';
|
||||||
|
element?: string;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
unlockCondition: {
|
||||||
|
type: 'attunement_level' | 'mana_unlocked' | 'dual_attunement';
|
||||||
|
attunement?: string;
|
||||||
|
level?: number;
|
||||||
|
manaType?: string;
|
||||||
|
attunements?: string[];
|
||||||
|
levels?: number[];
|
||||||
|
};
|
||||||
|
tier: number; // Power tier (1-4)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user