Completely remove legacy skill system and tests
This commit is contained in:
@@ -1,15 +1,14 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-14T10:03:17.211Z
|
Generated: 2026-05-15T16:50:47.281Z
|
||||||
Found: 8 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 7 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. Processed 174 files (2.4s) (27 warnings)
|
1. Processed 156 files (1.8s) (29 warnings)
|
||||||
2. 1) data/equipment/index.ts > data/equipment/utils.ts
|
2. 1) data/equipment/index.ts > data/equipment/utils.ts
|
||||||
3. 2) data/golems/index.ts > data/golems/utils.ts
|
3. 2) data/golems/index.ts > data/golems/utils.ts
|
||||||
4. 3) stores/combat-actions.ts > stores/combatStore.ts
|
4. 3) stores/combat-actions.ts > stores/combatStore.ts
|
||||||
5. 4) stores/combatStore.ts > stores/gameStore.ts
|
5. 4) stores/combatStore.ts > stores/gameStore.ts
|
||||||
6. 5) stores/combatStore.ts > stores/gameStore.ts > stores/gameActions.ts
|
6. 5) stores/combatStore.ts > stores/gameStore.ts > stores/gameActions.ts
|
||||||
7. 6) stores/combatStore.ts > stores/gameStore.ts > stores/gameActions.ts > stores/skillStore.ts
|
7. 6) stores/combatStore.ts > stores/gameStore.ts > stores/gameLoopActions.ts
|
||||||
8. 7) stores/combatStore.ts > stores/gameStore.ts > stores/gameLoopActions.ts
|
|
||||||
|
|
||||||
## How to fix
|
## How to fix
|
||||||
1. Identify which import in the chain can be extracted to a shared types/utils file.
|
1. Identify which import in the chain can be extracted to a shared types/utils file.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-14T10:03:14.529Z",
|
"generated": "2026-05-15T16:50:45.254Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
@@ -431,86 +431,6 @@
|
|||||||
"computed-stats.ts",
|
"computed-stats.ts",
|
||||||
"types.ts"
|
"types.ts"
|
||||||
],
|
],
|
||||||
"skill-evolution-modules/elemental-attunement.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/enchanting-skills.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/focused-mind.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/guardian-skills.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/hybrid-skills.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/index.ts": [
|
|
||||||
"skill-evolution-modules/elemental-attunement.ts",
|
|
||||||
"skill-evolution-modules/enchanting-skills.ts",
|
|
||||||
"skill-evolution-modules/focused-mind.ts",
|
|
||||||
"skill-evolution-modules/guardian-skills.ts",
|
|
||||||
"skill-evolution-modules/hybrid-skills.ts",
|
|
||||||
"skill-evolution-modules/insight-harvest.ts",
|
|
||||||
"skill-evolution-modules/invocation-skills.ts",
|
|
||||||
"skill-evolution-modules/knowledge-retention.ts",
|
|
||||||
"skill-evolution-modules/mana-utility-skills.ts",
|
|
||||||
"skill-evolution-modules/mana-well-flow.ts",
|
|
||||||
"skill-evolution-modules/quick-learner.ts",
|
|
||||||
"skill-evolution-modules/types.ts",
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/insight-harvest.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/invocation-skills.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/knowledge-retention.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/learning-skills.ts": [
|
|
||||||
"skill-evolution-modules/focused-mind.ts",
|
|
||||||
"skill-evolution-modules/insight-harvest.ts",
|
|
||||||
"skill-evolution-modules/knowledge-retention.ts",
|
|
||||||
"skill-evolution-modules/quick-learner.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/magic-skills.ts": [
|
|
||||||
"skill-evolution-modules/elemental-attunement.ts",
|
|
||||||
"skill-evolution-modules/mana-utility-skills.ts",
|
|
||||||
"skill-evolution-modules/mana-well-flow.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/mana-utility-skills.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/mana-well-flow.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/quick-learner.ts": [
|
|
||||||
"skill-evolution-modules/utils.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/types.ts": [
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution-modules/utils.ts": [
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"skill-evolution.ts": [
|
|
||||||
"skill-evolution-modules/index.ts"
|
|
||||||
],
|
|
||||||
"special-effects.ts": [
|
"special-effects.ts": [
|
||||||
"upgrade-effects.types.ts"
|
"upgrade-effects.types.ts"
|
||||||
],
|
],
|
||||||
@@ -553,7 +473,6 @@
|
|||||||
"data/equipment/index.ts",
|
"data/equipment/index.ts",
|
||||||
"data/golems/index.ts",
|
"data/golems/index.ts",
|
||||||
"effects.ts",
|
"effects.ts",
|
||||||
"skill-evolution.ts",
|
|
||||||
"special-effects.ts",
|
"special-effects.ts",
|
||||||
"store-modules/activity-log.ts",
|
"store-modules/activity-log.ts",
|
||||||
"store-modules/computed-stats.ts",
|
"store-modules/computed-stats.ts",
|
||||||
@@ -602,7 +521,6 @@
|
|||||||
"store/computed.ts": [
|
"store/computed.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
"effects.ts",
|
"effects.ts",
|
||||||
"skill-evolution.ts",
|
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"upgrade-effects.ts",
|
"upgrade-effects.ts",
|
||||||
"upgrade-effects.types.ts"
|
"upgrade-effects.types.ts"
|
||||||
@@ -676,7 +594,6 @@
|
|||||||
],
|
],
|
||||||
"store/skillSlice.ts": [
|
"store/skillSlice.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
"skill-evolution.ts",
|
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"upgrade-effects.ts"
|
"upgrade-effects.ts"
|
||||||
],
|
],
|
||||||
@@ -717,7 +634,6 @@
|
|||||||
"stores/combatStore.ts",
|
"stores/combatStore.ts",
|
||||||
"stores/gameStore.ts",
|
"stores/gameStore.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/skillStore.ts",
|
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"upgrade-effects.ts"
|
"upgrade-effects.ts"
|
||||||
@@ -726,7 +642,6 @@
|
|||||||
"stores/combatStore.ts",
|
"stores/combatStore.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
"stores/skillStore.ts",
|
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
"upgrade-effects.ts",
|
"upgrade-effects.ts",
|
||||||
"utils/index.ts"
|
"utils/index.ts"
|
||||||
@@ -739,7 +654,6 @@
|
|||||||
"stores/gameStore.ts",
|
"stores/gameStore.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
"stores/skillStore.ts",
|
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
"utils/index.ts"
|
"utils/index.ts"
|
||||||
],
|
],
|
||||||
@@ -748,7 +662,6 @@
|
|||||||
"stores/combatStore.ts",
|
"stores/combatStore.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
"stores/skillStore.ts",
|
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
"utils/index.ts"
|
"utils/index.ts"
|
||||||
],
|
],
|
||||||
@@ -762,7 +675,6 @@
|
|||||||
"stores/gameLoopActions.ts",
|
"stores/gameLoopActions.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
"stores/skillStore.ts",
|
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
"upgrade-effects.ts",
|
"upgrade-effects.ts",
|
||||||
"utils/index.ts"
|
"utils/index.ts"
|
||||||
@@ -777,7 +689,6 @@
|
|||||||
"stores/gameStore.ts",
|
"stores/gameStore.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
"stores/skillStore.ts",
|
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
"utils/index.ts"
|
"utils/index.ts"
|
||||||
],
|
],
|
||||||
@@ -789,13 +700,6 @@
|
|||||||
"constants.ts",
|
"constants.ts",
|
||||||
"types.ts"
|
"types.ts"
|
||||||
],
|
],
|
||||||
"stores/skillStore.ts": [
|
|
||||||
"constants.ts",
|
|
||||||
"skill-evolution.ts",
|
|
||||||
"stores/combatStore.ts",
|
|
||||||
"stores/manaStore.ts",
|
|
||||||
"types.ts"
|
|
||||||
],
|
|
||||||
"stores/uiStore.ts": [],
|
"stores/uiStore.ts": [],
|
||||||
"study-slice.ts": [
|
"study-slice.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
@@ -832,7 +736,6 @@
|
|||||||
"types/spells.ts": [],
|
"types/spells.ts": [],
|
||||||
"upgrade-effects.ts": [
|
"upgrade-effects.ts": [
|
||||||
"dynamic-compute.ts",
|
"dynamic-compute.ts",
|
||||||
"skill-evolution.ts",
|
|
||||||
"special-effects.ts",
|
"special-effects.ts",
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"upgrade-effects.types.ts"
|
"upgrade-effects.types.ts"
|
||||||
|
|||||||
+2
-139
@@ -105,7 +105,6 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── GameStateDebug.tsx
|
│ │ │ │ ├── GameStateDebug.tsx
|
||||||
│ │ │ │ ├── GolemDebug.tsx
|
│ │ │ │ ├── GolemDebug.tsx
|
||||||
│ │ │ │ ├── PactDebug.tsx
|
│ │ │ │ ├── PactDebug.tsx
|
||||||
│ │ │ │ ├── SkillDebug.tsx
|
|
||||||
│ │ │ │ └── index.tsx
|
│ │ │ │ └── index.tsx
|
||||||
│ │ │ ├── layout/
|
│ │ │ ├── layout/
|
||||||
│ │ │ │ ├── Header.tsx
|
│ │ │ │ ├── Header.tsx
|
||||||
@@ -121,38 +120,6 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── StudyStatsSection.tsx
|
│ │ │ │ ├── StudyStatsSection.tsx
|
||||||
│ │ │ │ ├── UpgradeEffectsSection.tsx
|
│ │ │ │ ├── UpgradeEffectsSection.tsx
|
||||||
│ │ │ │ └── index.tsx
|
│ │ │ │ └── index.tsx
|
||||||
│ │ │ ├── tabs/
|
|
||||||
│ │ │ │ ├── AchievementsTab.tsx
|
|
||||||
│ │ │ │ ├── ActivityLog.tsx
|
|
||||||
│ │ │ │ ├── AttunementsTab.tsx
|
|
||||||
│ │ │ │ ├── CategorySkillsList.tsx
|
|
||||||
│ │ │ │ ├── CombatStatsPanel.tsx
|
|
||||||
│ │ │ │ ├── CraftingTab.tsx
|
|
||||||
│ │ │ │ ├── DebugTab.tsx
|
|
||||||
│ │ │ │ ├── EnchantmentsPanel.tsx
|
|
||||||
│ │ │ │ ├── EquipmentControls.tsx
|
|
||||||
│ │ │ │ ├── EquipmentInventory.tsx
|
|
||||||
│ │ │ │ ├── EquipmentSlotGrid.tsx
|
|
||||||
│ │ │ │ ├── EquipmentTab.tsx
|
|
||||||
│ │ │ │ ├── FloorControls.tsx
|
|
||||||
│ │ │ │ ├── GolemancyTab.tsx
|
|
||||||
│ │ │ │ ├── GuardianPanel.tsx
|
|
||||||
│ │ │ │ ├── LootTab.tsx
|
|
||||||
│ │ │ │ ├── MilestoneProgress.tsx
|
|
||||||
│ │ │ │ ├── PrestigeTab.tsx
|
|
||||||
│ │ │ │ ├── RoomDisplay.tsx
|
|
||||||
│ │ │ │ ├── SkillCategoryHeader.tsx
|
|
||||||
│ │ │ │ ├── SkillMultipliers.tsx
|
|
||||||
│ │ │ │ ├── SkillRow.tsx
|
|
||||||
│ │ │ │ ├── SpellsTab.tsx
|
|
||||||
│ │ │ │ ├── SpireActiveSpells.tsx
|
|
||||||
│ │ │ │ ├── SpireGolems.tsx
|
|
||||||
│ │ │ │ ├── SpireHeader.tsx
|
|
||||||
│ │ │ │ ├── SpireTab.tsx
|
|
||||||
│ │ │ │ ├── StatsTab.tsx
|
|
||||||
│ │ │ │ ├── StudyProgress.tsx
|
|
||||||
│ │ │ │ ├── UpgradeDialog.tsx
|
|
||||||
│ │ │ │ └── index.ts
|
|
||||||
│ │ │ ├── AchievementsDisplay.tsx
|
│ │ │ ├── AchievementsDisplay.tsx
|
||||||
│ │ │ ├── ActionButtons.tsx
|
│ │ │ ├── ActionButtons.tsx
|
||||||
│ │ │ ├── ActivityLogPanel.tsx
|
│ │ │ ├── ActivityLogPanel.tsx
|
||||||
@@ -163,7 +130,6 @@ Mana-Loop/
|
|||||||
│ │ │ ├── GameContext.tsx
|
│ │ │ ├── GameContext.tsx
|
||||||
│ │ │ ├── GameToast.tsx
|
│ │ │ ├── GameToast.tsx
|
||||||
│ │ │ ├── ManaDisplay.tsx
|
│ │ │ ├── ManaDisplay.tsx
|
||||||
│ │ │ ├── SkillsTab.tsx
|
|
||||||
│ │ │ ├── SpellsTab.tsx
|
│ │ │ ├── SpellsTab.tsx
|
||||||
│ │ │ ├── StatsTab.tsx
|
│ │ │ ├── StatsTab.tsx
|
||||||
│ │ │ ├── StudyProgress.tsx
|
│ │ │ ├── StudyProgress.tsx
|
||||||
@@ -191,7 +157,6 @@ Mana-Loop/
|
|||||||
│ │ │ ├── separator.tsx
|
│ │ │ ├── separator.tsx
|
||||||
│ │ │ ├── sheet.tsx
|
│ │ │ ├── sheet.tsx
|
||||||
│ │ │ ├── skeleton.tsx
|
│ │ │ ├── skeleton.tsx
|
||||||
│ │ │ ├── skill-row.tsx
|
|
||||||
│ │ │ ├── stat-row.tsx
|
│ │ │ ├── stat-row.tsx
|
||||||
│ │ │ ├── stepper.tsx
|
│ │ │ ├── stepper.tsx
|
||||||
│ │ │ ├── switch.tsx
|
│ │ │ ├── switch.tsx
|
||||||
@@ -209,20 +174,9 @@ 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/
|
│ │ │ ├── store-method-tests/
|
||||||
│ │ │ ├── bug-fixes.test.ts
|
│ │ │ ├── bug-fixes.test.ts
|
||||||
│ │ │ ├── computed-stats.test.ts
|
│ │ │ └── computed-stats.test.ts
|
||||||
│ │ │ ├── skill-system.test.ts
|
|
||||||
│ │ │ └── skills.test.ts
|
|
||||||
│ │ ├── attunements/
|
│ │ ├── attunements/
|
||||||
│ │ │ ├── data.ts
|
│ │ │ ├── data.ts
|
||||||
│ │ │ ├── index.ts
|
│ │ │ ├── index.ts
|
||||||
@@ -246,20 +200,6 @@ Mana-Loop/
|
|||||||
│ │ │ ├── index.ts
|
│ │ │ ├── index.ts
|
||||||
│ │ │ ├── prestige.ts
|
│ │ │ ├── prestige.ts
|
||||||
│ │ │ ├── rooms.ts
|
│ │ │ ├── rooms.ts
|
||||||
│ │ │ ├── skills-combat.ts
|
|
||||||
│ │ │ ├── skills-core.ts
|
|
||||||
│ │ │ ├── skills-crafting.ts
|
|
||||||
│ │ │ ├── skills-element-caps.ts
|
|
||||||
│ │ │ ├── skills-enchant.ts
|
|
||||||
│ │ │ ├── skills-golemancy.ts
|
|
||||||
│ │ │ ├── skills-hybrid.ts
|
|
||||||
│ │ │ ├── skills-invocation.ts
|
|
||||||
│ │ │ ├── skills-research.ts
|
|
||||||
│ │ │ ├── skills-v2-defs.ts
|
|
||||||
│ │ │ ├── skills-v2-registry.ts
|
|
||||||
│ │ │ ├── skills-v2-types.ts
|
|
||||||
│ │ │ ├── skills-v2.ts
|
|
||||||
│ │ │ ├── skills.ts
|
|
||||||
│ │ │ └── spells.ts
|
│ │ │ └── spells.ts
|
||||||
│ │ ├── crafting-actions/
|
│ │ ├── crafting-actions/
|
||||||
│ │ │ ├── application-actions.ts
|
│ │ │ ├── application-actions.ts
|
||||||
@@ -315,13 +255,7 @@ Mana-Loop/
|
|||||||
│ │ │ ├── enchantment-types.ts
|
│ │ │ ├── enchantment-types.ts
|
||||||
│ │ │ └── loot-drops.ts
|
│ │ │ └── loot-drops.ts
|
||||||
│ │ ├── hooks/
|
│ │ ├── hooks/
|
||||||
│ │ │ ├── useGameDerived.ts
|
│ │ │ └── useGameDerived.ts
|
||||||
│ │ │ └── useSkillUpgradeSelection.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/
|
│ │ │ ├── crafting-modules/
|
||||||
│ │ │ │ ├── initial-state.ts
|
│ │ │ │ ├── initial-state.ts
|
||||||
@@ -338,7 +272,6 @@ Mana-Loop/
|
|||||||
│ │ │ ├── manaSlice.ts
|
│ │ │ ├── manaSlice.ts
|
||||||
│ │ │ ├── pactSlice.ts
|
│ │ │ ├── pactSlice.ts
|
||||||
│ │ │ ├── prestigeSlice.ts
|
│ │ │ ├── prestigeSlice.ts
|
||||||
│ │ │ ├── skillSlice.ts
|
|
||||||
│ │ │ └── timeSlice.ts
|
│ │ │ └── timeSlice.ts
|
||||||
│ │ ├── store-modules/
|
│ │ ├── store-modules/
|
||||||
│ │ │ ├── {room-utils,enemy-utils,initial-state,activity-log,store-actions}/
|
│ │ │ ├── {room-utils,enemy-utils,initial-state,activity-log,store-actions}/
|
||||||
@@ -349,68 +282,7 @@ Mana-Loop/
|
|||||||
│ │ │ ├── room-utils.ts
|
│ │ │ ├── room-utils.ts
|
||||||
│ │ │ ├── store-actions.ts
|
│ │ │ ├── store-actions.ts
|
||||||
│ │ │ └── tick-logic.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__/
|
|
||||||
│ │ │ │ ├── archive/
|
|
||||||
│ │ │ │ │ ├── store-methods.test.ts
|
|
||||||
│ │ │ │ │ └── stores.test.ts
|
|
||||||
│ │ │ │ ├── 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/
|
|
||||||
│ │ │ │ ├── computed-stats.test.ts
|
|
||||||
│ │ │ │ ├── equipment.test.ts
|
|
||||||
│ │ │ │ ├── mana-conversion-fix.test.ts
|
|
||||||
│ │ │ │ ├── mana.test.ts
|
|
||||||
│ │ │ │ ├── regen.test.ts
|
|
||||||
│ │ │ │ ├── skill.test.ts
|
|
||||||
│ │ │ │ ├── spell-cost.test.ts
|
|
||||||
│ │ │ │ ├── spire-exit-action.test.ts
|
|
||||||
│ │ │ │ └── spire-tab-refresh.test.ts
|
|
||||||
│ │ │ ├── attunementStore.ts
|
│ │ │ ├── attunementStore.ts
|
||||||
│ │ │ ├── combat-actions.ts
|
│ │ │ ├── combat-actions.ts
|
||||||
│ │ │ ├── combatStore.ts
|
│ │ │ ├── combatStore.ts
|
||||||
@@ -424,20 +296,12 @@ Mana-Loop/
|
|||||||
│ │ │ ├── manaStore.ts
|
│ │ │ ├── manaStore.ts
|
||||||
│ │ │ ├── prestigeStore.ts
|
│ │ │ ├── prestigeStore.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
|
||||||
│ │ │ ├── equipment.ts
|
│ │ │ ├── equipment.ts
|
||||||
│ │ │ ├── game.ts
|
│ │ │ ├── game.ts
|
||||||
│ │ │ ├── index.ts
|
│ │ │ ├── index.ts
|
||||||
│ │ │ ├── skills.ts
|
|
||||||
│ │ │ └── spells.ts
|
│ │ │ └── spells.ts
|
||||||
│ │ ├── utils/
|
│ │ ├── utils/
|
||||||
│ │ │ ├── activity-log.ts
|
│ │ │ ├── activity-log.ts
|
||||||
@@ -464,7 +328,6 @@ Mana-Loop/
|
|||||||
│ │ ├── effects.ts.fix
|
│ │ ├── effects.ts.fix
|
||||||
│ │ ├── formatting.ts
|
│ │ ├── formatting.ts
|
||||||
│ │ ├── navigation-slice.ts
|
│ │ ├── navigation-slice.ts
|
||||||
│ │ ├── skills.test.ts
|
|
||||||
│ │ ├── special-effects.ts
|
│ │ ├── special-effects.ts
|
||||||
│ │ ├── store.test.ts
|
│ │ ├── store.test.ts
|
||||||
│ │ ├── store.ts
|
│ │ ├── store.ts
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useSkillStore } from '@/lib/game/stores';
|
|
||||||
import { SKILL_CATEGORIES } from '@/lib/game/constants';
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import { SkillUpgradeDialog } from './SkillsTab/SkillUpgradeDialog';
|
|
||||||
import { SkillStudyProgress } from './SkillsTab/SkillStudyProgress';
|
|
||||||
import { SkillCategory } from './SkillsTab/SkillCategory';
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
|
|
||||||
export function SkillsTab() {
|
|
||||||
const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
|
|
||||||
const [upgradeDialogSkill, setUpgradeDialogSkill] = useState<string | null>(null);
|
|
||||||
const [upgradeDialogMilestone, setUpgradeDialogMilestone] = useState<5 | 10>(5);
|
|
||||||
|
|
||||||
const handleUpgradeClick = (skillId: string, milestone: 5 | 10) => {
|
|
||||||
setUpgradeDialogSkill(skillId);
|
|
||||||
setUpgradeDialogMilestone(milestone);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpgradeClose = () => {
|
|
||||||
setUpgradeDialogSkill(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="SkillsTab">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Upgrade Selection Dialog */}
|
|
||||||
<SkillUpgradeDialog
|
|
||||||
skillId={upgradeDialogSkill}
|
|
||||||
milestone={upgradeDialogMilestone}
|
|
||||||
onClose={handleUpgradeClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Current Study Progress */}
|
|
||||||
{currentStudyTarget && currentStudyTarget.type === 'skill' && (
|
|
||||||
<Card className="bg-gray-900/80 border-purple-600/50">
|
|
||||||
<CardContent className="pt-4">
|
|
||||||
<SkillStudyProgress />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{SKILL_CATEGORIES.map((cat) => (
|
|
||||||
<SkillCategory
|
|
||||||
key={cat.id}
|
|
||||||
categoryId={cat.id}
|
|
||||||
onUpgradeClick={handleUpgradeClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SkillsTab.displayName = "SkillsTab";
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { BookOpen } from 'lucide-react';
|
|
||||||
import { useSkillStore } from '@/lib/game/stores';
|
|
||||||
import { useManaStore } from '@/lib/game/stores';
|
|
||||||
|
|
||||||
export function SkillDebug() {
|
|
||||||
const skills = useSkillStore((s) => s.skills);
|
|
||||||
|
|
||||||
const incrementSkillLevel = (skillId: string) => {
|
|
||||||
useSkillStore.getState().incrementSkillLevel(skillId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setSkillLevel = (skillId: string, level: number) => {
|
|
||||||
useSkillStore.getState().setSkillLevel(skillId, level);
|
|
||||||
};
|
|
||||||
|
|
||||||
const unlockAllEffects = () => {
|
|
||||||
const effectIds = [
|
|
||||||
'spell_manaBolt', 'spell_manaStrike', 'spell_fireball', 'spell_emberShot',
|
|
||||||
'spell_waterJet', 'spell_iceShard', 'spell_gust', 'spell_windSlash',
|
|
||||||
'spell_stoneBullet', 'spell_rockSpike', 'spell_lightLance', 'spell_radiance',
|
|
||||||
'spell_shadowBolt', 'spell_darkPulse', 'spell_drain',
|
|
||||||
'spell_inferno', 'spell_flameWave', 'spell_tidalWave', 'spell_iceStorm',
|
|
||||||
'spell_hurricane', 'spell_windBlade', 'spell_earthquake', 'spell_stoneBarrage',
|
|
||||||
'spell_solarFlare', 'spell_divineSmite', 'spell_voidRift', 'spell_shadowStorm',
|
|
||||||
'spell_pyroclasm', 'spell_tsunami', 'spell_meteorStrike',
|
|
||||||
'spell_spark', 'spell_lightningBolt', 'spell_chainLightning',
|
|
||||||
'spell_stormCall', 'spell_thunderStrike',
|
|
||||||
'spell_metalShard', 'spell_ironFist', 'spell_steelTempest', 'spell_furnaceBlast',
|
|
||||||
'spell_sandBlast', 'spell_sandstorm', 'spell_desertWind', 'spell_duneCollapse',
|
|
||||||
'mana_cap_50', 'mana_cap_100', 'mana_regen_1', 'mana_regen_2', 'mana_regen_5',
|
|
||||||
'click_mana_1', 'click_mana_3',
|
|
||||||
'damage_5', 'damage_10', 'damage_pct_10', 'crit_5', 'attack_speed_10',
|
|
||||||
'meditate_10', 'study_10', 'insight_5',
|
|
||||||
'spell_echo_10', 'guardian_dmg_10', 'overpower_80',
|
|
||||||
'weapon_mana_cap_20', 'weapon_mana_cap_50', 'weapon_mana_cap_100',
|
|
||||||
'weapon_mana_regen_1', 'weapon_mana_regen_2', 'weapon_mana_regen_5',
|
|
||||||
'sword_fire', 'sword_frost', 'sword_lightning', 'sword_void'
|
|
||||||
];
|
|
||||||
useManaStore.setState((prev: any) => {
|
|
||||||
const currentEffects = prev.unlockedEffects || [];
|
|
||||||
const newEffects = [...currentEffects];
|
|
||||||
effectIds.forEach(id => {
|
|
||||||
if (!newEffects.includes(id)) {
|
|
||||||
newEffects.push(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { ...prev, unlockedEffects: newEffects };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700 md:col-span-2">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-purple-400 text-sm flex items-center gap-2">
|
|
||||||
<BookOpen className="w-4 h-4" />
|
|
||||||
Skill Research Debug
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* Enchanting Skills */}
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-400 mb-2">Enchanting Skills:</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
// Level up all enchanting skills by 1
|
|
||||||
const enchantSkills = ['enchanting', 'efficientEnchant', 'enchantSpeed', 'essenceRefining'];
|
|
||||||
enchantSkills.forEach(skillId => {
|
|
||||||
const currentLevel = skills[skillId] || 0;
|
|
||||||
if (currentLevel < 10) {
|
|
||||||
incrementSkillLevel(skillId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+1 All Enchanting
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
// Max all enchanting skills
|
|
||||||
const enchantSkills = ['enchanting', 'efficientEnchant', 'enchantSpeed', 'essenceRefining'];
|
|
||||||
enchantSkills.forEach(skillId => {
|
|
||||||
setSkillLevel(skillId, 10);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Max All Enchanting
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mana Skills */}
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-400 mb-2">Mana Skills:</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const manaSkills = ['manaWell', 'manaFlow', 'manaOverflow', 'fireManaCap', 'waterManaCap', 'airManaCap', 'earthManaCap', 'lightManaCap', 'darkManaCap', 'deathManaCap', 'metalManaCap', 'sandManaCap', 'lightningManaCap', 'transferenceManaCap'];
|
|
||||||
manaSkills.forEach(skillId => {
|
|
||||||
const currentLevel = skills[skillId] || 0;
|
|
||||||
if (currentLevel < 10) {
|
|
||||||
incrementSkillLevel(skillId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+1 All Mana
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const manaSkills = ['manaWell', 'manaFlow', 'manaOverflow', 'fireManaCap', 'waterManaCap', 'airManaCap', 'earthManaCap', 'lightManaCap', 'darkManaCap', 'deathManaCap', 'metalManaCap', 'sandManaCap', 'lightningManaCap', 'transferenceManaCap'];
|
|
||||||
manaSkills.forEach(skillId => {
|
|
||||||
setSkillLevel(skillId, 10);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Max All Mana
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Study Skills */}
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-400 mb-2">Study Skills:</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const studySkills = ['quickLearner', 'focusedMind', 'meditation', 'knowledgeRetention'];
|
|
||||||
studySkills.forEach(skillId => {
|
|
||||||
const currentLevel = skills[skillId] || 0;
|
|
||||||
if (currentLevel < 10) {
|
|
||||||
incrementSkillLevel(skillId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+1 All Study
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const studySkills = ['quickLearner', 'focusedMind', 'meditation', 'knowledgeRetention'];
|
|
||||||
studySkills.forEach(skillId => {
|
|
||||||
setSkillLevel(skillId, 10);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Max All Study
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Crafting Skills */}
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-400 mb-2">Crafting Skills:</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const craftSkills = ['effCrafting', 'fieldRepair', 'elemCrafting'];
|
|
||||||
craftSkills.forEach(skillId => {
|
|
||||||
const currentLevel = skills[skillId] || 0;
|
|
||||||
if (currentLevel < 10) {
|
|
||||||
incrementSkillLevel(skillId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+1 All Crafting
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const craftSkills = ['effCrafting', 'fieldRepair', 'elemCrafting'];
|
|
||||||
craftSkills.forEach(skillId => {
|
|
||||||
setSkillLevel(skillId, 10);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Max All Crafting
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Research Effects */}
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-400 mb-2">Research Effects:</div>
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const researchSkills = [
|
|
||||||
'researchManaSpells', 'researchFireSpells', 'researchWaterSpells',
|
|
||||||
'researchAirSpells', 'researchEarthSpells', 'researchLightSpells',
|
|
||||||
'researchDarkSpells', 'researchLifeDeathSpells',
|
|
||||||
'researchAdvancedFire', 'researchAdvancedWater', 'researchAdvancedAir',
|
|
||||||
'researchAdvancedEarth', 'researchAdvancedLight', 'researchAdvancedDark',
|
|
||||||
'researchMasterFire', 'researchMasterWater', 'researchMasterEarth',
|
|
||||||
'researchDamageEffects', 'researchCombatEffects', 'researchManaEffects',
|
|
||||||
'researchAdvancedManaEffects', 'researchUtilityEffects'
|
|
||||||
];
|
|
||||||
researchSkills.forEach(skillId => {
|
|
||||||
setSkillLevel(skillId, 1);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Unlock All Research
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
unlockAllEffects();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Unlock All Effects
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Max All */}
|
|
||||||
<Separator className="bg-gray-700" />
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
|
||||||
onClick={() => {
|
|
||||||
// Max all skills to level 10
|
|
||||||
Object.keys(skills).forEach(skillId => {
|
|
||||||
setSkillLevel(skillId, 10);
|
|
||||||
});
|
|
||||||
// Unlock all effects
|
|
||||||
unlockAllEffects();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🚀 Max Everything
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SkillDebug.displayName = "SkillDebug";
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
|
|
||||||
import { useCombatStore, useManaStore, usePrestigeStore } from '@/lib/game/stores';
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
|
|
||||||
export function AchievementsTab() {
|
|
||||||
const achievements = useCombatStore((s) => s.achievements);
|
|
||||||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
|
||||||
const totalManaGathered = useManaStore((s) => s.totalManaGathered);
|
|
||||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
|
||||||
const totalSpellsCast = useCombatStore((s) => s.totalSpellsCast);
|
|
||||||
const totalDamageDealt = useCombatStore((s) => s.totalDamageDealt);
|
|
||||||
const totalCraftsCompleted = useCombatStore((s) => s.totalCraftsCompleted);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="AchievementsTab">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<AchievementsDisplay
|
|
||||||
achievements={achievements}
|
|
||||||
gameState={{
|
|
||||||
maxFloorReached,
|
|
||||||
totalManaGathered,
|
|
||||||
signedPacts,
|
|
||||||
totalSpellsCast,
|
|
||||||
totalDamageDealt,
|
|
||||||
totalCraftsCompleted,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AchievementsTab.displayName = "AchievementsTab";
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import type { ActivityLogEntry } from '@/lib/game/types';
|
|
||||||
|
|
||||||
interface ActivityLogProps {
|
|
||||||
activityLog?: ActivityLogEntry[];
|
|
||||||
maxEntries?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActivityLog({ activityLog, maxEntries = 50 }: ActivityLogProps) {
|
|
||||||
const entries = activityLog || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ScrollArea className="h-48">
|
|
||||||
<div className="space-y-1">
|
|
||||||
{entries.slice(0, maxEntries).map((entry, i) => {
|
|
||||||
const isLatest = i === 0;
|
|
||||||
const color = getEventStyle(entry.eventType);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={entry.id}
|
|
||||||
className={`text-xs ${isLatest ? 'text-gray-200 font-semibold' : color}`}
|
|
||||||
>
|
|
||||||
{entry.message}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{entries.length === 0 && (
|
|
||||||
<div className="text-xs text-gray-500 italic">No activity yet...</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEventStyle(eventType: string): 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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getTotalAttunementRegen, getAvailableSkillCategories, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL, getAttunementConversionRate } from '@/lib/game/data/attunements';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
import type { AttunementState } from '@/lib/game/types';
|
|
||||||
import { usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { Lock, TrendingUp } from 'lucide-react';
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
|
|
||||||
export function AttunementsTab() {
|
|
||||||
const attunements = usePrestigeStore((s) => s.attunements) || {};
|
|
||||||
const elements = useManaStore((s) => s.elements);
|
|
||||||
|
|
||||||
// Get active attunements
|
|
||||||
const activeAttunements = Object.entries(attunements)
|
|
||||||
.filter(([, state]) => state.active)
|
|
||||||
.map(([id]) => ATTUNEMENTS_DEF[id])
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
// Calculate total regen from attunements
|
|
||||||
const totalAttunementRegen = getTotalAttunementRegen(attunements);
|
|
||||||
|
|
||||||
// Get available skill categories
|
|
||||||
const availableCategories = getAvailableSkillCategories(attunements);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="AttunementsTab">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Overview Card */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 text-sm">Your Attunements</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-sm text-gray-400 mb-3">
|
|
||||||
Attunements are magical bonds tied to specific body locations. Each attunement grants unique capabilities,
|
|
||||||
mana regeneration, and access to specialized skills. Level them up to increase their power.
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Badge className="bg-teal-900/50 text-teal-300">
|
|
||||||
+{totalAttunementRegen.toFixed(1)} raw mana/hr
|
|
||||||
</Badge>
|
|
||||||
<Badge className="bg-purple-900/50 text-purple-300">
|
|
||||||
{activeAttunements.length} active attunement{activeAttunements.length !== 1 ? 's' : ''}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Attunement Slots */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{Object.entries(ATTUNEMENTS_DEF).map(([id, def]) => {
|
|
||||||
const state = attunements[id];
|
|
||||||
const isActive = state?.active;
|
|
||||||
const isUnlocked = state?.active || def.unlocked;
|
|
||||||
const level = state?.level || 1;
|
|
||||||
const xp = state?.experience || 0;
|
|
||||||
const xpNeeded = getAttunementXPForLevel(level + 1);
|
|
||||||
const xpProgress = xpNeeded > 0 ? (xp / xpNeeded) * 100 : 100;
|
|
||||||
const isMaxLevel = level >= MAX_ATTUNEMENT_LEVEL;
|
|
||||||
|
|
||||||
// Get primary mana element info
|
|
||||||
const primaryElem = def.primaryManaType ? ELEMENTS[def.primaryManaType] : null;
|
|
||||||
|
|
||||||
// Get current mana for this attunement's type
|
|
||||||
const currentMana = def.primaryManaType ? elements[def.primaryManaType]?.current || 0 : 0;
|
|
||||||
const maxMana = def.primaryManaType ? elements[def.primaryManaType]?.max || 50 : 50;
|
|
||||||
|
|
||||||
// Calculate level-scaled stats
|
|
||||||
const levelMult = Math.pow(1.5, level - 1);
|
|
||||||
const scaledRegen = def.rawManaRegen * levelMult;
|
|
||||||
const scaledConversion = getAttunementConversionRate(id, level);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={id}
|
|
||||||
className={`bg-gray-900/80 transition-all ${
|
|
||||||
isActive
|
|
||||||
? 'border-2 shadow-lg'
|
|
||||||
: isUnlocked
|
|
||||||
? 'border-gray-600'
|
|
||||||
: 'border-gray-800 opacity-70'
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
borderColor: isActive ? def.color : undefined,
|
|
||||||
boxShadow: isActive ? `0 0 20px ${def.color}30` : undefined
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-2xl">{def.icon}</span>
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-sm" style={{ color: isActive ? def.color : '#9CA3AF' }}>
|
|
||||||
{def.name}
|
|
||||||
</CardTitle>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{ATTUNEMENT_SLOT_NAMES[def.slot]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isUnlocked && (
|
|
||||||
<Lock className="w-4 h-4 text-gray-600" />
|
|
||||||
)}
|
|
||||||
{isActive && (
|
|
||||||
<Badge className="text-xs" style={{ backgroundColor: `${def.color}30`, color: def.color }}>
|
|
||||||
Lv.{level}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<p className="text-xs text-gray-400">{def.desc}</p>
|
|
||||||
|
|
||||||
{/* Mana Type */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="text-gray-500">Primary Mana</span>
|
|
||||||
{primaryElem ? (
|
|
||||||
<span style={{ color: primaryElem.color }}>
|
|
||||||
{primaryElem.sym} {primaryElem.name}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-purple-400">From Pacts</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mana bar (only for attunements with primary type) */}
|
|
||||||
{primaryElem && isActive && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Progress
|
|
||||||
value={(currentMana / maxMana) * 100}
|
|
||||||
className="h-2 bg-gray-800"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between text-xs text-gray-500">
|
|
||||||
<span>{currentMana.toFixed(1)}</span>
|
|
||||||
<span>/{maxMana}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats with level scaling */}
|
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
||||||
<div className="p-2 bg-gray-800/50 rounded">
|
|
||||||
<div className="text-gray-500">Raw Regen</div>
|
|
||||||
<div className="text-green-400 font-semibold">
|
|
||||||
+{scaledRegen.toFixed(2)}/hr
|
|
||||||
{level > 1 && <span className="text-xs ml-1">({((levelMult - 1) * 100).toFixed(0)}% bonus)</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-2 bg-gray-800/50 rounded">
|
|
||||||
<div className="text-gray-500">Conversion</div>
|
|
||||||
<div className="text-cyan-400 font-semibold">
|
|
||||||
{scaledConversion > 0 ? `${scaledConversion.toFixed(2)}/hr` : '—'}
|
|
||||||
{level > 1 && scaledConversion > 0 && <span className="text-xs ml-1">({((levelMult - 1) * 100).toFixed(0)}% bonus)</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* XP Progress Bar */}
|
|
||||||
{isUnlocked && state && !isMaxLevel && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="text-gray-500 flex items-center gap-1">
|
|
||||||
<TrendingUp className="w-3 h-3" />
|
|
||||||
XP Progress
|
|
||||||
</span>
|
|
||||||
<span className="text-amber-400">{xp} / {xpNeeded}</span>
|
|
||||||
</div>
|
|
||||||
<Progress
|
|
||||||
value={xpProgress}
|
|
||||||
className="h-2 bg-gray-800"
|
|
||||||
/>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{isMaxLevel ? 'Max Level' : `${xpNeeded - xp} XP to Level ${level + 1}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Max Level Indicator */}
|
|
||||||
{isMaxLevel && (
|
|
||||||
<div className="text-xs text-amber-400 text-center font-semibold">
|
|
||||||
✨ MAX LEVEL ✨
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Capabilities */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-xs text-gray-500">Capabilities</div>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{def.capabilities.map(cap => (
|
|
||||||
<Badge key={cap} variant="outline" className="text-xs">
|
|
||||||
{cap === 'enchanting' && '✨ Enchanting'}
|
|
||||||
{cap === 'disenchanting' && '🔄 Disenchant'} {/* TODO: Remove after bug 13 complete */}
|
|
||||||
{cap === 'pacts' && '🤝 Pacts'}
|
|
||||||
{cap === 'guardianPowers' && '💜 Guardian Powers'}
|
|
||||||
{cap === 'elementalMastery' && '🌟 Elem. Mastery'}
|
|
||||||
{cap === 'golemCrafting' && '🗿 Golems'}
|
|
||||||
{cap === 'gearCrafting' && '⚒️ Gear'}
|
|
||||||
{cap === 'earthShaping' && '⛰️ Earth Shaping'}
|
|
||||||
{!['enchanting', 'pacts', 'guardianPowers',
|
|
||||||
'elementalMastery', 'golemCrafting', 'gearCrafting', 'earthShaping'].includes(cap) && cap}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Unlock condition for locked attunements */}
|
|
||||||
{!isUnlocked && def.unlockCondition && (
|
|
||||||
<div className="text-xs text-amber-400 italic">
|
|
||||||
🔒 {def.unlockCondition}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Available Skills Summary */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 text-sm">Available Skill Categories</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-xs text-gray-400 mb-2">
|
|
||||||
Your attunements grant access to specialized skill categories:
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{availableCategories.map(cat => {
|
|
||||||
const attunement = Object.values(ATTUNEMENTS_DEF || {}).find(a =>
|
|
||||||
a.skillCategories.includes(cat) && attunements[a.id]?.active
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
key={cat}
|
|
||||||
className={attunement ? '' : 'bg-gray-700/50 text-gray-400'}
|
|
||||||
style={attunement ? {
|
|
||||||
backgroundColor: `${attunement.color}30`,
|
|
||||||
color: attunement.color
|
|
||||||
} : undefined}
|
|
||||||
>
|
|
||||||
{cat === 'mana' && '💧 Mana'}
|
|
||||||
{cat === 'study' && '📚 Study'}
|
|
||||||
{cat === 'research' && '🔮 Research'} {/* TODO: Remove after Bug 12 - research moved to mana */}
|
|
||||||
{cat === 'ascension' && '⭐ Ascension'}
|
|
||||||
{cat === 'enchant' && '✨ Enchanting'}
|
|
||||||
{cat === 'effectResearch' && '🔬 Effect Research'}
|
|
||||||
{cat === 'invocation' && '💜 Invocation'}
|
|
||||||
{cat === 'pact' && '🤝 Pact Mastery'}
|
|
||||||
{cat === 'fabrication' && '⚒️ Fabrication'}
|
|
||||||
{cat === 'golemancy' && '🗿 Golemancy'}
|
|
||||||
{!['mana', 'study', 'research', 'ascension', 'enchant', 'effectResearch',
|
|
||||||
'invocation', 'pact', 'fabrication', 'golemancy'].includes(cat) && cat}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AttunementsTab.displayName = "AttunementsTab";
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
// CategorySkillsList - Displays skills for a specific category
|
|
||||||
// Migrated to use hooks directly (removed GameStore prop)
|
|
||||||
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
|
||||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
|
||||||
import { useSkillStore, useGameStore, usePrestigeStore } from '@/lib/game/stores';
|
|
||||||
import { SkillRow } from './SkillRow';
|
|
||||||
import type { GameStore } from '@/lib/game/stores'; // Keep type import for backward compatibility
|
|
||||||
|
|
||||||
interface CategorySkillsListProps {
|
|
||||||
categoryId: string;
|
|
||||||
categoryName: string;
|
|
||||||
skills: Record<string, number>;
|
|
||||||
skillUpgrades: Record<string, string[]>;
|
|
||||||
skillTiers: Record<string, number>;
|
|
||||||
prestigeUpgrades: Record<string, number>;
|
|
||||||
studySpeedMult: number;
|
|
||||||
upgradeEffects: any;
|
|
||||||
currentStudyTarget: any;
|
|
||||||
onStartStudying: (skillId: string) => void;
|
|
||||||
onParallelStudy: (skillId: string) => void;
|
|
||||||
onCancelStudy: () => void;
|
|
||||||
onOpenUpgradeDialog: (skillId: string, milestone: 5 | 10) => void;
|
|
||||||
onTierUp: (skillId: string) => void;
|
|
||||||
pendingSelections: string[];
|
|
||||||
setPendingSelections: (selections: string[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CategorySkillsList({
|
|
||||||
categoryId,
|
|
||||||
categoryName,
|
|
||||||
skills,
|
|
||||||
skillUpgrades,
|
|
||||||
skillTiers,
|
|
||||||
prestigeUpgrades,
|
|
||||||
studySpeedMult,
|
|
||||||
upgradeEffects,
|
|
||||||
currentStudyTarget,
|
|
||||||
onStartStudying,
|
|
||||||
onParallelStudy,
|
|
||||||
onCancelStudy,
|
|
||||||
onOpenUpgradeDialog,
|
|
||||||
onTierUp,
|
|
||||||
pendingSelections,
|
|
||||||
setPendingSelections,
|
|
||||||
}: CategorySkillsListProps) {
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
|
|
||||||
const categorySkills = Object.entries(SKILLS_DEF || {})
|
|
||||||
.filter(([, def]) => def.category === categoryId)
|
|
||||||
.sort((a, b) => (a[1].tier || 0) - (b[1].tier || 0));
|
|
||||||
|
|
||||||
const toggleCollapse = () => setCollapsed(!collapsed);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-4">
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 mb-2 cursor-pointer"
|
|
||||||
onClick={toggleCollapse}
|
|
||||||
>
|
|
||||||
<span className="text-sm font-semibold text-gray-300">
|
|
||||||
{categoryName} ({categorySkills.length})
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500">
|
|
||||||
{collapsed ? '▼' : '▶'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!collapsed && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{categorySkills.map(([skillId, def]) => {
|
|
||||||
const skillLevel = skills[skillId] || 0;
|
|
||||||
const tier = skillTiers[skillId] || 0;
|
|
||||||
const tierMult = getTierMultiplier(skillId)(tier);
|
|
||||||
const isStudying = currentStudyTarget?.id === skillId;
|
|
||||||
const isParallel = currentStudyTarget?.type === 'parallel' && currentStudyTarget?.id === skillId;
|
|
||||||
|
|
||||||
// Get upgrade choices for this skill
|
|
||||||
const store = useGameStore.getState();
|
|
||||||
const { available, selected } = store.getSkillUpgradeChoices(skillId, tier as 5 | 10);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SkillRow
|
|
||||||
key={skillId}
|
|
||||||
skillId={skillId}
|
|
||||||
skillDef={def}
|
|
||||||
skillLevel={skillLevel}
|
|
||||||
tier={tier}
|
|
||||||
tierMult={tierMult}
|
|
||||||
isStudying={isStudying}
|
|
||||||
isParallel={isParallel}
|
|
||||||
studySpeedMult={studySpeedMult}
|
|
||||||
upgradeEffects={upgradeEffects}
|
|
||||||
availableUpgrades={available}
|
|
||||||
selectedUpgrades={selected}
|
|
||||||
pendingSelections={pendingSelections}
|
|
||||||
onToggleUpgrade={(upgradeId) => {
|
|
||||||
if (pendingSelections.includes(upgradeId)) {
|
|
||||||
setPendingSelections(pendingSelections.filter(id => id !== upgradeId));
|
|
||||||
} else {
|
|
||||||
setPendingSelections([...pendingSelections, upgradeId]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onStartStudying={() => onStartStudying(skillId)}
|
|
||||||
onParallelStudy={() => onParallelStudy(skillId)}
|
|
||||||
onTierUp={() => onTierUp(skillId)}
|
|
||||||
onOpenUpgradeDialog={(milestone) => onOpenUpgradeDialog(skillId, milestone)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
|
||||||
import { Zap, Shield, ShieldCheck, Wind, Heart, Mountain, BookOpen } from 'lucide-react';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
|
||||||
import type { CombatStatsPanelProps } from '@/lib/game/types';
|
|
||||||
import { useCombatStore } from '@/lib/game/stores';
|
|
||||||
import { useSkillStore } from '@/lib/game/stores';
|
|
||||||
import { usePrestigeStore } from '@/lib/game/stores';
|
|
||||||
|
|
||||||
export function CombatStatsPanel({
|
|
||||||
activeEquipmentSpells,
|
|
||||||
totalDPS,
|
|
||||||
calcDamage,
|
|
||||||
formatSpellCost,
|
|
||||||
getSpellCostColor,
|
|
||||||
SPELLS_DEF,
|
|
||||||
upgradeEffects,
|
|
||||||
canCastSpell,
|
|
||||||
studySpeedMult,
|
|
||||||
storeCurrentAction,
|
|
||||||
}: CombatStatsPanelProps) {
|
|
||||||
const golemancy = useCombatStore((s) => s.golemancy);
|
|
||||||
const equipmentSpellStates = useCombatStore((s) => s.equipmentSpellStates);
|
|
||||||
const skills = useSkillStore((s) => s.skills);
|
|
||||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
|
||||||
const activeGolems = golemancy.summonedGolems;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Combat Stats</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
Total DPS: <span className="text-amber-400 font-semibold">{storeCurrentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeEquipmentSpells.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-gray-500 font-semibold uppercase tracking-wider">Active Spells</div>
|
|
||||||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
|
||||||
const spellDef = SPELLS_DEF[spellId];
|
|
||||||
if (!spellDef) return null;
|
|
||||||
const spellState = equipmentSpellStates?.find(
|
|
||||||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
|
||||||
);
|
|
||||||
const progress = spellState?.castProgress || 0;
|
|
||||||
const canCast = canCastSpell(spellId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={`${spellId}-${equipmentId}`} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="text-xs font-semibold" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
|
||||||
{spellDef.name}
|
|
||||||
{spellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200 text-xs">Basic</Badge>}
|
|
||||||
{spellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100 text-xs">Legendary</Badge>}
|
|
||||||
</span>
|
|
||||||
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>
|
|
||||||
{canCast ? '✓' : '✗'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 mb-1">
|
|
||||||
⚔️ {fmt(calcDamage({ skills, signedPacts }, spellId))} dmg • {' '}
|
|
||||||
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
|
||||||
{formatSpellCost(spellDef.cost)}
|
|
||||||
</span>
|
|
||||||
{' '}• ⚡ {fmt(Math.floor(calcDamage({ skills, signedPacts }, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
|
||||||
</div>
|
|
||||||
{storeCurrentAction === 'climb' && (
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<div className="flex justify-between text-xs text-gray-500">
|
|
||||||
<span>Cast</span>
|
|
||||||
<span>{(progress * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min(100, progress * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color}99, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeGolems.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500 font-semibold uppercase tracking-wider">
|
|
||||||
<Mountain className="w-3 h-3" />
|
|
||||||
Active Golems
|
|
||||||
</div>
|
|
||||||
{activeGolems.map((summoned) => {
|
|
||||||
const golemDef = GOLEMS_DEF[summoned.golemId];
|
|
||||||
if (!golemDef) return null;
|
|
||||||
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
|
|
||||||
const damage = getGolemDamage(summoned.golemId, skills);
|
|
||||||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, skills);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={summoned.golemId} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Mountain className="w-3 h-3" style={{ color: elemColor }} />
|
|
||||||
<span className="text-xs font-semibold" style={{ color: elemColor }}>
|
|
||||||
{golemDef.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{golemDef.isAoe && (
|
|
||||||
<Badge variant="outline" className="text-xs py-0">AOE {golemDef.aoeTargets}</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr
|
|
||||||
</div>
|
|
||||||
{storeCurrentAction === 'climb' && summoned.attackProgress > 0 && (
|
|
||||||
<div className="space-y-0.5 mt-1">
|
|
||||||
<div className="flex justify-between text-xs text-gray-500">
|
|
||||||
<span>Attack</span>
|
|
||||||
<span>{Math.min(100, (summoned.attackProgress * 100)).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min(100, summoned.attackProgress * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${elemColor}99, ${elemColor})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="pt-2 border-t border-gray-700">
|
|
||||||
<div className="text-xs text-gray-500 mb-1">Study Speed: {Math.round(studySpeedMult * 100)}%</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmt(value: number): string {
|
|
||||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
|
||||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
|
||||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
|
||||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
|
||||||
return value.toFixed(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtDec(value: number): string {
|
|
||||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
|
||||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
|
||||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
|
||||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
|
||||||
return value.toFixed(0);
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { GameCard } from '@/components/ui/game-card';
|
|
||||||
import { SectionHeader } from '@/components/ui/section-header';
|
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
|
||||||
import { Scroll, Hammer, Sparkles, Anvil } from 'lucide-react';
|
|
||||||
import { fmt } from '@/lib/game/stores';
|
|
||||||
import {
|
|
||||||
EnchantmentDesigner,
|
|
||||||
EnchantmentPreparer,
|
|
||||||
EnchantmentApplier,
|
|
||||||
EquipmentCrafter,
|
|
||||||
} from '@/components/game/crafting';
|
|
||||||
import { useCombatStore, useCraftingStore } from '@/lib/game/stores';
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
import { useGameToast } from '@/components/game/GameToast';
|
|
||||||
import type { DesignEffect } from '@/lib/game/types';
|
|
||||||
|
|
||||||
export function CraftingTab() {
|
|
||||||
const showToast = useGameToast();
|
|
||||||
const currentAction = useCombatStore((s) => s.currentAction);
|
|
||||||
const designProgress = useCraftingStore((s) => s.designProgress);
|
|
||||||
const preparationProgress = useCraftingStore((s) => s.preparationProgress);
|
|
||||||
const applicationProgress = useCraftingStore((s) => s.applicationProgress);
|
|
||||||
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
|
||||||
const pauseApplication = useCraftingStore((s) => s.pauseApplication);
|
|
||||||
const resumeApplication = useCraftingStore((s) => s.resumeApplication);
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'fabricate' | 'enchant'>('fabricate');
|
|
||||||
const [enchantStage, setEnchantStage] = useState<'design' | 'prepare' | 'apply'>('design');
|
|
||||||
|
|
||||||
// Enchant state
|
|
||||||
const [selectedEquipmentType, setSelectedEquipmentType] = useState<string | null>(null);
|
|
||||||
const [selectedEffects, setSelectedEffects] = useState<DesignEffect[]>([]);
|
|
||||||
const [designName, setDesignName] = useState('');
|
|
||||||
const [selectedDesign, setSelectedDesign] = useState<string | null>(null);
|
|
||||||
const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Safe toFixed helper
|
|
||||||
const safeToFixed = (value: number | undefined, decimals: number = 0): string => {
|
|
||||||
if (value === undefined || isNaN(value)) return '0';
|
|
||||||
return value.toFixed(decimals);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Safe percentage calculation
|
|
||||||
const calcPercent = (progress: number, required: number): number => {
|
|
||||||
if (!required || required === 0) return 0;
|
|
||||||
return (progress / required) * 100;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle enchantment application with toast
|
|
||||||
const handleEnchantmentApplied = () => {
|
|
||||||
showToast('success', 'Enchantment Applied', 'The enchantment has been successfully applied!');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle enchantment capacity exceeded
|
|
||||||
const handleCapacityExceeded = (itemName: string, used: number, total: number) => {
|
|
||||||
showToast('error', 'Enchantment Capacity Exceeded', `${itemName} can only hold ${total} enchantments (${used}/${total} used). Remove some enchantments first.`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="CraftingTab">
|
|
||||||
<div className="space-y-4 max-w-full overflow-x-hidden">
|
|
||||||
{/* Top Sub-Tabs: Fabricate / Enchant */}
|
|
||||||
<GameCard variant="default" className="p-4">
|
|
||||||
<div className="flex justify-center gap-2">
|
|
||||||
<ActionButton
|
|
||||||
variant={activeTab === 'fabricate' ? 'primary' : 'secondary'}
|
|
||||||
onClick={() => setActiveTab('fabricate')}
|
|
||||||
className={activeTab === 'fabricate' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
|
||||||
>
|
|
||||||
<Anvil size={14} className="mr-1" />
|
|
||||||
Fabricate
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
variant={activeTab === 'enchant' ? 'primary' : 'secondary'}
|
|
||||||
onClick={() => setActiveTab('enchant')}
|
|
||||||
className={activeTab === 'enchant' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
|
||||||
>
|
|
||||||
<Sparkles size={14} className="mr-1" />
|
|
||||||
Enchant
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Fabricate Content: EquipmentCrafter */}
|
|
||||||
{activeTab === 'fabricate' && (
|
|
||||||
<EquipmentCrafter />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Enchant Content: Design → Prepare → Apply workflow */}
|
|
||||||
{activeTab === 'enchant' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Enchant Sub-Navigation (no numbered stepper) */}
|
|
||||||
<GameCard variant="default" className="p-4">
|
|
||||||
<div className="flex justify-center gap-2 flex-wrap">
|
|
||||||
<ActionButton
|
|
||||||
variant={enchantStage === 'design' ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setEnchantStage('design')}
|
|
||||||
className={enchantStage === 'design' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
|
||||||
>
|
|
||||||
<Scroll size={14} className="mr-1" />
|
|
||||||
Design
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
variant={enchantStage === 'prepare' ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setEnchantStage('prepare')}
|
|
||||||
className={enchantStage === 'prepare' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
|
||||||
>
|
|
||||||
<Hammer size={14} className="mr-1" />
|
|
||||||
Prepare
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
variant={enchantStage === 'apply' ? 'primary' : 'secondary'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setEnchantStage('apply')}
|
|
||||||
className={enchantStage === 'apply' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
|
||||||
>
|
|
||||||
<Sparkles size={14} className="mr-1" />
|
|
||||||
Apply
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Enchant Stage Content */}
|
|
||||||
{enchantStage === 'design' && (
|
|
||||||
<EnchantmentDesigner
|
|
||||||
selectedEquipmentType={selectedEquipmentType}
|
|
||||||
setSelectedEquipmentType={setSelectedEquipmentType}
|
|
||||||
selectedEffects={selectedEffects}
|
|
||||||
setSelectedEffects={setSelectedEffects}
|
|
||||||
designName={designName}
|
|
||||||
setDesignName={setDesignName}
|
|
||||||
selectedDesign={selectedDesign}
|
|
||||||
setSelectedDesign={setSelectedDesign}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{enchantStage === 'prepare' && (
|
|
||||||
<EnchantmentPreparer
|
|
||||||
selectedEquipmentInstance={selectedEquipmentInstance}
|
|
||||||
setSelectedEquipmentInstance={setSelectedEquipmentInstance}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{enchantStage === 'apply' && (
|
|
||||||
<EnchantmentApplier
|
|
||||||
selectedEquipmentInstance={selectedEquipmentInstance}
|
|
||||||
setSelectedEquipmentInstance={setSelectedEquipmentInstance}
|
|
||||||
selectedDesign={selectedDesign}
|
|
||||||
setSelectedDesign={setSelectedDesign}
|
|
||||||
onEnchantmentApplied={handleEnchantmentApplied}
|
|
||||||
onCapacityExceeded={handleCapacityExceeded}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current Activity Indicator: Crafting */}
|
|
||||||
{currentAction === 'craft' && equipmentCraftingProgress && (
|
|
||||||
<GameCard variant="default" className="border-[var(--mana-water)]/60 bg-[var(--mana-water)]/10">
|
|
||||||
<SectionHeader
|
|
||||||
title="Crafting Equipment"
|
|
||||||
action={
|
|
||||||
<span className="text-sm text-[var(--text-muted)]">
|
|
||||||
{safeToFixed(calcPercent(equipmentCraftingProgress.progress, equipmentCraftingProgress.required), 0)}%
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Progress
|
|
||||||
value={calcPercent(equipmentCraftingProgress.progress, equipmentCraftingProgress.required)}
|
|
||||||
className="h-3 bg-[var(--bg-sunken)]"
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2 mt-2 text-sm text-[var(--text-secondary)]">
|
|
||||||
<Anvil size={16} className="text-[var(--mana-water)]" />
|
|
||||||
<span>Crafting equipment...</span>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current Activity Indicator: Designing */}
|
|
||||||
{currentAction === 'design' && designProgress && (
|
|
||||||
<GameCard variant="default" className="border-[var(--mana-stellar)]/60 bg-[var(--mana-stellar)]/10">
|
|
||||||
<SectionHeader
|
|
||||||
title="Designing Enchantment"
|
|
||||||
action={
|
|
||||||
<ActionButton variant="ghost" size="sm" onClick={() => useCraftingStore.getState().cancelDesign()}>
|
|
||||||
Cancel
|
|
||||||
</ActionButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Progress
|
|
||||||
value={calcPercent(designProgress.progress, designProgress.required)}
|
|
||||||
className="h-3 bg-[var(--bg-sunken)]"
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2 mt-2 text-sm text-[var(--text-secondary)]">
|
|
||||||
<Scroll size={16} className="text-[var(--mana-stellar)]" />
|
|
||||||
<span>Designing: {designProgress.name}</span>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current Activity Indicator: Preparing */}
|
|
||||||
{currentAction === 'prepare' && preparationProgress && (
|
|
||||||
<GameCard variant="default" className="border-[var(--color-warning)]/60 bg-[var(--color-warning)]/10">
|
|
||||||
<SectionHeader
|
|
||||||
title="Preparing Equipment"
|
|
||||||
action={
|
|
||||||
<ActionButton variant="ghost" size="sm" onClick={() => useCraftingStore.getState().cancelPreparation()}>
|
|
||||||
Cancel
|
|
||||||
</ActionButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Progress
|
|
||||||
value={calcPercent(preparationProgress.progress, preparationProgress.required)}
|
|
||||||
className="h-3 bg-[var(--bg-sunken)]"
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2 mt-2 text-sm text-[var(--text-secondary)]">
|
|
||||||
<Hammer size={16} className="text-[var(--color-warning)]" />
|
|
||||||
<span>Preparing equipment...</span>
|
|
||||||
<span className="text-[var(--text-muted)] ml-auto">
|
|
||||||
Mana paid: {fmt(preparationProgress.manaCostPaid)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current Activity Indicator: Enchanting */}
|
|
||||||
{currentAction === 'enchant' && applicationProgress && (
|
|
||||||
<GameCard variant="default" className="border-[var(--mana-light)]/60 bg-[var(--mana-light)]/10">
|
|
||||||
<SectionHeader
|
|
||||||
title={applicationProgress.paused ? "Enchantment Paused" : "Applying Enchantment"}
|
|
||||||
action={
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{applicationProgress.paused ? (
|
|
||||||
<ActionButton size="sm" onClick={resumeApplication}>Resume</ActionButton>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ActionButton variant="secondary" size="sm" onClick={pauseApplication}>Pause</ActionButton>
|
|
||||||
<ActionButton variant="ghost" size="sm" onClick={() => {
|
|
||||||
useCraftingStore.getState().cancelApplication();
|
|
||||||
showToast('warning', 'Enchantment Cancelled', 'The enchantment application was cancelled.');
|
|
||||||
}}>Cancel</ActionButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Progress
|
|
||||||
value={calcPercent(applicationProgress.progress, applicationProgress.required)}
|
|
||||||
className="h-3 bg-[var(--bg-sunken)]"
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2 mt-2 text-sm text-[var(--text-secondary)]">
|
|
||||||
<Sparkles size={16} className="text-[var(--mana-light)]" />
|
|
||||||
<span>{applicationProgress.paused ? 'Enchantment paused' : 'Applying enchantment...'}</span>
|
|
||||||
<span className="text-[var(--text-muted)] ml-auto">
|
|
||||||
{safeToFixed(calcPercent(applicationProgress.progress, applicationProgress.required), 0)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CraftingTab.displayName = 'CraftingTab';
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { GameStateDebug } from '@/components/game/debug/GameStateDebug';
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
import {
|
|
||||||
SkillDebug,
|
|
||||||
AttunementDebug,
|
|
||||||
ElementDebug,
|
|
||||||
GolemDebug,
|
|
||||||
PactDebug
|
|
||||||
} from '@/components/game/debug';
|
|
||||||
|
|
||||||
export function DebugTab() {
|
|
||||||
return (
|
|
||||||
<DebugName name="DebugTab">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<GameStateDebug />
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<AttunementDebug />
|
|
||||||
<ElementDebug />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SkillDebug />
|
|
||||||
<GolemDebug />
|
|
||||||
<PactDebug />
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugTab.displayName = "DebugTab";
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
|
|
||||||
interface EnchantmentsPanelProps {
|
|
||||||
enchantments: Array<{ effectId: string; stacks: number }>;
|
|
||||||
compact?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EnchantmentsPanel({
|
|
||||||
enchantments,
|
|
||||||
compact = false,
|
|
||||||
}: EnchantmentsPanelProps) {
|
|
||||||
if (enchantments.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex flex-wrap gap-1 ${compact ? 'mt-1' : ''}`}>
|
|
||||||
{enchantments.map((ench, i) => {
|
|
||||||
const effect = ENCHANTMENT_EFFECTS[ench.effectId];
|
|
||||||
return (
|
|
||||||
<TooltipProvider key={i}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs cursor-help border-[var(--border-default)] text-[var(--text-secondary)]"
|
|
||||||
>
|
|
||||||
{effect?.name || ench.effectId}
|
|
||||||
{ench.stacks > 1 && ` x${ench.stacks}`}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
|
||||||
<p>{effect?.description || 'Unknown effect'}</p>
|
|
||||||
<p className="text-[var(--text-muted)] text-xs">
|
|
||||||
Category: {effect?.category || 'unknown'}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { EquipmentSlot } from '@/lib/game/data/equipment';
|
|
||||||
// GameStore import removed - not used
|
|
||||||
import { ActionButton } from '@/components/ui/action-button';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { X } from 'lucide-react';
|
|
||||||
|
|
||||||
interface EquipmentControlsProps {
|
|
||||||
onUnequip: (slot: EquipmentSlot) => void;
|
|
||||||
onDelete: (instanceId: string, name: string) => void;
|
|
||||||
selectedSlot: EquipmentSlot | null;
|
|
||||||
isSlotBlocked: (slot: EquipmentSlot) => boolean;
|
|
||||||
isEquipped: (instanceId: string) => boolean;
|
|
||||||
getEquippableSlots: (typeId: string) => EquipmentSlot[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EquipmentControls({
|
|
||||||
onUnequip,
|
|
||||||
onDelete,
|
|
||||||
selectedSlot,
|
|
||||||
isSlotBlocked,
|
|
||||||
isEquipped,
|
|
||||||
getEquippableSlots,
|
|
||||||
}: EquipmentControlsProps) {
|
|
||||||
const SLOT_NAMES = {
|
|
||||||
mainHand: 'Main Hand',
|
|
||||||
offHand: 'Off Hand',
|
|
||||||
head: 'Head',
|
|
||||||
body: 'Body',
|
|
||||||
hands: 'Hands',
|
|
||||||
feet: 'Feet',
|
|
||||||
accessory1: 'Accessory 1',
|
|
||||||
accessory2: 'Accessory 2',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
return {
|
|
||||||
renderUnequipButton: (slot: EquipmentSlot, instanceName: string) => (
|
|
||||||
<ActionButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onUnequip(slot);
|
|
||||||
}}
|
|
||||||
aria-label={`Unequip ${instanceName}`}
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</ActionButton>
|
|
||||||
),
|
|
||||||
|
|
||||||
renderDeleteButton: (instanceId: string, name: string) => (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<ActionButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 w-8 p-0 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
|
||||||
onClick={() => onDelete(instanceId, name)}
|
|
||||||
aria-label={`Delete ${name}`}
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</ActionButton>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
|
||||||
<p>Delete this item</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { EquipmentSlot } from '@/lib/game/data/equipment';
|
|
||||||
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
|
||||||
import type { EquipmentInstance } from '@/lib/game/types';
|
|
||||||
import { GameCard } from '@/components/ui/game-card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { EnchantmentsPanel } from './EnchantmentsPanel';
|
|
||||||
|
|
||||||
interface EquipmentInventoryProps {
|
|
||||||
unequippedItems: EquipmentInstance[];
|
|
||||||
onEquip: (instanceId: string, slot: EquipmentSlot) => void;
|
|
||||||
onDelete: (instanceId: string, name: string) => void;
|
|
||||||
getEquippableSlots: (typeId: string) => EquipmentSlot[];
|
|
||||||
SLOT_NAMES: Record<EquipmentSlot, string>;
|
|
||||||
SLOT_ICONS: Record<EquipmentSlot, React.ElementType>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EquipmentInventory({
|
|
||||||
unequippedItems,
|
|
||||||
onEquip,
|
|
||||||
onDelete,
|
|
||||||
getEquippableSlots,
|
|
||||||
SLOT_NAMES,
|
|
||||||
SLOT_ICONS,
|
|
||||||
}: EquipmentInventoryProps) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 max-h-96 overflow-y-auto">
|
|
||||||
{unequippedItems.map((instance) => {
|
|
||||||
const equipmentType = EQUIPMENT_TYPES[instance.typeId];
|
|
||||||
const validSlots = getEquippableSlots(instance.typeId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GameCard
|
|
||||||
key={instance.instanceId}
|
|
||||||
variant="default"
|
|
||||||
className={`${getRarityBorderColor(instance.rarity)} ${getRarityBgColor(instance.rarity)}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between mb-2">
|
|
||||||
<div>
|
|
||||||
<div className={`font-semibold text-sm ${getRarityTextColor(instance.rarity)}`}>
|
|
||||||
{instance.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)]">
|
|
||||||
{equipmentType?.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-xs border-[var(--border-default)] text-[var(--text-secondary)]">
|
|
||||||
{equipmentType?.category || 'unknown'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-xs text-[var(--text-muted)] space-y-1 mb-2">
|
|
||||||
<div>
|
|
||||||
Capacity: {instance.usedCapacity}/{instance.totalCapacity}
|
|
||||||
{instance.quality < 100 && (
|
|
||||||
<span className="text-[var(--mana-light)] ml-1">
|
|
||||||
(Quality: {instance.quality}%)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{instance.enchantments.length > 0 && (
|
|
||||||
<EnchantmentsPanel enchantments={instance.enchantments} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{validSlots.length > 0 && (
|
|
||||||
<EquipControls
|
|
||||||
instance={instance}
|
|
||||||
validSlots={validSlots}
|
|
||||||
onEquip={onEquip}
|
|
||||||
onDelete={onDelete}
|
|
||||||
SLOT_NAMES={SLOT_NAMES}
|
|
||||||
SLOT_ICONS={SLOT_ICONS}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</GameCard>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRarityBorderColor(rarity: string) {
|
|
||||||
const colors: Record<string, string> = {
|
|
||||||
common: 'border-[var(--text-muted)]',
|
|
||||||
uncommon: 'border-[var(--color-success)]',
|
|
||||||
rare: 'border-[var(--mana-water)]',
|
|
||||||
epic: 'border-[var(--mana-stellar)]',
|
|
||||||
legendary: 'border-[var(--mana-light)]',
|
|
||||||
mythic: 'border-[var(--mana-dark)]',
|
|
||||||
};
|
|
||||||
return colors[rarity] || 'border-[var(--border-default)]';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRarityBgColor(rarity: string) {
|
|
||||||
const colors: Record<string, string> = {
|
|
||||||
common: 'bg-[var(--bg-sunken)]/30',
|
|
||||||
uncommon: 'bg-[var(--color-success)]/10',
|
|
||||||
rare: 'bg-[var(--mana-water)]/10',
|
|
||||||
epic: 'bg-[var(--mana-stellar)]/10',
|
|
||||||
legendary: 'bg-[var(--mana-light)]/10',
|
|
||||||
mythic: 'bg-[var(--mana-dark)]/10',
|
|
||||||
};
|
|
||||||
return colors[rarity] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRarityTextColor(rarity: string) {
|
|
||||||
const colors: Record<string, string> = {
|
|
||||||
common: 'text-[var(--text-secondary)]',
|
|
||||||
uncommon: 'text-[var(--color-success)]',
|
|
||||||
rare: 'text-[var(--mana-water)]',
|
|
||||||
epic: 'text-[var(--mana-stellar)]',
|
|
||||||
legendary: 'text-[var(--mana-light)]',
|
|
||||||
mythic: 'text-[var(--mana-dark)]',
|
|
||||||
};
|
|
||||||
return colors[rarity] || 'text-[var(--text-primary)]';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EquipControlsProps {
|
|
||||||
instance: EquipmentInstance;
|
|
||||||
validSlots: EquipmentSlot[];
|
|
||||||
onEquip: (instanceId: string, slot: EquipmentSlot) => void;
|
|
||||||
onDelete: (instanceId: string, name: string) => void;
|
|
||||||
SLOT_NAMES: Record<EquipmentSlot, string>;
|
|
||||||
SLOT_ICONS: Record<EquipmentSlot, React.ElementType>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EquipControls({
|
|
||||||
instance,
|
|
||||||
validSlots,
|
|
||||||
onEquip,
|
|
||||||
onDelete,
|
|
||||||
SLOT_NAMES,
|
|
||||||
SLOT_ICONS,
|
|
||||||
}: EquipControlsProps) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => onEquip(instance.instanceId, value as EquipmentSlot)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 text-xs bg-[var(--bg-sunken)] border-[var(--border-default)]">
|
|
||||||
<SelectValue placeholder="Equip to..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="bg-[var(--bg-elevated)] border-[var(--border-default)]">
|
|
||||||
{validSlots.map((slot) => (
|
|
||||||
<SelectItem
|
|
||||||
key={slot}
|
|
||||||
value={slot}
|
|
||||||
className="text-xs text-[var(--text-primary)] focus:bg-[var(--bg-sunken)]"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{(() => {
|
|
||||||
const Icon = SLOT_ICONS[slot];
|
|
||||||
return <Icon size={14} />;
|
|
||||||
})()}
|
|
||||||
{SLOT_NAMES[slot]}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="h-8 w-8 p-0 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20 rounded flex items-center justify-center"
|
|
||||||
onClick={() => onDelete(instance.instanceId, instance.name)}
|
|
||||||
aria-label={`Delete ${instance.name}`}
|
|
||||||
>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { EquipmentSlot } from '@/lib/game/data/equipment';
|
|
||||||
import { SLOT_NAMES, EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
|
||||||
import type { EquipmentInstance } from '@/lib/game/types';
|
|
||||||
import { GameCard } from '@/components/ui/game-card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { AlertCircle, Sword, Shield, HardHat, Shirt, Hand, Footprints, Gem } from 'lucide-react';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { RARITY_BORDER_COLORS, RARITY_BG_COLORS, RARITY_TEXT_COLORS } from './EquipmentTab';
|
|
||||||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
|
||||||
import type { EquipmentType } from '@/lib/game/data/equipment';
|
|
||||||
|
|
||||||
const SLOT_ICONS: Record<EquipmentSlot, React.ElementType> = {
|
|
||||||
mainHand: Sword,
|
|
||||||
offHand: Shield,
|
|
||||||
head: HardHat,
|
|
||||||
body: Shirt,
|
|
||||||
hands: Hand,
|
|
||||||
feet: Footprints,
|
|
||||||
accessory1: Gem,
|
|
||||||
accessory2: Gem,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EquipmentSlotGridProps {
|
|
||||||
equippedInstances: Record<string, string | null>;
|
|
||||||
equipmentInstances: Record<string, EquipmentInstance>;
|
|
||||||
selectedSlot: EquipmentSlot | null;
|
|
||||||
onSlotClick: (slot: EquipmentSlot) => void;
|
|
||||||
onUnequip: (slot: EquipmentSlot) => void;
|
|
||||||
isSlotBlocked: (slot: EquipmentSlot) => boolean;
|
|
||||||
SLOT_GROUPS: Array<{ label: string; slots: EquipmentSlot[] }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EquipmentSlotGrid({
|
|
||||||
equippedInstances,
|
|
||||||
equipmentInstances,
|
|
||||||
selectedSlot,
|
|
||||||
onSlotClick,
|
|
||||||
onUnequip,
|
|
||||||
isSlotBlocked,
|
|
||||||
SLOT_GROUPS,
|
|
||||||
}: EquipmentSlotGridProps) {
|
|
||||||
const renderSlot = (slot: EquipmentSlot) => {
|
|
||||||
const instanceId = equippedInstances[slot];
|
|
||||||
const instance = instanceId ? equipmentInstances[instanceId] : null;
|
|
||||||
const equipmentType = instance ? EQUIPMENT_TYPES?.[instance.typeId] : null;
|
|
||||||
const blocked = isSlotBlocked(slot);
|
|
||||||
const isEmpty = !instance;
|
|
||||||
const SlotIcon = SLOT_ICONS[slot];
|
|
||||||
|
|
||||||
const slotContent = (
|
|
||||||
<GameCard
|
|
||||||
variant={blocked ? 'danger' : instance ? 'default' : 'sunken'}
|
|
||||||
className={`relative transition-all duration-200
|
|
||||||
${isEmpty && !blocked ? 'border-dashed' : ''}
|
|
||||||
${blocked ? 'opacity-60 cursor-not-allowed' : 'hover:border-[var(--border-default)]'}
|
|
||||||
`}
|
|
||||||
role="button"
|
|
||||||
aria-label={`${SLOT_NAMES[slot]} slot${blocked ? ' (blocked by 2-handed weapon)' : ''}${instance ? `: ${instance.name}` : ' (empty)'}`}
|
|
||||||
tabIndex={blocked ? -1 : 0}
|
|
||||||
onClick={() => !blocked && onSlotClick(slot)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (!blocked && (e.key === 'Enter' || e.key === ' ')) {
|
|
||||||
onSlotClick(slot);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<SlotIcon
|
|
||||||
size={16}
|
|
||||||
className={blocked ? 'text-[var(--text-disabled)]' : 'text-[var(--text-secondary)]'}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`text-sm font-semibold
|
|
||||||
${blocked ? 'text-[var(--text-disabled)]' : 'text-[var(--text-primary)]'}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{SLOT_NAMES[slot]}
|
|
||||||
</span>
|
|
||||||
{blocked && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs border-[var(--mana-dark)] text-[var(--mana-dark)] ml-2"
|
|
||||||
>
|
|
||||||
<AlertCircle size={12} className="mr-1" />
|
|
||||||
Occupied — 2H Weapon
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{instance && !blocked && (
|
|
||||||
<button
|
|
||||||
className="h-6 w-6 p-0 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20 rounded flex items-center justify-center"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onUnequip(slot);
|
|
||||||
}}
|
|
||||||
aria-label={`Unequip ${instance.name}`}
|
|
||||||
>
|
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{instance ? (
|
|
||||||
<EquipmentItemDisplay
|
|
||||||
instance={instance}
|
|
||||||
equipmentType={equipmentType}
|
|
||||||
isTwoHanded={equipmentType?.twoHanded || false}
|
|
||||||
isCompact={true}
|
|
||||||
/>
|
|
||||||
) : blocked ? (
|
|
||||||
<div className="text-sm text-[var(--text-disabled)] italic">
|
|
||||||
<AlertCircle size={14} className="inline mr-1" />
|
|
||||||
Blocked by 2-handed weapon
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-sm text-[var(--text-muted)] italic text-center py-2">
|
|
||||||
{SLOT_NAMES[slot]}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</GameCard>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (blocked) {
|
|
||||||
return (
|
|
||||||
<TooltipProvider key={slot}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
{slotContent}
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
|
||||||
<p>The offhand slot is blocked because a 2-handed weapon is equipped in the main hand.</p>
|
|
||||||
<p className="text-[var(--text-muted)] text-xs mt-1">Unequip the 2-handed weapon to use this slot.</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div key={slot}>{slotContent}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{SLOT_GROUPS.map((group) => (
|
|
||||||
<div key={group.label}>
|
|
||||||
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-2">
|
|
||||||
{group.label}
|
|
||||||
</h4>
|
|
||||||
<div className={`grid gap-3
|
|
||||||
grid-cols-2
|
|
||||||
${group.slots.includes('mainHand' as EquipmentSlot) ? 'sm:grid-cols-2' : 'sm:grid-cols-2 lg:grid-cols-4'}
|
|
||||||
`}>
|
|
||||||
{group.slots.map((slot) => renderSlot(slot))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EquipmentItemDisplayProps {
|
|
||||||
instance: EquipmentInstance;
|
|
||||||
equipmentType: EquipmentType | undefined;
|
|
||||||
isTwoHanded: boolean;
|
|
||||||
isCompact?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EquipmentItemDisplay({
|
|
||||||
instance,
|
|
||||||
equipmentType,
|
|
||||||
isTwoHanded,
|
|
||||||
isCompact = false,
|
|
||||||
}: EquipmentItemDisplayProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className={`font-semibold text-sm ${RARITY_TEXT_COLORS[instance.rarity] || 'text-[var(--text-primary)]'}`}>
|
|
||||||
{instance.name}
|
|
||||||
{isTwoHanded && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="ml-2 text-xs border-[var(--mana-light)] text-[var(--mana-light)]"
|
|
||||||
>
|
|
||||||
2-Handed
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-secondary)]">
|
|
||||||
Enchantments: {instance.enchantments.length}/{instance.totalCapacity}
|
|
||||||
</div>
|
|
||||||
{instance.enchantments.length > 0 && (
|
|
||||||
<EnchantmentsDisplay enchantments={instance.enchantments} compact={isCompact} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EnchantmentsDisplayProps {
|
|
||||||
enchantments: Array<{ effectId: string; stacks: number }>;
|
|
||||||
compact?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EnchantmentsDisplay({ enchantments, compact = false }: EnchantmentsDisplayProps) {
|
|
||||||
return (
|
|
||||||
<div className={`flex flex-wrap gap-1 ${compact ? 'mt-1' : ''}`}>
|
|
||||||
{enchantments.map((ench, i) => {
|
|
||||||
const effect = ENCHANTMENT_EFFECTS[ench.effectId];
|
|
||||||
return (
|
|
||||||
<TooltipProvider key={i}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs cursor-help border-[var(--border-default)] text-[var(--text-secondary)]"
|
|
||||||
>
|
|
||||||
{effect?.name || ench.effectId}
|
|
||||||
{ench.stacks > 1 && ` x${ench.stacks}`}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
|
||||||
<p>{effect?.description || 'Unknown effect'}</p>
|
|
||||||
<p className="text-[var(--text-muted)] text-xs">
|
|
||||||
Category: {effect?.category || 'unknown'}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
|
||||||
import {
|
|
||||||
EQUIPMENT_TYPES,
|
|
||||||
EQUIPMENT_SLOTS,
|
|
||||||
SLOT_NAMES,
|
|
||||||
getEquipmentBySlot,
|
|
||||||
type EquipmentSlot,
|
|
||||||
type EquipmentType,
|
|
||||||
} from '@/lib/game/data/equipment';
|
|
||||||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
|
||||||
import { fmt } from '@/lib/game/stores';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { GameCard } from '@/components/ui/game-card';
|
|
||||||
import { SectionHeader } from '@/components/ui/section-header';
|
|
||||||
import { StatRow } from '@/components/ui/stat-row';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import type { EquipmentInstance } from '@/lib/game/types';
|
|
||||||
import { EquipmentSlotGrid } from './EquipmentSlotGrid';
|
|
||||||
import { EquipmentInventory } from './EquipmentInventory';
|
|
||||||
import { EnchantmentsPanel } from './EnchantmentsPanel';
|
|
||||||
import { useGameToast } from '@/components/game/GameToast';
|
|
||||||
import { ConfirmDialog } from '@/components/game/ConfirmDialog';
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
import { equipItem, unequipItem, deleteEquipmentInstance } from '@/lib/game/crafting-actions';
|
|
||||||
import { useCombatStore, useCraftingStore } from '@/lib/game/stores';
|
|
||||||
import type { GameState } from '@/lib/game/types';
|
|
||||||
|
|
||||||
// Rarity color mappings using design system tokens
|
|
||||||
export const RARITY_BORDER_COLORS: Record<string, string> = {
|
|
||||||
common: 'border-[var(--text-muted)]',
|
|
||||||
uncommon: 'border-[var(--color-success)]',
|
|
||||||
rare: 'border-[var(--mana-water)]',
|
|
||||||
epic: 'border-[var(--mana-stellar)]',
|
|
||||||
legendary: 'border-[var(--mana-light)]',
|
|
||||||
mythic: 'border-[var(--mana-dark)]',
|
|
||||||
};
|
|
||||||
export const RARITY_BG_COLORS: Record<string, string> = {
|
|
||||||
common: 'bg-[var(--bg-sunken)]/30',
|
|
||||||
uncommon: 'bg-[var(--color-success)]/10',
|
|
||||||
rare: 'bg-[var(--mana-water)]/10',
|
|
||||||
epic: 'bg-[var(--mana-stellar)]/10',
|
|
||||||
legendary: 'bg-[var(--mana-light)]/10',
|
|
||||||
mythic: 'bg-[var(--mana-dark)]/10',
|
|
||||||
};
|
|
||||||
export const RARITY_TEXT_COLORS: Record<string, string> = {
|
|
||||||
common: 'text-[var(--text-secondary)]',
|
|
||||||
uncommon: 'text-[var(--color-success)]',
|
|
||||||
rare: 'text-[var(--mana-water)]',
|
|
||||||
epic: 'text-[var(--mana-stellar)]',
|
|
||||||
legendary: 'text-[var(--mana-light)]',
|
|
||||||
mythic: 'text-[var(--mana-dark)]',
|
|
||||||
};
|
|
||||||
|
|
||||||
const SLOT_ICONS: Record<EquipmentSlot, React.ElementType> = {
|
|
||||||
mainHand: () => (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M14.5 4H5.5L2 10v10a2 2 0 002 2h16a2 2 0 002-2V10l-3.5-6z" />
|
|
||||||
<line x1="6" y1="10" x2="18" y2="10" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
offHand: () => (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M20 7H9a2 2 0 01-2-2V4a2 2 0 012-2h5l3 6-3 6h-5a2 2 0 01-2 2v4a2 2 0 002 2h7l3 6-3 6H4a2 2 0 01-2-2v-2" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
head: () => (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
|
||||||
<path d="M14 2v6h6M10 14l2 2 4-4" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
body: () => (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M20.38 8.5c.6-1.4 1.3-4.9-1.2-8.5-1.1-1.8-3.6-1.8-4.7 0-2.5 3.6-1.9 7.1-1.2 8.5.7 1.4 2.5 1.9 4 1.9 1.5 0 3.3-.5 4-1.9z" />
|
|
||||||
<path d="M12 8v7M8 18h8" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
hands: () => (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M18 11V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v5M6 11v6a2 2 0 002 2h5a2 2 0 002-2v-6" />
|
|
||||||
<path d="M10 15V7a2 2 0 012-2h2a2 2 0 012 2v8" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
feet: () => (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M12 2v20M5 13a4 4 0 000 8h14a4 4 0 000-8" />
|
|
||||||
<path d="M9 13V5a2 2 0 012-2h2a2 2 0 012 2v8" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
accessory1: () => (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<circle cx="12" cy="12" r="8" />
|
|
||||||
<path d="M12 4v8l2 4" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
accessory2: () => (
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<circle cx="12" cy="12" r="8" />
|
|
||||||
<path d="M12 4v8l2 4" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Slot grouping for visual layout
|
|
||||||
type SlotGroup = {
|
|
||||||
label: string;
|
|
||||||
slots: EquipmentSlot[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const SLOT_GROUPS: SlotGroup[] = [
|
|
||||||
{ label: 'Weapon & Shield', slots: ['mainHand', 'offHand'] },
|
|
||||||
{ label: 'Armor', slots: ['head', 'body', 'hands', 'feet'] },
|
|
||||||
{ label: 'Accessories', slots: ['accessory1', 'accessory2'] },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function EquipmentTab() {
|
|
||||||
const showToast = useGameToast();
|
|
||||||
const [selectedSlot, setSelectedSlot] = useState<EquipmentSlot | null>(null);
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<{ instanceId: string; name: string } | null>(null);
|
|
||||||
|
|
||||||
// Use modular store directly - MUST be called before any conditional returns
|
|
||||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
|
||||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
|
||||||
|
|
||||||
// Get unequipped items - hooks must be called before conditional returns
|
|
||||||
const equippedIds = useMemo(() =>
|
|
||||||
new Set(Object.values(equippedInstances || {}).filter(Boolean)),
|
|
||||||
[equippedInstances]
|
|
||||||
);
|
|
||||||
|
|
||||||
const unequippedItems = useMemo(() =>
|
|
||||||
Object.values(equipmentInstances || {}).filter(
|
|
||||||
(inst) => !equippedIds.has(inst.instanceId)
|
|
||||||
),
|
|
||||||
[equipmentInstances, equippedIds]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Guard against undefined during initialization - AFTER all hooks
|
|
||||||
if (!equippedInstances || !equipmentInstances) {
|
|
||||||
return (
|
|
||||||
<DebugName name="EquipmentTab">
|
|
||||||
<div className="p-4 text-center text-[var(--text-muted)]">
|
|
||||||
Loading equipment data...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</DebugName>);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equip an item to a slot
|
|
||||||
const handleEquip = (instanceId: string, slot: EquipmentSlot) => {
|
|
||||||
const instance = equipmentInstances[instanceId];
|
|
||||||
equipItem(instanceId, slot, useCraftingStore.getState as () => GameState, (fn) => useCraftingStore.setState(fn as any));
|
|
||||||
setSelectedSlot(null);
|
|
||||||
showToast('success', 'Item Equipped', `${instance?.name || 'Item'} equipped to ${SLOT_NAMES[slot]}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Unequip from a slot
|
|
||||||
const handleUnequip = (slot: EquipmentSlot) => {
|
|
||||||
const instanceId = equippedInstances[slot];
|
|
||||||
const instance = instanceId ? equipmentInstances[instanceId] : null;
|
|
||||||
unequipItem(slot, (fn) => useCraftingStore.setState(fn as any));
|
|
||||||
showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed from ${SLOT_NAMES[slot]}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if a slot is blocked by a 2-handed weapon
|
|
||||||
const isSlotBlocked = (slot: EquipmentSlot): boolean => {
|
|
||||||
if (slot === 'offHand' && equippedInstances.mainHand) {
|
|
||||||
const mainHandInstance = equipmentInstances[equippedInstances.mainHand];
|
|
||||||
if (!mainHandInstance) return false;
|
|
||||||
const mainHandType = EQUIPMENT_TYPES[mainHandInstance.typeId];
|
|
||||||
return mainHandType?.twoHanded === true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get items that can be equipped in a slot
|
|
||||||
const getEquippableItems = (slot: EquipmentSlot): EquipmentInstance[] => {
|
|
||||||
const equipmentTypes = getEquipmentBySlot(slot);
|
|
||||||
const typeIds = new Set(equipmentTypes.map((t) => t.id));
|
|
||||||
return unequippedItems.filter((inst) => typeIds.has(inst.typeId));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get all items that can go in a slot
|
|
||||||
const getItemsForSlot = (slot: EquipmentSlot): EquipmentInstance[] => {
|
|
||||||
if (isSlotBlocked(slot)) return [];
|
|
||||||
|
|
||||||
if (slot === 'accessory1' || slot === 'accessory2') {
|
|
||||||
const accessoryTypeIds = Object.values(EQUIPMENT_TYPES)
|
|
||||||
.filter((t) => t.category === 'accessory')
|
|
||||||
.map((t) => t.id);
|
|
||||||
return unequippedItems.filter((inst) => accessoryTypeIds.includes(inst.typeId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slot === 'offHand') {
|
|
||||||
return getEquippableItems(slot).filter((inst) => {
|
|
||||||
const type = EQUIPMENT_TYPES[inst.typeId];
|
|
||||||
return !type?.twoHanded;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return getEquippableItems(slot);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if an instance is currently equipped
|
|
||||||
const isEquipped = (instanceId: string): boolean =>
|
|
||||||
Object.values(equippedInstances || {}).includes(instanceId);
|
|
||||||
|
|
||||||
// Get all slots an item type can be equipped to
|
|
||||||
const getEquippableSlots = (typeId: string): EquipmentSlot[] => {
|
|
||||||
const equipmentType = EQUIPMENT_TYPES[typeId];
|
|
||||||
if (!equipmentType) return [];
|
|
||||||
if (equipmentType.category === 'accessory') {
|
|
||||||
return ['accessory1', 'accessory2'];
|
|
||||||
}
|
|
||||||
return [equipmentType.slot];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle item deletion
|
|
||||||
const handleDelete = (instanceId: string, name: string) => {
|
|
||||||
setDeleteConfirm({ instanceId, name });
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
if (deleteConfirm) {
|
|
||||||
deleteEquipmentInstance(deleteConfirm.instanceId, useCraftingStore.getState, (fn) => useCraftingStore.setState(fn));
|
|
||||||
showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`);
|
|
||||||
setDeleteConfirm(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use already-fetched values for unified effects
|
|
||||||
const unifiedEffects = getUnifiedEffects({ equipmentInstances, equippedInstances });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="EquipmentTab">
|
|
||||||
<div className="space-y-4 max-w-full overflow-x-hidden">
|
|
||||||
{/* Equipment Slots */}
|
|
||||||
<GameCard variant="default">
|
|
||||||
<SectionHeader
|
|
||||||
title="Equipped Gear"
|
|
||||||
action={
|
|
||||||
<span className="text-xs text-[var(--text-muted)]">
|
|
||||||
{Object.values(equippedInstances || {}).filter(Boolean).length} / {EQUIPMENT_SLOTS.length} slots filled
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<EquipmentSlotGrid
|
|
||||||
equippedInstances={equippedInstances}
|
|
||||||
equipmentInstances={equipmentInstances}
|
|
||||||
selectedSlot={selectedSlot}
|
|
||||||
onSlotClick={setSelectedSlot}
|
|
||||||
onUnequip={handleUnequip}
|
|
||||||
isSlotBlocked={isSlotBlocked}
|
|
||||||
SLOT_GROUPS={SLOT_GROUPS}
|
|
||||||
SLOT_NAMES={SLOT_NAMES}
|
|
||||||
SLOT_ICONS={SLOT_ICONS}
|
|
||||||
/>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Equipment Inventory */}
|
|
||||||
<GameCard variant="default">
|
|
||||||
<SectionHeader
|
|
||||||
title={`Equipment Inventory (${unequippedItems.length} items)`}
|
|
||||||
/>
|
|
||||||
{unequippedItems.length === 0 ? (
|
|
||||||
<div className="text-[var(--text-muted)] text-sm text-center py-4" role="status">
|
|
||||||
No unequipped items. Craft new gear in the Crafting tab.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<EquipmentInventory
|
|
||||||
unequippedItems={unequippedItems}
|
|
||||||
equipmentInstances={equipmentInstances}
|
|
||||||
onEquip={handleEquip}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
getEquippableSlots={getEquippableSlots}
|
|
||||||
SLOT_NAMES={SLOT_NAMES}
|
|
||||||
SLOT_ICONS={SLOT_ICONS}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Equipment Stats Summary */}
|
|
||||||
<GameCard variant="default">
|
|
||||||
<SectionHeader title="Equipment Stats Summary" />
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
||||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
|
||||||
<div className="text-2xl font-bold text-[var(--mana-light)] font-[var(--font-mono)]">
|
|
||||||
{Object.values(equipmentInstances || {}).length}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)]">Total Items</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
|
||||||
<div className="text-2xl font-bold text-[var(--color-success)] font-[var(--font-mono)]">
|
|
||||||
{equippedIds.size}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)]">Equipped</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
|
||||||
<div className="text-2xl font-bold text-[var(--mana-water)] font-[var(--font-mono)]">
|
|
||||||
{unequippedItems.length}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)]">In Inventory</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
|
||||||
<div className="text-2xl font-bold text-[var(--mana-stellar)] font-[var(--font-mono)]">
|
|
||||||
{Object.values(equipmentInstances || {}).reduce(
|
|
||||||
(sum, inst) => sum + inst.enchantments.length,
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)]">Total Enchantments</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enchantment Power */}
|
|
||||||
<GameCard className="mt-4">
|
|
||||||
<div className="pb-2">
|
|
||||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] flex items-center gap-2">
|
|
||||||
✨ Enchantment Power
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{(() => {
|
|
||||||
const effects = unifiedEffects;
|
|
||||||
if (!effects) return null;
|
|
||||||
const enchantPower = effects.enchantmentPowerMultiplier || 1;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StatRow
|
|
||||||
label="Enchantment Power:"
|
|
||||||
value={`${enchantPower.toFixed(2)}×`}
|
|
||||||
highlight={enchantPower > 1 ? 'success' : 'default'}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-[var(--text-muted)] mt-2">
|
|
||||||
Increases the power of all enchantments by {(enchantPower - 1) * 100}%. Multiplier applied to all enchantment effects.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Active Effects from Equipment */}
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="text-sm text-[var(--text-muted)] mb-2">Active Effects from Equipment:</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{(() => {
|
|
||||||
const effects = unifiedEffects;
|
|
||||||
if (!effects?.equipmentEffects) {
|
|
||||||
return <span className="text-[var(--text-muted)] text-sm">No active effects</span>;
|
|
||||||
}
|
|
||||||
const effectEntries = Object.entries(effects.equipmentEffects).filter(([, v]) => v > 0);
|
|
||||||
|
|
||||||
if (effectEntries.length === 0) {
|
|
||||||
return <span className="text-[var(--text-muted)] text-sm">No active effects</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return effectEntries.map(([stat, value]) => (
|
|
||||||
<Badge key={stat} variant="outline" className="text-xs border-[var(--border-default)] text-[var(--text-secondary)]">
|
|
||||||
{stat}: +{fmt(value)}
|
|
||||||
</Badge>
|
|
||||||
));
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
|
||||||
<ConfirmDialog
|
|
||||||
open={!!deleteConfirm}
|
|
||||||
onOpenChange={() => setDeleteConfirm(null)}
|
|
||||||
title="Discard Item?"
|
|
||||||
description={`Discard ${deleteConfirm?.name}? This cannot be undone.`}
|
|
||||||
variant="danger"
|
|
||||||
confirmText="Discard"
|
|
||||||
onConfirm={confirmDelete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</DebugName>);
|
|
||||||
}
|
|
||||||
|
|
||||||
EquipmentTab.displayName = 'EquipmentTab';
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { ChevronUp, ChevronDown, Mountain, Skull } from 'lucide-react';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
interface FloorControlsProps {
|
|
||||||
// Store values passed as individual props
|
|
||||||
currentFloor: number;
|
|
||||||
floorHP: number;
|
|
||||||
floorMaxHP: number;
|
|
||||||
maxFloorReached: number;
|
|
||||||
equipmentSpellStates: any[];
|
|
||||||
|
|
||||||
// Other props
|
|
||||||
climbDirection: 'up' | 'down' | null;
|
|
||||||
isGuardianFloor: boolean;
|
|
||||||
currentRoom: any;
|
|
||||||
currentGuardian: any;
|
|
||||||
isFloorCleared: boolean;
|
|
||||||
floorElemDef: any;
|
|
||||||
roomType: string;
|
|
||||||
roomConfig: { label: string; icon: string; color: string };
|
|
||||||
activeEquipmentSpells: any[];
|
|
||||||
floorElem: string;
|
|
||||||
totalDPS: number;
|
|
||||||
calcDamage: (state: { skills: Record<string, number>; signedPacts: number[] }, spellId: string, floorElem?: string) => number;
|
|
||||||
SPELLS_DEF: Record<string, any>;
|
|
||||||
canCastSpell: (spellId: string) => boolean;
|
|
||||||
storeCurrentAction: string;
|
|
||||||
handleClimb: (direction: 'up' | 'down') => void;
|
|
||||||
formatSpellCost: (cost: any) => string;
|
|
||||||
getSpellCostColor: (cost: any) => string;
|
|
||||||
// Skills and pacts needed for calcDamage
|
|
||||||
skills: Record<string, number>;
|
|
||||||
signedPacts: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FloorControls({
|
|
||||||
currentFloor,
|
|
||||||
floorHP,
|
|
||||||
floorMaxHP,
|
|
||||||
maxFloorReached,
|
|
||||||
equipmentSpellStates,
|
|
||||||
climbDirection,
|
|
||||||
isGuardianFloor,
|
|
||||||
currentRoom,
|
|
||||||
currentGuardian,
|
|
||||||
isFloorCleared,
|
|
||||||
floorElemDef,
|
|
||||||
roomType,
|
|
||||||
roomConfig,
|
|
||||||
activeEquipmentSpells,
|
|
||||||
floorElem,
|
|
||||||
totalDPS,
|
|
||||||
calcDamage,
|
|
||||||
SPELLS_DEF,
|
|
||||||
canCastSpell,
|
|
||||||
storeCurrentAction,
|
|
||||||
handleClimb,
|
|
||||||
formatSpellCost,
|
|
||||||
getSpellCostColor,
|
|
||||||
skills,
|
|
||||||
signedPacts,
|
|
||||||
}: FloorControlsProps) {
|
|
||||||
return (
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Floor Navigation</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleClimb('up')}
|
|
||||||
disabled={storeCurrentAction === 'climb' || isFloorCleared || maxFloorReached >= 100}
|
|
||||||
className="border-gray-600 hover:bg-gray-800"
|
|
||||||
>
|
|
||||||
<ChevronUp className="w-4 h-4 mr-1" />
|
|
||||||
Climb Up
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleClimb('down')}
|
|
||||||
disabled={storeCurrentAction === 'climb' || currentFloor <= 1}
|
|
||||||
className="border-gray-600 hover:bg-gray-800"
|
|
||||||
>
|
|
||||||
<ChevronDown className="w-4 h-4 mr-1" />
|
|
||||||
Climb Down
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{storeCurrentAction === 'climb' && (
|
|
||||||
<div className="p-3 bg-amber-900/30 rounded border border-amber-700/50">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Mountain className="w-4 h-4 text-amber-400" />
|
|
||||||
<span className="text-sm font-semibold text-amber-400">
|
|
||||||
Climbing {climbDirection === 'up' ? 'Up' : 'Down'}
|
|
||||||
</span>
|
|
||||||
{isGuardianFloor && (
|
|
||||||
<Badge className="bg-red-900/50 text-red-300 border-red-600">GUARDIAN</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentGuardian && (
|
|
||||||
<div className="text-xs mb-2 p-2 bg-gray-800/50 rounded">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Skull className="w-3 h-3 text-red-400" />
|
|
||||||
<span className="font-semibold" style={{ color: floorElemDef?.color }}>
|
|
||||||
{currentGuardian.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!(roomType === 'swarm' || roomType === 'puzzle') && (
|
|
||||||
<div className="space-y-1 mb-2">
|
|
||||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.max(0, (floorHP / floorMaxHP) * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
|
||||||
boxShadow: `0 0 8px ${floorElemDef?.glow}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-xs text-gray-400">
|
|
||||||
<span>{fmt(floorHP)} / {fmt(floorMaxHP)} HP</span>
|
|
||||||
<span className="text-amber-400">
|
|
||||||
DPS: {activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeEquipmentSpells.length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
|
||||||
const spellDef = SPELLS_DEF[spellId];
|
|
||||||
if (!spellDef) return null;
|
|
||||||
const spellState = equipmentSpellStates?.find(
|
|
||||||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
|
||||||
);
|
|
||||||
const progress = spellState?.castProgress || 0;
|
|
||||||
const canCast = canCastSpell(spellId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={`${spellId}-${equipmentId}`} className="p-2 bg-gray-800/50 rounded">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="text-xs font-semibold" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
|
||||||
{spellDef.name}
|
|
||||||
</span>
|
|
||||||
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>
|
|
||||||
{canCast ? '✓' : '✗'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 mb-1">
|
|
||||||
⚔️ {fmt(calcDamage({ skills, signedPacts }, spellId, floorElem))} dmg • {' '}
|
|
||||||
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
|
||||||
{formatSpellCost(spellDef.cost)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<div className="flex justify-between text-xs text-gray-500">
|
|
||||||
<span>Cast</span>
|
|
||||||
<span>{(progress * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min(100, progress * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color}99, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-xs text-gray-500 italic">No active spells. Equip staves with spell effects.</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{storeCurrentAction !== 'climb' && (
|
|
||||||
<div className="text-xs text-gray-500 text-center">
|
|
||||||
Click Climb Up/Down to begin climbing
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmt(value: number): string {
|
|
||||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
|
||||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
|
||||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
|
||||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
|
||||||
return value.toFixed(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtDec(value: number): string {
|
|
||||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
|
||||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
|
||||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
|
||||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
|
||||||
return value.toFixed(0);
|
|
||||||
}
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { GameCard, StatRow, ElementBadge, ActionButton } from '@/components/ui';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import {
|
|
||||||
Mountain, Zap, Clock, Swords, Sparkles, Lock, Check, X,
|
|
||||||
Info, HelpCircle
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration } from '@/lib/game/data/golems';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
import { useManaStore, useSkillStore, useCombatStore, useAttunementStore } from '@/lib/game/stores';
|
|
||||||
|
|
||||||
export function GolemancyTab() {
|
|
||||||
const attunements = useAttunementStore((s) => s.attunements);
|
|
||||||
const elements = useManaStore((s) => s.elements);
|
|
||||||
const skills = useSkillStore((s) => s.skills);
|
|
||||||
const golemancy = useCombatStore((s) => s.golemancy);
|
|
||||||
const currentFloor = useCombatStore((s) => s.currentFloor);
|
|
||||||
const currentRoom = useCombatStore((s) => s.currentRoom);
|
|
||||||
const toggleGolem = useCombatStore((s) => s.toggleGolem);
|
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
|
||||||
|
|
||||||
// Get Fabricator level and golem slots
|
|
||||||
const fabricatorLevel = attunements.fabricator?.level || 0;
|
|
||||||
const fabricatorActive = attunements.fabricator?.active || false;
|
|
||||||
const maxSlots = getGolemSlots(fabricatorLevel);
|
|
||||||
|
|
||||||
// Get unlocked elements
|
|
||||||
const unlockedElements = Object.entries(elements)
|
|
||||||
.filter(([, e]) => e.unlocked)
|
|
||||||
.map(([id]) => id);
|
|
||||||
|
|
||||||
// Get all unlocked golems
|
|
||||||
const unlockedGolems = Object.values(GOLEMS_DEF || {}).filter(golem =>
|
|
||||||
isGolemUnlocked(golem.id, attunements, unlockedElements)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if golemancy is available
|
|
||||||
const hasGolemancy = fabricatorActive && fabricatorLevel >= 2;
|
|
||||||
|
|
||||||
// Check if currently in combat (not puzzle)
|
|
||||||
const inCombat = currentRoom?.roomType !== 'puzzle';
|
|
||||||
|
|
||||||
// Get element info helper
|
|
||||||
const getElementInfo = (elementId: string) => {
|
|
||||||
return ELEMENTS[elementId];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render a golem card
|
|
||||||
const renderGolemCard = (golemId: string, isUnlocked: boolean) => {
|
|
||||||
const golem = GOLEMS_DEF[golemId];
|
|
||||||
if (!golem) return null;
|
|
||||||
|
|
||||||
const isEnabled = golemancy.enabledGolems.includes(golemId);
|
|
||||||
const isSelected = golemancy.summonedGolems.some(g => g.golemId === golemId);
|
|
||||||
|
|
||||||
// Calculate effective stats
|
|
||||||
const damage = getGolemDamage(golemId, skills);
|
|
||||||
const attackSpeed = getGolemAttackSpeed(golemId, skills);
|
|
||||||
const floorDuration = getGolemFloorDuration(skills);
|
|
||||||
|
|
||||||
// Get element color
|
|
||||||
const primaryElement = getElementInfo(golem.baseManaType);
|
|
||||||
const elementId = golem.baseManaType;
|
|
||||||
|
|
||||||
if (!isUnlocked) {
|
|
||||||
// Locked golem card
|
|
||||||
return (
|
|
||||||
|
|
||||||
<DebugName name="GolemancyTab">
|
|
||||||
<GameCard key={golemId} variant="sunken" className="opacity-60">
|
|
||||||
<div className="pb-2">
|
|
||||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
|
||||||
<Lock className="w-4 h-4" />
|
|
||||||
<span className="text-[var(--text-muted)]">???</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--text-muted)]">
|
|
||||||
{golem.unlockCondition.type === 'attunement_level' && (
|
|
||||||
<div>Requires Fabricator Level {golem.unlockCondition.level}</div>
|
|
||||||
)}
|
|
||||||
{golem.unlockCondition.type === 'mana_unlocked' && (
|
|
||||||
<div>Requires {ELEMENTS[golem.unlockCondition.manaType || '']?.name || golem.unlockCondition.manaType} Mana</div>
|
|
||||||
)}
|
|
||||||
{golem.unlockCondition.type === 'dual_attunement' && (
|
|
||||||
<div>Requires Enchanter & Fabricator Level 5</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
</DebugName>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GameCard
|
|
||||||
key={golemId}
|
|
||||||
variant={isEnabled ? "default" : "sunken"}
|
|
||||||
className={`transition-all cursor-pointer border-2 ${
|
|
||||||
isEnabled
|
|
||||||
? 'border-[var(--color-success)] bg-[var(--bg-surface)]'
|
|
||||||
: 'border-[var(--border-subtle)] hover:border-[var(--border-default)]'
|
|
||||||
}`}
|
|
||||||
onClick={() => toggleGolem(golemId)}
|
|
||||||
aria-label={`${isEnabled ? 'Disable' : 'Enable'} ${golem.name}`}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<div className="pb-2">
|
|
||||||
<h3 className="text-sm font-semibold flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Mountain className="w-4 h-4" style={{ color: `var(--mana-${elementId})` }} />
|
|
||||||
<span style={{ color: `var(--mana-${elementId})` }}>{golem.name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{golem.isAoe && (
|
|
||||||
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
|
||||||
AOE {golem.aoeTargets}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
|
||||||
{golem.tier}
|
|
||||||
</span>
|
|
||||||
{isEnabled ? (
|
|
||||||
<Check className="w-4 h-4 text-[var(--color-success)]" />
|
|
||||||
) : (
|
|
||||||
<X className="w-4 h-4 text-[var(--text-muted)]" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs text-[var(--text-secondary)]">{golem.description}</p>
|
|
||||||
|
|
||||||
<Separator className="bg-[var(--border-subtle)]" />
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
||||||
<StatRow label="DMG:" value={damage.toString()} />
|
|
||||||
<StatRow label="Speed:" value={`${attackSpeed.toFixed(1)}/hr`} />
|
|
||||||
<StatRow label="Pierce:" value={`${Math.floor(golem.armorPierce * 100)}%`} />
|
|
||||||
<StatRow label="Duration:" value={`${floorDuration} floor(s)`} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator className="bg-[var(--border-subtle)]" />
|
|
||||||
|
|
||||||
{/* Summon Cost */}
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-[var(--text-secondary)] mb-1">Summon Cost:</div>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{golem.summonCost.map((cost, idx) => {
|
|
||||||
const elem = getElementInfo(cost.element || '');
|
|
||||||
const available = cost.type === 'raw'
|
|
||||||
? rawMana
|
|
||||||
: elements[cost.element || '']?.current || 0;
|
|
||||||
const canAfford = available >= cost.amount;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className={`text-xs px-1.5 py-0.5 border rounded ${
|
|
||||||
canAfford
|
|
||||||
? 'border-[var(--color-success)] text-[var(--color-success)]'
|
|
||||||
: 'border-[var(--color-danger)] text-[var(--color-danger)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{cost.element && <ElementBadge elementId={cost.element} size="sm" />}
|
|
||||||
{' '}{cost.amount}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Maintenance Cost */}
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-[var(--text-secondary)] mb-1">Maintenance/hr:</div>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{golem.maintenanceCost.map((cost, idx) => {
|
|
||||||
return (
|
|
||||||
<span key={idx} className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
|
||||||
{cost.element && <ElementBadge elementId={cost.element} size="sm" />}
|
|
||||||
{' '}{cost.amount}/hr
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status */}
|
|
||||||
{isSelected && (
|
|
||||||
<div className="mt-2 text-xs text-[var(--color-success)] flex items-center gap-1">
|
|
||||||
<Sparkles className="w-3 h-3" />
|
|
||||||
Active on Floor {currentFloor}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Header */}
|
|
||||||
<GameCard>
|
|
||||||
<div className="pb-2">
|
|
||||||
<h2 className="text-lg font-[var(--font-heading)] font-semibold flex items-center gap-2 text-[var(--text-primary)]">
|
|
||||||
<Mountain className="w-5 h-5 text-[var(--mana-earth)]" />
|
|
||||||
Golemancy
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{!hasGolemancy ? (
|
|
||||||
<div className="text-center text-[var(--text-secondary)] py-4">
|
|
||||||
<Lock className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
|
||||||
<p>Unlock the Fabricator attunement and reach Level 2 to summon golems.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<StatRow
|
|
||||||
label="Golem Slots:"
|
|
||||||
value={`${golemancy.enabledGolems.length} / ${maxSlots}`}
|
|
||||||
highlight={golemancy.enabledGolems.length > 0 ? 'success' : undefined}
|
|
||||||
/>
|
|
||||||
<StatRow
|
|
||||||
label="Fabricator Level:"
|
|
||||||
value={fabricatorLevel.toString()}
|
|
||||||
highlight="warning"
|
|
||||||
/>
|
|
||||||
<StatRow
|
|
||||||
label="Floor Duration:"
|
|
||||||
value={`${getGolemFloorDuration(skills)} floor(s)`}
|
|
||||||
/>
|
|
||||||
<StatRow
|
|
||||||
label="Status:"
|
|
||||||
value={inCombat ? 'Combat Active' : 'Puzzle Room (No Golems)'}
|
|
||||||
highlight={inCombat ? 'success' : 'warning'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-xs text-[var(--text-muted)] mt-2">
|
|
||||||
Golems are automatically summoned at the start of each combat floor.
|
|
||||||
They cost mana to maintain and will be dismissed if you run out.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
|
|
||||||
{/* Active Golems - Empty State */}
|
|
||||||
{hasGolemancy && golemancy.summonedGolems.length === 0 && (
|
|
||||||
<GameCard variant="sunken">
|
|
||||||
<div className="text-center py-4 text-[var(--text-muted)]">
|
|
||||||
<Info className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
|
||||||
<p className="text-sm">No golems summoned</p>
|
|
||||||
<p className="text-xs mt-1">Enable golems below to summon them at the start of combat</p>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Active Golems */}
|
|
||||||
{hasGolemancy && golemancy.summonedGolems.length > 0 && (
|
|
||||||
<GameCard variant="default" className="border-[var(--color-success)]">
|
|
||||||
<div className="pb-2">
|
|
||||||
<h3 className="text-sm font-semibold flex items-center gap-2 text-[var(--color-success)]">
|
|
||||||
<Sparkles className="w-4 h-4" />
|
|
||||||
Active Golems ({golemancy.summonedGolems.length})
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{golemancy.summonedGolems.map(sg => {
|
|
||||||
const golem = GOLEMS_DEF[sg.golemId];
|
|
||||||
if (!golem) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span key={sg.golemId} className="text-xs px-2 py-1 border border-[var(--border-default)] rounded">
|
|
||||||
<Mountain className="w-3 h-3 inline mr-1" style={{ color: `var(--mana-${golem.baseManaType})` }} />
|
|
||||||
{golem.name}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Golem Selection */}
|
|
||||||
{hasGolemancy && (
|
|
||||||
<GameCard>
|
|
||||||
<div className="pb-2">
|
|
||||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Select Golems to Summon</h3>
|
|
||||||
</div>
|
|
||||||
<ScrollArea className="h-96">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pr-4">
|
|
||||||
{/* Unlocked Golems */}
|
|
||||||
{unlockedGolems.map(golem => renderGolemCard(golem.id, true))}
|
|
||||||
|
|
||||||
{/* Locked Golems */}
|
|
||||||
{Object.values(GOLEMS_DEF || {})
|
|
||||||
.filter(g => !isGolemUnlocked(g.id, attunements, unlockedElements))
|
|
||||||
.map(golem => renderGolemCard(golem.id, false))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</GameCard>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Golemancy Skills Info */}
|
|
||||||
<GameCard>
|
|
||||||
<div className="pb-2">
|
|
||||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Golemancy Skills</h3>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 text-xs">
|
|
||||||
<StatRow label="Golem Mastery:" value={`+${(skills.golemMastery || 0) * 10}% damage`} />
|
|
||||||
<StatRow label="Golem Efficiency:" value={`+${(skills.golemEfficiency || 0) * 5}% attack speed`} />
|
|
||||||
<StatRow label="Golem Longevity:" value={`+${skills.golemLongevity || 0} floor duration`} />
|
|
||||||
<StatRow label="Golem Siphon:" value={`-${(skills.golemSiphon || 0) * 10}% maintenance`} />
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
GolemancyTab.displayName = "GolemancyTab";
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
import { GUARDIANS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
interface GuardianPanelProps {
|
|
||||||
currentFloor: number;
|
|
||||||
floorElemDef: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GuardianPanel({ currentFloor, floorElemDef }: GuardianPanelProps) {
|
|
||||||
const guardian = GUARDIANS[currentFloor];
|
|
||||||
if (!guardian) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-3 bg-red-900/20 rounded border border-red-700/50 mb-4">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<span className="text-lg">⚔️</span>
|
|
||||||
<span className="text-sm font-semibold" style={{ color: floorElemDef?.color }}>
|
|
||||||
{guardian.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1 text-xs text-gray-300">
|
|
||||||
<div>Power: <strong className="text-red-400">{fmt(guardian.power)}</strong></div>
|
|
||||||
|
|
||||||
{guardian.armor > 0 && (
|
|
||||||
<div>Armor: <strong>{(guardian.armor * 100).toFixed(0)}%</strong></div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{guardian.effects && guardian.effects.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1 mt-2">
|
|
||||||
{guardian.effects.map((eff: any, i: number) => (
|
|
||||||
<Badge key={i} variant="outline" className="text-xs py-0">
|
|
||||||
{eff.type === 'burn' && `🔥 Burn ${(eff.value * 100).toFixed(0)}%`}
|
|
||||||
{eff.type === 'armor_pierce' && `🗡️ Pierce ${(eff.value * 100).toFixed(0)}%`}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmt(value: number): string {
|
|
||||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
|
||||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
|
||||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
|
||||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
|
||||||
return value.toFixed(0);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useCraftingStore, useManaStore } from '@/lib/game/stores';
|
|
||||||
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
|
|
||||||
export function LootTab() {
|
|
||||||
const lootInventory = useCraftingStore((s) => s.lootInventory);
|
|
||||||
const elements = useManaStore((s) => s.elements);
|
|
||||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
|
||||||
const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
|
|
||||||
const deleteEquipmentInstance = useCraftingStore((s) => s.deleteEquipmentInstance);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="LootTab">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<LootInventoryDisplay
|
|
||||||
inventory={lootInventory}
|
|
||||||
elements={elements}
|
|
||||||
equipmentInstances={equipmentInstances}
|
|
||||||
onDeleteMaterial={deleteMaterial}
|
|
||||||
onDeleteEquipment={deleteEquipmentInstance}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LootTab.displayName = "LootTab";
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// ─── Milestone Progress ───────────────────────────────────────────
|
|
||||||
// Milestone upgrade progress indicator for skill rows
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
|
|
||||||
interface MilestoneProgressProps {
|
|
||||||
milestoneInfo: {
|
|
||||||
milestone: 5 | 10;
|
|
||||||
hasUpgrades: boolean;
|
|
||||||
selectedCount: number;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MilestoneProgress({ milestoneInfo }: MilestoneProgressProps) {
|
|
||||||
if (!milestoneInfo) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="text-xs text-amber-400 mt-1 flex items-center gap-1">
|
|
||||||
⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// ─── Prestige/Grimoire Tab ──────────────────────────
|
|
||||||
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { usePrestigeStore, useSkillStore, useManaStore, useCraftingStore } from '@/lib/game/stores';
|
|
||||||
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
|
||||||
import {
|
|
||||||
ELEMENTS,
|
|
||||||
GUARDIANS,
|
|
||||||
PRESTIGE_DEF,
|
|
||||||
getStudySpeedMultiplier,
|
|
||||||
} from '@/lib/game/constants';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { fmt } from '@/lib/game/stores';
|
|
||||||
|
|
||||||
export function PrestigeTab() {
|
|
||||||
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
|
||||||
|
|
||||||
useGameLoop();
|
|
||||||
|
|
||||||
const skills = useSkillStore((s) => s.skills);
|
|
||||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
|
||||||
const elements = useManaStore((s) => s.elements);
|
|
||||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
|
||||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
|
||||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
|
||||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
|
||||||
const doPrestige = usePrestigeStore((s) => s.doPrestige);
|
|
||||||
|
|
||||||
const upgradeEffects = getUnifiedEffects({
|
|
||||||
skillUpgrades,
|
|
||||||
skillTiers,
|
|
||||||
equipmentInstances,
|
|
||||||
equippedInstances,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get unlocked elements for mana type selector
|
|
||||||
const unlockedElements = Object.entries(ELEMENTS)
|
|
||||||
.filter(([id]) => elements[id]?.unlocked)
|
|
||||||
.map(([id, elem]) => ({
|
|
||||||
id,
|
|
||||||
name: elem.name,
|
|
||||||
sym: elem.sym,
|
|
||||||
color: elem.color,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea className="h-full">
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{/* Prestige Upgrades */}
|
|
||||||
{Object.entries(PRESTIGE_DEF || {}).map(([id, def]) => {
|
|
||||||
const level = prestigeUpgrades[id] || 0;
|
|
||||||
const canAfford = rawMana >= def.cost;
|
|
||||||
const effect = upgradeEffects ? upgradeEffects.specials.has(id) : false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card key={id} className={effect ? "border-[var(--color-success)]/50 bg-[var(--color-success)]/10" : "bg-gray-900/80 border-gray-700"}>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-sm flex items-center gap-2">
|
|
||||||
<span>{def.name}</span>
|
|
||||||
{effect && <Badge className="bg-[var(--color-success)]/20 text-[var(--color-success)]">Active</Badge>}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-xs text-gray-400 mb-2">{def.description}</p>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-gray-400">Level: {level}/{def.maxLevel}</span>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
disabled={!canAfford || level >= def.maxLevel}
|
|
||||||
onClick={() => doPrestige(id)}
|
|
||||||
>
|
|
||||||
{level >= def.maxLevel ? 'Maxed' : `Upgrade (${fmt(def.cost)})`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Mana Type Selection for Attunements */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-sm">Select Mana Type for Attunement</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{unlockedElements.map(elem => (
|
|
||||||
<Button
|
|
||||||
key={elem.id}
|
|
||||||
variant={selectedManaType === elem.id ? "default" : "outline"}
|
|
||||||
onClick={() => setSelectedManaType(elem.id)}
|
|
||||||
className="justify-start"
|
|
||||||
>
|
|
||||||
<span className="mr-2" style={{ color: elem.color }}>{elem.sym}</span>
|
|
||||||
{elem.name}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PrestigeTab.displayName = "PrestigeTab";
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { Shield, Wind, Heart, ShieldCheck, Skull } from 'lucide-react';
|
|
||||||
import type { RoomDisplayProps } from '@/lib/game/types';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: string }> = {
|
|
||||||
combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
|
|
||||||
swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
|
|
||||||
speed: { label: 'Speed', icon: '💨', color: '#3B82F6' },
|
|
||||||
guardian: { label: 'Guardian', icon: '🛡️', color: '#EF4444' },
|
|
||||||
puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function RoomDisplay({
|
|
||||||
roomType,
|
|
||||||
roomConfig,
|
|
||||||
primaryEnemy,
|
|
||||||
swarmEnemies,
|
|
||||||
puzzleId,
|
|
||||||
puzzleProgress,
|
|
||||||
simpleMode,
|
|
||||||
floorElemDef,
|
|
||||||
floorHP,
|
|
||||||
floorMaxHP,
|
|
||||||
totalDPS,
|
|
||||||
currentAction,
|
|
||||||
activeEquipmentSpells,
|
|
||||||
}: RoomDisplayProps) {
|
|
||||||
// Puzzle Room Display
|
|
||||||
if (roomType === 'puzzle') {
|
|
||||||
return (
|
|
||||||
<div className="p-3 bg-purple-900/20 rounded border border-purple-700">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<span className="text-lg">🧩</span>
|
|
||||||
<span className="text-sm font-semibold text-purple-300">
|
|
||||||
{puzzleId ? puzzleId.replace(/_/g, ' ').toUpperCase() : 'Puzzle Room'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex justify-between text-xs text-gray-400">
|
|
||||||
<span>Progress</span>
|
|
||||||
<span>{((puzzleProgress || 0) * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={Math.min(100, (puzzleProgress || 0) * 100)} className="h-2 bg-gray-800" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single Enemy Display (Combat/Speed/Guardian - non-swarm)
|
|
||||||
if ((roomType === 'combat' || roomType === 'speed' || roomType === 'guardian') && primaryEnemy) {
|
|
||||||
return (
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded border border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Skull className="w-4 h-4 text-red-400" />
|
|
||||||
<span className="text-sm font-semibold text-gray-200">
|
|
||||||
{primaryEnemy.name || 'Unknown Enemy'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{ELEMENTS[primaryEnemy.element]?.sym} {ELEMENTS[primaryEnemy.element]?.name}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enemy HP Bar */}
|
|
||||||
<div className="space-y-1 mb-2">
|
|
||||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.max(0, (primaryEnemy.hp / primaryEnemy.maxHP) * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
|
||||||
<span>{fmt(primaryEnemy.hp)} / {fmt(primaryEnemy.maxHP)} HP</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enemy Properties */}
|
|
||||||
<EnemyProperties enemy={primaryEnemy} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swarm Enemies Display
|
|
||||||
if (roomType === 'swarm' && swarmEnemies && swarmEnemies.length > 0) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-gray-400 font-semibold">
|
|
||||||
Swarm Enemies ({swarmEnemies.length})
|
|
||||||
</div>
|
|
||||||
{swarmEnemies.map((enemy: any, index: number) => (
|
|
||||||
<div key={enemy.id || `swarm-${index}`} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Skull className="w-3 h-3 text-red-400" />
|
|
||||||
<span className="text-xs font-semibold text-gray-300">
|
|
||||||
{enemy.name || `Enemy ${index + 1}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
{ELEMENTS[enemy.element]?.sym} {fmt(enemy.hp)}/{fmt(enemy.maxHP)} HP
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.max(0, (enemy.hp / enemy.maxHP) * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${ELEMENTS[enemy.element]?.color}99, ${ELEMENTS[enemy.element]?.color})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<EnemyProperties enemy={enemy} small />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Floor HP Bar (non-swarm, non-puzzle)
|
|
||||||
return (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="h-3 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{
|
|
||||||
width: `${Math.max(0, (floorHP / floorMaxHP) * 100)}%`,
|
|
||||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
|
||||||
boxShadow: `0 0 10px ${floorElemDef?.glow}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
|
||||||
<span>{fmt(floorHP)} / {fmt(floorMaxHP)} HP</span>
|
|
||||||
<span>DPS: {currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EnemyProperties({ enemy, small }: { enemy: any; small?: boolean }) {
|
|
||||||
const props = [];
|
|
||||||
if (enemy.armor > 0) props.push({ type: 'armor', label: `${(enemy.armor * 100).toFixed(0)}% Armor`, icon: Shield });
|
|
||||||
if (enemy.dodgeChance > 0) props.push({ type: 'dodge', label: `${(enemy.dodgeChance * 100).toFixed(0)}% Dodge`, icon: Wind });
|
|
||||||
if (enemy.healthRegen > 0) props.push({ type: 'regen', label: `${(enemy.healthRegen * 100).toFixed(1)}% Regen`, icon: Heart });
|
|
||||||
if (enemy.barrier > 0) props.push({ type: 'barrier', label: `${(enemy.barrier * 100).toFixed(0)}% Barrier`, icon: ShieldCheck });
|
|
||||||
|
|
||||||
if (props.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex flex-wrap gap-2 mt-2 ${small ? 'mt-1' : ''}`}>
|
|
||||||
{props.map((p, i) => {
|
|
||||||
const Icon = p.icon;
|
|
||||||
return (
|
|
||||||
<Tooltip key={i}>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Badge variant="outline" className={`text-xs py-0 ${small ? 'text-xs py-0' : ''}`}>
|
|
||||||
<Icon className={`w-${small ? 2 : 3} h-${small ? 2 : 3} mr-1`} />
|
|
||||||
{p.label}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Reduces incoming damage by {(enemy.armor * 100).toFixed(0)}%</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmt(value: number): string {
|
|
||||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
|
||||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
|
||||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
|
||||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
|
||||||
return value.toFixed(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtDec(value: number): string {
|
|
||||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
|
||||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
|
||||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
|
||||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
|
||||||
return value.toFixed(0);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// ─── Skill Category Header ───────────────────────────────────────────
|
|
||||||
// Header for a skill category with collapse/expand toggle
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
||||||
|
|
||||||
interface SkillCategoryHeaderProps {
|
|
||||||
category: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
icon: string;
|
|
||||||
};
|
|
||||||
skillCount: number;
|
|
||||||
isCollapsed: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SkillCategoryHeader({
|
|
||||||
category,
|
|
||||||
skillCount,
|
|
||||||
isCollapsed,
|
|
||||||
onToggle,
|
|
||||||
}: SkillCategoryHeaderProps) {
|
|
||||||
return (
|
|
||||||
<CardHeader className="pb-2 cursor-pointer" onClick={onToggle}>
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center justify-between">
|
|
||||||
<span>
|
|
||||||
{category.icon} {category.name}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant="outline" className="text-xs">
|
|
||||||
{skillCount} skills
|
|
||||||
</Badge>
|
|
||||||
{isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
|
||||||
</div>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local import of CardHeader and CardTitle to avoid circular deps
|
|
||||||
import { CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// ─── Skill Multipliers ───────────────────────────────────────────
|
|
||||||
// Study speed and cost multiplier display for skill rows
|
|
||||||
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
type AdditionalCost = { type: 'element'; element: string; amount: number };
|
|
||||||
|
|
||||||
interface SkillMultipliersProps {
|
|
||||||
effectiveStudyTime: number;
|
|
||||||
speedMult: number;
|
|
||||||
costMult: number;
|
|
||||||
cost: number;
|
|
||||||
additionalCost?: AdditionalCost;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SkillMultipliers({
|
|
||||||
effectiveStudyTime,
|
|
||||||
speedMult,
|
|
||||||
costMult,
|
|
||||||
cost,
|
|
||||||
additionalCost,
|
|
||||||
}: SkillMultipliersProps) {
|
|
||||||
return (
|
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
|
||||||
<span className={speedMult > 1 ? 'text-green-400' : ''}>
|
|
||||||
Study: {formatStudyTime(effectiveStudyTime)}{speedMult > 1 && (
|
|
||||||
<span className="text-xs ml-1">({Math.round(speedMult * 100)}% speed)</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{' • '}
|
|
||||||
<span className={costMult < 1 ? 'text-green-400' : ''}>
|
|
||||||
Cost: {cost} mana{costMult < 1 && (
|
|
||||||
<span className="text-xs ml-1">({Math.round(costMult * 100)}% cost)</span>
|
|
||||||
)}
|
|
||||||
{additionalCost && additionalCost.type === 'element' && (
|
|
||||||
<span className="ml-2" style={{ color: ELEMENTS[additionalCost.element]?.color }}>
|
|
||||||
+ {additionalCost.amount} {ELEMENTS[additionalCost.element]?.sym} {additionalCost.element}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatStudyTime(ms: number): string {
|
|
||||||
if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
|
|
||||||
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`;
|
|
||||||
if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ${Math.floor((ms % 3_600_000) / 60_000)}m`;
|
|
||||||
return `${Math.floor(ms / 86_400_000)}d ${Math.floor((ms % 86_400_000) / 3_600_000)}h`;
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
// ─── Skill Row Component ───────────────────────────────────────────────────
|
|
||||||
// Individual skill row for the Skills tab - extracted from SkillsTab for modularity
|
|
||||||
|
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { ChevronRight } from 'lucide-react';
|
|
||||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
import { formatStudyTime } from '@/lib/game/formatting';
|
|
||||||
import { getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier, SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
|
|
||||||
import type { ComputedEffects } from '@/lib/game/upgrade-effects.types';
|
|
||||||
import { SPECIAL_EFFECTS, hasSpecial } from '@/lib/game/special-effects';
|
|
||||||
import { MilestoneProgress } from './MilestoneProgress';
|
|
||||||
import { SkillMultipliers } from './SkillMultipliers';
|
|
||||||
|
|
||||||
type StudyTarget = { type: 'skill' | 'spell'; id: string; progress: number; required: number; } | null;
|
|
||||||
|
|
||||||
interface SkillRowProps {
|
|
||||||
skillId: string;
|
|
||||||
def: {
|
|
||||||
name: string;
|
|
||||||
desc: string;
|
|
||||||
cat: string;
|
|
||||||
max: number;
|
|
||||||
studyTime: number;
|
|
||||||
base: number;
|
|
||||||
cost?: { type: 'element'; element: string; amount: number } | { type: 'raw'; amount: number };
|
|
||||||
req?: Record<string, number>;
|
|
||||||
};
|
|
||||||
level: number;
|
|
||||||
maxed: boolean;
|
|
||||||
isStudying: boolean;
|
|
||||||
tierMultiplier: number;
|
|
||||||
skillDisplayName: string;
|
|
||||||
selectedUpgrades: string[];
|
|
||||||
selectedL5: string[];
|
|
||||||
selectedL10: string[];
|
|
||||||
prereqMet: boolean;
|
|
||||||
canStudy: boolean;
|
|
||||||
isParallelStudy: boolean;
|
|
||||||
canParallelStudy: boolean;
|
|
||||||
canTierUp: boolean;
|
|
||||||
hasInsufficientMana: boolean;
|
|
||||||
currentStudyTarget: StudyTarget;
|
|
||||||
milestoneInfo: { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null;
|
|
||||||
upgradeEffects: ComputedEffects;
|
|
||||||
// Costs and times
|
|
||||||
cost: number;
|
|
||||||
additionalCost?: { type: 'element'; element: string; amount: number };
|
|
||||||
effectiveStudyTime: number;
|
|
||||||
costMult: number;
|
|
||||||
speedMult: number;
|
|
||||||
// Callbacks
|
|
||||||
onStudy: (skillId: string) => void;
|
|
||||||
onParallelStudy: (skillId: string) => void;
|
|
||||||
onCancelStudy: (skillId: string) => void;
|
|
||||||
onUpgradeDialogOpen: (skillId: string, milestone: 5 | 10) => void;
|
|
||||||
onTierUp: (skillId: string) => void;
|
|
||||||
onShowToast: (type: 'info' | 'error', title: string, description: string) => void;
|
|
||||||
tierUpLabel?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SkillRow(props: SkillRowProps) {
|
|
||||||
const {
|
|
||||||
skillId,
|
|
||||||
def,
|
|
||||||
level,
|
|
||||||
maxed,
|
|
||||||
isStudying,
|
|
||||||
tierMultiplier,
|
|
||||||
skillDisplayName,
|
|
||||||
selectedUpgrades,
|
|
||||||
selectedL5,
|
|
||||||
selectedL10,
|
|
||||||
prereqMet,
|
|
||||||
canStudy,
|
|
||||||
isParallelStudy,
|
|
||||||
canParallelStudy,
|
|
||||||
canTierUp,
|
|
||||||
hasInsufficientMana,
|
|
||||||
currentStudyTarget,
|
|
||||||
milestoneInfo,
|
|
||||||
upgradeEffects,
|
|
||||||
cost,
|
|
||||||
additionalCost,
|
|
||||||
effectiveStudyTime,
|
|
||||||
costMult,
|
|
||||||
speedMult,
|
|
||||||
onStudy,
|
|
||||||
onParallelStudy,
|
|
||||||
onCancelStudy,
|
|
||||||
onUpgradeDialogOpen,
|
|
||||||
onTierUp,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={skillId}
|
|
||||||
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>
|
|
||||||
{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}{level > 0 && tierMultiplier !== 1 && ` (Tier ${tierMultiplier}x effect)`}</div>
|
|
||||||
{!prereqMet && def.req && (
|
|
||||||
<div className="text-xs text-red-400 mt-1">
|
|
||||||
Requires: {Object.entries(def.req).map(([r, rl]) => `${r} Lv.${rl}`).join(', ')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<SkillMultipliers
|
|
||||||
effectiveStudyTime={effectiveStudyTime}
|
|
||||||
speedMult={speedMult}
|
|
||||||
costMult={costMult}
|
|
||||||
cost={cost}
|
|
||||||
additionalCost={additionalCost}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{hasInsufficientMana && (
|
|
||||||
<div className="text-xs text-red-400 mt-1">
|
|
||||||
Insufficient mana! Need {cost} mana to study.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<MilestoneProgress milestoneInfo={milestoneInfo} />
|
|
||||||
</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(currentStudyTarget?.progress || 0)}/{formatStudyTime(def.studyTime * (level > 1 ? level : 1))}
|
|
||||||
</div>
|
|
||||||
) : milestoneInfo ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="bg-amber-600 hover:bg-amber-700"
|
|
||||||
onClick={() => onUpgradeDialogOpen(skillId, milestoneInfo.milestone)}
|
|
||||||
>
|
|
||||||
Choose Upgrades
|
|
||||||
</Button>
|
|
||||||
) : canTierUp ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
|
||||||
onClick={() => onTierUp(skillId)}
|
|
||||||
>
|
|
||||||
⬆️ Tier Up
|
|
||||||
</Button>
|
|
||||||
) : maxed ? (
|
|
||||||
<Badge className="bg-green-900/50 text-green-300">Maxed</Badge>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant={canStudy ? 'default' : 'outline'}
|
|
||||||
disabled={!canStudy}
|
|
||||||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
|
||||||
onClick={() => {
|
|
||||||
if (cost > 0) {
|
|
||||||
onStudy(skillId);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Study ({cost}{additionalCost && additionalCost.type === 'element' && ` + ${additionalCost.amount} ${ELEMENTS[additionalCost.element]?.sym}`}
|
|
||||||
</Button>
|
|
||||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) &&
|
|
||||||
currentStudyTarget &&
|
|
||||||
!isParallelStudy &&
|
|
||||||
canParallelStudy &&
|
|
||||||
canStudy && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
|
||||||
onClick={() => {
|
|
||||||
if (cost > 0) {
|
|
||||||
onParallelStudy(skillId);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
⚡
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Study in parallel (50% speed)</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useCombatStore, useCraftingStore, useManaStore, usePrestigeStore } from '@/lib/game/stores';
|
|
||||||
import { GameCard, ElementBadge } from '@/components/ui';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
|
||||||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
|
||||||
import { canAffordSpellCost } from '@/lib/game/utils';
|
|
||||||
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
|
|
||||||
|
|
||||||
export function SpellsTab() {
|
|
||||||
// Use modular stores directly
|
|
||||||
const spells = useCombatStore((s) => s.spells);
|
|
||||||
const activeSpell = useCombatStore((s) => s.activeSpell);
|
|
||||||
const setSpell = useCombatStore((s) => s.setSpell);
|
|
||||||
|
|
||||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
|
||||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
|
||||||
|
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
|
||||||
const elements = useManaStore((s) => s.elements);
|
|
||||||
|
|
||||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
|
||||||
const unlockedEffects = useCraftingStore((s) => s.unlockedEffects);
|
|
||||||
|
|
||||||
// Get spells from equipment
|
|
||||||
const equipmentSpellIds: string[] = [];
|
|
||||||
const spellSources: Record<string, string[]> = {};
|
|
||||||
|
|
||||||
// Guard against undefined stores during initialization
|
|
||||||
if (!equippedInstances || !equipmentInstances) {
|
|
||||||
return (
|
|
||||||
|
|
||||||
<DebugName name="SpellsTab">
|
|
||||||
<div className="p-4 text-center text-[var(--text-muted)]">
|
|
||||||
Loading spell data...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</DebugName>);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const instanceId of Object.values(equippedInstances || {})) {
|
|
||||||
if (!instanceId) continue;
|
|
||||||
const instance = 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) {
|
|
||||||
const spellId = effectDef.effect.spellId;
|
|
||||||
if (!equipmentSpellIds.includes(spellId)) {
|
|
||||||
equipmentSpellIds.push(spellId);
|
|
||||||
}
|
|
||||||
if (!spellSources[spellId]) {
|
|
||||||
spellSources[spellId] = [];
|
|
||||||
}
|
|
||||||
spellSources[spellId].push(instance.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const canCastSpell = (spellId: string): boolean => {
|
|
||||||
const spell = SPELLS_DEF[spellId];
|
|
||||||
if (!spell || !spell.cost) return false;
|
|
||||||
return canAffordSpellCost(spell.cost, rawMana, elements);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasPactSpells = signedPacts && signedPacts.length > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Equipment-Granted Spells */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--mana-crystal)]">
|
|
||||||
Known Spells
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
|
||||||
Spells are obtained by enchanting equipment with spell effects.
|
|
||||||
Visit the Crafting tab to design and apply enchantments.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{equipmentSpellIds.length > 0 ? (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{equipmentSpellIds.map(id => {
|
|
||||||
const def = SPELLS_DEF[id];
|
|
||||||
if (!def) return null;
|
|
||||||
|
|
||||||
const isActive = activeSpell === id;
|
|
||||||
const canCast = canCastSpell(id);
|
|
||||||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
|
||||||
const sources = spellSources[id] || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GameCard
|
|
||||||
key={id}
|
|
||||||
className={canCast ? 'ring-1 ring-[var(--color-success)]/30' : ''}
|
|
||||||
>
|
|
||||||
<div className="pb-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4
|
|
||||||
className="text-sm font-semibold"
|
|
||||||
style={{ color: def.elem === 'raw' ? 'var(--mana-transfer)' : `var(--mana-${def.elem})` }}
|
|
||||||
>
|
|
||||||
{def.name}
|
|
||||||
</h4>
|
|
||||||
<Badge className="bg-[var(--bg-elevated)] text-[var(--mana-crystal)] text-xs border border-[var(--mana-crystal)]/30">
|
|
||||||
Equipment
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-[var(--text-secondary)]">
|
|
||||||
{def.elem !== 'raw' && (
|
|
||||||
<span className="mr-2">
|
|
||||||
<ElementBadge element={def.elem} size="sm" /> {elemDef?.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span>⚔️ {def.dmg} dmg</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs font-[var(--font-mono)]" style={{ color: getSpellCostColor(def.cost) }}>
|
|
||||||
Cost: {formatSpellCost(def.cost)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-[var(--mana-crystal)]/70">From: {sources.join(', ')}</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{isActive ? (
|
|
||||||
<Badge className="bg-[var(--bg-elevated)] text-[var(--color-warning)] border border-[var(--color-warning)]/30">
|
|
||||||
Active
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="px-3 py-1 text-xs border border-[var(--border-default)] rounded hover:border-[var(--border-focus)] transition-colors"
|
|
||||||
onClick={() => setSpell(id)}
|
|
||||||
>
|
|
||||||
Set Active
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center p-8 bg-[var(--bg-sunken)] rounded border border-[var(--border-subtle)]">
|
|
||||||
<div className="text-[var(--text-muted)] mb-2">No spells known yet</div>
|
|
||||||
<div className="text-sm text-[var(--text-muted)]">Enchant a staff with a spell effect to gain spells</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pact Spells (from guardian defeats) - Empty State */}
|
|
||||||
{!hasPactSpells && (
|
|
||||||
<div className="mb-6">
|
|
||||||
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--color-warning)]">
|
|
||||||
Pact Spells
|
|
||||||
</h3>
|
|
||||||
<div className="text-center p-6 bg-[var(--bg-sunken)] rounded border border-[var(--border-subtle)]">
|
|
||||||
<p className="text-sm text-[var(--text-muted)]">Defeat guardians and sign pacts to unlock powerful spells</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasPactSpells && (
|
|
||||||
<div className="mb-6">
|
|
||||||
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--color-warning)]">
|
|
||||||
Pact Spells
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-3">Spells earned through guardian pacts appear here.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Spell Reference - show all available spells for enchanting */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--mana-death)]">
|
|
||||||
Spell Reference
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
|
||||||
These spells can be applied to equipment through the enchanting system.
|
|
||||||
Research enchantment effects in the Skills tab to unlock them for designing.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{Object.entries(SPELLS_DEF).map(([id, def]) => {
|
|
||||||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
|
||||||
const isUnlocked = unlockedEffects?.includes(`spell_${id}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GameCard
|
|
||||||
key={id}
|
|
||||||
variant={isUnlocked ? "default" : "sunken"}
|
|
||||||
className={isUnlocked ? 'border-[var(--mana-death)]/50' : 'opacity-60'}
|
|
||||||
>
|
|
||||||
<div className="pb-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h4
|
|
||||||
className="text-sm font-semibold"
|
|
||||||
style={{ color: def.elem === 'raw' ? 'var(--mana-transfer)' : `var(--mana-${def.elem})` }}
|
|
||||||
>
|
|
||||||
{def.name}
|
|
||||||
</h4>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{def.tier > 0 && (
|
|
||||||
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
|
||||||
T{def.tier}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isUnlocked && (
|
|
||||||
<Badge className="bg-[var(--bg-elevated)] text-[var(--mana-death)] text-xs border border-[var(--mana-death)]/30">
|
|
||||||
Unlocked
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-[var(--text-secondary)]">
|
|
||||||
{def.elem !== 'raw' && (
|
|
||||||
<span className="mr-2">
|
|
||||||
<ElementBadge element={def.elem} size="sm" /> {elemDef?.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span>⚔️ {def.dmg} dmg</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs font-[var(--font-mono)]" style={{ color: getSpellCostColor(def.cost) }}>
|
|
||||||
Cost: {formatSpellCost(def.cost)}
|
|
||||||
</div>
|
|
||||||
{def.desc && (
|
|
||||||
<div className="text-xs text-[var(--text-muted)] italic">{def.desc}</div>
|
|
||||||
)}
|
|
||||||
{!isUnlocked && (
|
|
||||||
<div className="text-xs text-[var(--color-warning)]/70">Research to unlock for enchanting</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</GameCard>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SpellsTab.displayName = "SpellsTab";
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
|
||||||
import { calcDamage } from '@/lib/game/stores';
|
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
|
||||||
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
|
|
||||||
import { canAffordSpellCost } from '@/lib/game/stores';
|
|
||||||
import type { EquipmentSpellState } from '@/lib/game/types';
|
|
||||||
|
|
||||||
interface ActiveSpellsProps {
|
|
||||||
activeEquipmentSpells: { spellId: string; equipmentId: string }[];
|
|
||||||
spells: Record<string, { learned: boolean; level: number; studyProgress: number }>;
|
|
||||||
equipmentSpellStates: EquipmentSpellState[];
|
|
||||||
skills: Record<string, number>;
|
|
||||||
skillUpgrades: Record<string, string[]>;
|
|
||||||
skillTiers: Record<string, number>;
|
|
||||||
signedPacts: number[];
|
|
||||||
currentAction: string;
|
|
||||||
floorElem: string;
|
|
||||||
canCastSpell: (spellId: string) => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SpireActiveSpells({
|
|
||||||
activeEquipmentSpells,
|
|
||||||
spells,
|
|
||||||
equipmentSpellStates,
|
|
||||||
skills,
|
|
||||||
skillUpgrades,
|
|
||||||
skillTiers,
|
|
||||||
signedPacts,
|
|
||||||
currentAction,
|
|
||||||
floorElem,
|
|
||||||
canCastSpell,
|
|
||||||
}: ActiveSpellsProps) {
|
|
||||||
return (
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardContent className="pt-4 pb-4">
|
|
||||||
<div className="text-amber-400 text-xs font-semibold mb-3 uppercase tracking-wider flex items-center gap-2">
|
|
||||||
<span>Active Spells ({activeEquipmentSpells.length})</span>
|
|
||||||
</div>
|
|
||||||
{activeEquipmentSpells.length > 0 ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
|
||||||
const spellDef = SPELLS_DEF[spellId];
|
|
||||||
if (!spellDef) return null;
|
|
||||||
const spellState = equipmentSpellStates?.find(
|
|
||||||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
|
||||||
);
|
|
||||||
const progress = spellState?.castProgress || 0;
|
|
||||||
const canCast = canCastSpell(spellId);
|
|
||||||
|
|
||||||
// Compute progress bar JSX
|
|
||||||
let progressBar = null;
|
|
||||||
if (currentAction === 'climb') {
|
|
||||||
const widthPercent = Math.min(100, progress * 100);
|
|
||||||
const elemColor = spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color || '#60A5FA';
|
|
||||||
const backgroundGradient = 'linear-gradient(90deg, ' + elemColor + '99, ' + elemColor + ')';
|
|
||||||
progressBar = (
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<div className="flex justify-between text-xs text-gray-500">
|
|
||||||
<span>Cast</span>
|
|
||||||
<span>{widthPercent.toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{ width: widthPercent + '%', background: backgroundGradient }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={`${spellId}-${equipmentId}`} className="p-2 rounded bg-gray-800/30 border border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="text-sm font-semibold" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
|
||||||
{spellDef.name}
|
|
||||||
{spellDef.tier === 0 && <span className="ml-2 bg-gray-600 text-gray-200 text-xs px-1 rounded">Basic</span>}
|
|
||||||
</span>
|
|
||||||
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>{canCast ? '✓' : '✗'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 mb-1">
|
|
||||||
⚔️ {calcDamage({ skills, signedPacts, skillUpgrades, skillTiers }, spellId, floorElem)} dmg • <span style={{ color: getSpellCostColor(spellDef.cost) }}>{formatSpellCost(spellDef.cost)}</span> {' '}• ⚡ {Math.floor(calcDamage({ skills, signedPacts, skillUpgrades, skillTiers }, spellId, floorElem) * (spellDef.castSpeed || 1))} dmg/hr
|
|
||||||
</div>
|
|
||||||
{progressBar}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-gray-500 text-sm">No spells on equipped weapons. Enchant a staff with spell effects in the Crafting tab.</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import { Mountain } from 'lucide-react';
|
|
||||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
interface SpireGolemsProps {
|
|
||||||
golemancy: {
|
|
||||||
summonedGolems: string[];
|
|
||||||
enabledGolems: string[];
|
|
||||||
};
|
|
||||||
skills: Record<string, number>;
|
|
||||||
floorElem: string;
|
|
||||||
currentAction: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SpireGolems({ golemancy, skills, floorElem }: SpireGolemsProps) {
|
|
||||||
if (golemancy.summonedGolems.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-gray-900/80 border-amber-600/50">
|
|
||||||
<CardContent className="pt-4 pb-4">
|
|
||||||
<div className="text-amber-400 text-xs font-semibold mb-3 uppercase tracking-wider flex items-center gap-2">
|
|
||||||
<Mountain className="w-4 h-4" />
|
|
||||||
Active Golems ({golemancy.summonedGolems.length})
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{golemancy.summonedGolems.map((summoned) => {
|
|
||||||
const golemDef = GOLEMS_DEF[summoned.golemId];
|
|
||||||
if (!golemDef) return null;
|
|
||||||
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
|
|
||||||
const damage = getGolemDamage(summoned.golemId, skills);
|
|
||||||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, skills);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={summoned.golemId} className="p-2 rounded bg-gray-800/30 border border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Mountain className="w-3 h-3" style={{ color: elemColor }} />
|
|
||||||
<span className="text-xs font-semibold" style={{ color: elemColor }}>{golemDef.name}</span>
|
|
||||||
</div>
|
|
||||||
{golemDef.isAoe && <span className="text-xs border border-gray-600 px-1 rounded">AOE {golemDef.aoeTargets}</span>}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400">⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr</div>
|
|
||||||
{currentAction === 'climb' && summoned.attackProgress > 0 && (
|
|
||||||
<div className="mt-1">
|
|
||||||
<div className="flex justify-between text-xs text-gray-500 mb-0.5">
|
|
||||||
<span>Attack</span>
|
|
||||||
<span>{(summoned.attackProgress * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1 bg-gray-700 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${Math.min(100, summoned.attackProgress * 100)}%`, background: elemColor }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { Mountain } from 'lucide-react';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
interface SpireHeaderProps {
|
|
||||||
currentFloor: number;
|
|
||||||
maxFloorReached: number;
|
|
||||||
signedPacts: number;
|
|
||||||
isGuardianFloor: boolean;
|
|
||||||
roomType: string;
|
|
||||||
roomLabel: string;
|
|
||||||
roomIcon: string;
|
|
||||||
roomColor: string;
|
|
||||||
floorElem: string;
|
|
||||||
floorElemDef: any;
|
|
||||||
simpleMode: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SpireHeader({
|
|
||||||
currentFloor,
|
|
||||||
maxFloorReached,
|
|
||||||
signedPacts,
|
|
||||||
isGuardianFloor,
|
|
||||||
roomType,
|
|
||||||
roomLabel,
|
|
||||||
roomIcon,
|
|
||||||
roomColor,
|
|
||||||
floorElem,
|
|
||||||
floorElemDef,
|
|
||||||
simpleMode,
|
|
||||||
}: SpireHeaderProps) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Current Floor Card - Only show in Spire Mode (simpleMode) */}
|
|
||||||
{simpleMode && (
|
|
||||||
<div className="bg-gray-900/80 border-gray-700 rounded-lg p-4 lg:p-6">
|
|
||||||
<div className="pb-2 mb-4 border-b border-gray-700">
|
|
||||||
<h3 className="text-amber-400 text-xs font-semibold tracking-wide uppercase flex items-center justify-between">
|
|
||||||
<span>Current Floor</span>
|
|
||||||
<Badge
|
|
||||||
className="ml-2"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${roomColor}20`,
|
|
||||||
color: roomColor,
|
|
||||||
borderColor: `${roomColor}60`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{roomIcon} {roomLabel}
|
|
||||||
</Badge>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-baseline gap-2">
|
|
||||||
<span className="text-4xl font-bold tracking-tight" style={{ color: floorElemDef?.color }}>
|
|
||||||
{currentFloor}
|
|
||||||
</span>
|
|
||||||
<span className="text-gray-400 text-sm">/ 100</span>
|
|
||||||
<span className="ml-auto text-sm" style={{ color: floorElemDef?.color }}>
|
|
||||||
{floorElemDef?.sym} {floorElemDef?.name}
|
|
||||||
</span>
|
|
||||||
{isGuardianFloor && (
|
|
||||||
<Badge className="bg-red-900/50 text-red-300 border-red-600">GUARDIAN</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isGuardianFloor && (
|
|
||||||
<div className="text-sm font-semibold" style={{ color: floorElemDef?.color }}>
|
|
||||||
⚔️ Guardian Floor
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
Best: Floor <strong className="text-gray-200">{maxFloorReached}</strong> •
|
|
||||||
Pacts: <strong className="text-amber-400">{signedPacts}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Spire Stats Card - Only show in normal mode */}
|
|
||||||
{!simpleMode && (
|
|
||||||
<div className="bg-gray-900/80 border-gray-700 rounded-lg p-4 lg:p-6">
|
|
||||||
<div className="pb-2 mb-4 border-b border-gray-700">
|
|
||||||
<h3 className="text-amber-400 text-xs font-semibold tracking-wide uppercase">Spire Stats</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded">
|
|
||||||
<div className="text-2xl font-bold text-amber-400 tracking-tight">{maxFloorReached}</div>
|
|
||||||
<div className="text-xs text-gray-400">Best Floor</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-gray-800/50 rounded">
|
|
||||||
<div className="text-2xl font-bold text-purple-400 tracking-tight">{signedPacts}</div>
|
|
||||||
<div className="text-xs text-gray-400">Pacts Signed</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 text-sm text-gray-400">
|
|
||||||
Current Floor: <strong className="text-gray-200">{currentFloor}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import { Mountain, ChevronDown } from 'lucide-react';
|
|
||||||
import type { ActivityLogEntry } from '@/lib/game/types';
|
|
||||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants';
|
|
||||||
import { calcDamage, getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/utils';
|
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
|
||||||
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
|
|
||||||
import { canAffordSpellCost, getFloorElement } from '@/lib/game/utils';
|
|
||||||
import { useManaStore, useSkillStore, useCombatStore, usePrestigeStore, useCraftingStore } from '@/lib/game/stores';
|
|
||||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
|
||||||
|
|
||||||
// Extracted components
|
|
||||||
import { SpireHeader } from './SpireHeader';
|
|
||||||
import { GuardianPanel } from './GuardianPanel';
|
|
||||||
import { RoomDisplay } from './RoomDisplay';
|
|
||||||
import { FloorControls } from './FloorControls';
|
|
||||||
import { CombatStatsPanel } from './CombatStatsPanel';
|
|
||||||
import { ActivityLog } from './ActivityLog';
|
|
||||||
import { SpireActiveSpells } from './SpireActiveSpells';
|
|
||||||
import { SpireGolems } from './SpireGolems';
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
|
|
||||||
// Room type configurations
|
|
||||||
const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: string }> = {
|
|
||||||
combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
|
|
||||||
swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
|
|
||||||
speed: { label: 'Speed', icon: '💨', color: '#3B82F6' },
|
|
||||||
guardian: { label: 'Guardian', icon: '🛡️', color: '#EF4444' },
|
|
||||||
puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SpireTabProps {
|
|
||||||
simpleMode?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if player can enter spire mode
|
|
||||||
const canEnterSpireMode = (spireMode: boolean): boolean => {
|
|
||||||
return !spireMode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SpireTab({ simpleMode = false }: SpireTabProps) {
|
|
||||||
// Get state from modular stores
|
|
||||||
const currentFloor = useCombatStore((s) => s.currentFloor);
|
|
||||||
const floorHP = useCombatStore((s) => s.floorHP);
|
|
||||||
const floorMaxHP = useCombatStore((s) => s.floorMaxHP);
|
|
||||||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
|
||||||
const currentAction = useCombatStore((s) => s.currentAction);
|
|
||||||
const castProgress = useCombatStore((s) => s.castProgress);
|
|
||||||
const activeSpell = useCombatStore((s) => s.activeSpell);
|
|
||||||
const spireMode = useCombatStore((s) => s.spireMode);
|
|
||||||
const climbDirection = useCombatStore((s) => s.climbDirection) || 'up';
|
|
||||||
const clearedFloors = useCombatStore((s) => s.clearedFloors || {});
|
|
||||||
const currentRoom = useCombatStore((s) => s.currentRoom);
|
|
||||||
const equipmentSpellStates = useCombatStore((s) => s.equipmentSpellStates);
|
|
||||||
const golemancy = useCombatStore((s) => s.golemancy);
|
|
||||||
const activityLog = useCombatStore((s) => s.activityLog);
|
|
||||||
|
|
||||||
const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
|
|
||||||
const parallelStudyTarget = useSkillStore((s) => s.parallelStudyTarget);
|
|
||||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
|
||||||
const skills = useSkillStore((s) => s.skills);
|
|
||||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
|
||||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
|
||||||
|
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
|
||||||
const elements = useManaStore((s) => s.elements);
|
|
||||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
|
||||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
|
||||||
const designProgress = useCraftingStore((s) => s.designProgress);
|
|
||||||
const designProgress2 = useCraftingStore((s) => s.designProgress2);
|
|
||||||
const preparationProgress = useCraftingStore((s) => s.preparationProgress);
|
|
||||||
const applicationProgress = useCraftingStore((s) => s.applicationProgress);
|
|
||||||
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
|
||||||
|
|
||||||
// Derived data
|
|
||||||
const floorElem = getFloorElement(currentFloor);
|
|
||||||
const floorElemDef = ELEMENTS[floorElem];
|
|
||||||
const isGuardianFloor = !!GUARDIANS[currentFloor];
|
|
||||||
const currentGuardian = GUARDIANS[currentFloor];
|
|
||||||
const isFloorCleared = clearedFloors[currentFloor];
|
|
||||||
const roomType = currentRoom?.roomType || 'combat';
|
|
||||||
const roomConfig = ROOM_TYPE_CONFIG[roomType] || ROOM_TYPE_CONFIG.combat;
|
|
||||||
|
|
||||||
const activeEquipmentSpells = useMemo(
|
|
||||||
() => getActiveEquipmentSpells(equippedInstances, equipmentInstances),
|
|
||||||
[equippedInstances, equipmentInstances]
|
|
||||||
);
|
|
||||||
|
|
||||||
const upgradeEffects = useMemo(
|
|
||||||
() => getUnifiedEffects({
|
|
||||||
skillUpgrades,
|
|
||||||
skillTiers,
|
|
||||||
equippedInstances,
|
|
||||||
equipmentInstances,
|
|
||||||
}),
|
|
||||||
[skillUpgrades, skillTiers, equippedInstances, equipmentInstances]
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalDPS = useMemo(
|
|
||||||
() => getTotalDPS({ skills, signedPacts, skillUpgrades, skillTiers }, upgradeEffects, floorElem),
|
|
||||||
[skills, signedPacts, skillUpgrades, skillTiers, upgradeEffects, floorElem]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Spell casting check
|
|
||||||
const canCastSpell = (spellId: string): boolean => {
|
|
||||||
const spell = SPELLS_DEF[spellId];
|
|
||||||
if (!spell || !spell.cost) return false;
|
|
||||||
return canAffordSpellCost(spell.cost, rawMana, elements);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Climb handlers - defined OUTSIDE of JSX
|
|
||||||
const handleClimbUp = () => {
|
|
||||||
useCombatStore.getState().startClimbUp();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClimbDown = () => {
|
|
||||||
useCombatStore.getState().startClimbDown();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClimb = (direction: 'up' | 'down') => {
|
|
||||||
if (direction === 'up') {
|
|
||||||
handleClimbUp();
|
|
||||||
} else {
|
|
||||||
handleClimbDown();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSkillName = (skillId: string): string => {
|
|
||||||
return SKILLS_DEF[skillId]?.name || skillId;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle exit spire mode
|
|
||||||
const exitSpireMode = () => {
|
|
||||||
useCombatStore.getState().exitSpireMode();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle enter spire mode
|
|
||||||
const enterSpireMode = () => {
|
|
||||||
useCombatStore.getState().enterSpireMode();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="SpireTab">
|
|
||||||
<div className="grid gap-4">
|
|
||||||
{/* Enter Spire Mode - Normal mode only */}
|
|
||||||
{!simpleMode && (
|
|
||||||
<Card className="bg-gray-900/80 border-amber-600/50">
|
|
||||||
<CardContent className="pt-4">
|
|
||||||
<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={enterSpireMode}
|
|
||||||
disabled={!canEnterSpireMode(spireMode)}
|
|
||||||
>
|
|
||||||
<Mountain className="w-5 h-5 mr-2" />
|
|
||||||
Enter Spire Mode
|
|
||||||
</Button>
|
|
||||||
<div className="text-xs text-gray-400 text-center mt-2">
|
|
||||||
Climb the Spire to face guardians and earn pacts
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Exit Spire Mode - Spire mode only */}
|
|
||||||
{simpleMode && (
|
|
||||||
<Card className="bg-gray-900/80 border-red-600/50">
|
|
||||||
<CardContent className="pt-4">
|
|
||||||
<Button
|
|
||||||
className="w-full bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800"
|
|
||||||
size="lg"
|
|
||||||
onClick={exitSpireMode}
|
|
||||||
>
|
|
||||||
<ChevronDown className="w-5 h-5 mr-2" />
|
|
||||||
Exit Spire Mode
|
|
||||||
</Button>
|
|
||||||
<div className="text-xs text-gray-400 text-center mt-2">
|
|
||||||
Climb down to floor 1 to return to the main game
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Spire Header */}
|
|
||||||
<SpireHeader
|
|
||||||
currentFloor={currentFloor}
|
|
||||||
maxFloorReached={maxFloorReached}
|
|
||||||
signedPacts={signedPacts.length}
|
|
||||||
isGuardianFloor={isGuardianFloor}
|
|
||||||
roomType={roomType}
|
|
||||||
roomLabel={roomConfig.label}
|
|
||||||
roomIcon={roomConfig.icon}
|
|
||||||
roomColor={roomConfig.color}
|
|
||||||
floorElem={floorElem}
|
|
||||||
floorElemDef={floorElemDef}
|
|
||||||
simpleMode={simpleMode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Active Spells Card - Spire Mode only */}
|
|
||||||
{simpleMode && (
|
|
||||||
<SpireActiveSpells
|
|
||||||
activeEquipmentSpells={activeEquipmentSpells}
|
|
||||||
spells={useSkillStore.getState().spells}
|
|
||||||
equipmentSpellStates={equipmentSpellStates}
|
|
||||||
skills={skills}
|
|
||||||
skillUpgrades={skillUpgrades}
|
|
||||||
skillTiers={skillTiers}
|
|
||||||
signedPacts={signedPacts}
|
|
||||||
currentAction={currentAction}
|
|
||||||
floorElem={floorElem}
|
|
||||||
canCastSpell={canCastSpell}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Summoned Golems */}
|
|
||||||
{simpleMode && golemancy.summonedGolems.length > 0 && (
|
|
||||||
<SpireGolems
|
|
||||||
golemancy={golemancy}
|
|
||||||
skills={skills}
|
|
||||||
currentAction={currentAction}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Guardian Panel */}
|
|
||||||
{isGuardianFloor && simpleMode && (
|
|
||||||
<GuardianPanel
|
|
||||||
currentFloor={currentFloor}
|
|
||||||
floorElemDef={floorElemDef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Room Display */}
|
|
||||||
{simpleMode && (
|
|
||||||
<RoomDisplay
|
|
||||||
roomType={roomType}
|
|
||||||
roomConfig={roomConfig}
|
|
||||||
primaryEnemy={currentRoom?.enemies?.[0] || null}
|
|
||||||
swarmEnemies={roomType === 'swarm' ? (currentRoom?.enemies || []) : []}
|
|
||||||
puzzleId={currentRoom?.puzzleId}
|
|
||||||
puzzleProgress={currentRoom?.puzzleProgress}
|
|
||||||
simpleMode={true}
|
|
||||||
floorElemDef={floorElemDef}
|
|
||||||
floorHP={floorHP}
|
|
||||||
floorMaxHP={floorMaxHP}
|
|
||||||
totalDPS={totalDPS}
|
|
||||||
currentAction={currentAction}
|
|
||||||
activeEquipmentSpells={activeEquipmentSpells}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Floor Controls */}
|
|
||||||
{simpleMode && (
|
|
||||||
<FloorControls
|
|
||||||
currentFloor={currentFloor}
|
|
||||||
floorHP={floorHP}
|
|
||||||
floorMaxHP={floorMaxHP}
|
|
||||||
maxFloorReached={maxFloorReached}
|
|
||||||
equipmentSpellStates={equipmentSpellStates}
|
|
||||||
skills={skills}
|
|
||||||
signedPacts={signedPacts}
|
|
||||||
storeCurrentAction={currentAction}
|
|
||||||
climbDirection={climbDirection}
|
|
||||||
isGuardianFloor={isGuardianFloor}
|
|
||||||
currentRoom={currentRoom}
|
|
||||||
currentGuardian={currentGuardian}
|
|
||||||
isFloorCleared={isFloorCleared}
|
|
||||||
floorElemDef={floorElemDef}
|
|
||||||
roomType={roomType}
|
|
||||||
roomConfig={roomConfig}
|
|
||||||
activeEquipmentSpells={activeEquipmentSpells}
|
|
||||||
floorElem={floorElem}
|
|
||||||
totalDPS={totalDPS}
|
|
||||||
calcDamage={calcDamage}
|
|
||||||
SPELLS_DEF={SPELLS_DEF}
|
|
||||||
canCastSpell={canCastSpell}
|
|
||||||
handleClimb={handleClimb}
|
|
||||||
formatSpellCost={formatSpellCost}
|
|
||||||
getSpellCostColor={getSpellCostColor}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Combat Stats Panel */}
|
|
||||||
{simpleMode && (
|
|
||||||
<CombatStatsPanel
|
|
||||||
activeEquipmentSpells={activeEquipmentSpells}
|
|
||||||
storeCurrentAction={currentAction}
|
|
||||||
totalDPS={totalDPS}
|
|
||||||
calcDamage={calcDamage}
|
|
||||||
formatSpellCost={formatSpellCost}
|
|
||||||
getSpellCostColor={getSpellCostColor}
|
|
||||||
SPELLS_DEF={SPELLS_DEF}
|
|
||||||
upgradeEffects={upgradeEffects}
|
|
||||||
canCastSpell={canCastSpell}
|
|
||||||
studySpeedMult={1}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Activity Log - Spire Mode only */}
|
|
||||||
{simpleMode && <ActivityLog activityLog={activityLog} />}
|
|
||||||
|
|
||||||
{/* Study Progress - Normal mode only */}
|
|
||||||
{!simpleMode && currentStudyTarget && (
|
|
||||||
<Card className="bg-gray-900/80 border-purple-600/50">
|
|
||||||
<CardContent className="pt-4 pb-4">
|
|
||||||
<div className="text-xs text-gray-400 mb-2">Study: {getSkillName(currentStudyTarget.id)}</div>
|
|
||||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300 bg-purple-500"
|
|
||||||
style={{ width: `${Math.min(100, (currentStudyTarget.progress / currentStudyTarget.required) * 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{parallelStudyTarget && (
|
|
||||||
<div className="mt-3 p-2 rounded border border-cyan-600/50 bg-cyan-900/20">
|
|
||||||
<div className="text-xs text-cyan-300 mb-1">Parallel: {getSkillName(parallelStudyTarget.id)} (50% speed)</div>
|
|
||||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300 bg-cyan-500"
|
|
||||||
style={{ width: `${Math.min(100, (parallelStudyTarget.progress / parallelStudyTarget.required) * 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Crafting Progress - Normal mode only */}
|
|
||||||
{!simpleMode && (designProgress || preparationProgress || applicationProgress) && (
|
|
||||||
<Card className="bg-gray-900/80 border-cyan-600/50">
|
|
||||||
<CardContent className="pt-4 pb-4">
|
|
||||||
{designProgress && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="text-xs text-gray-400 mb-1">Design Progress</div>
|
|
||||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300 bg-cyan-500"
|
|
||||||
style={{ width: `${Math.min(100, (designProgress.progress / designProgress.required) * 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{preparationProgress && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="text-xs text-gray-400 mb-1">Preparation Progress</div>
|
|
||||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300 bg-cyan-500"
|
|
||||||
style={{ width: `${Math.min(100, (preparationProgress.progress / preparationProgress.required) * 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{applicationProgress && (
|
|
||||||
<div>
|
|
||||||
<div className="text-xs text-gray-400 mb-1">Application Progress</div>
|
|
||||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full transition-all duration-300 bg-cyan-500"
|
|
||||||
style={{ width: `${Math.min(100, (applicationProgress.progress / applicationProgress.required) * 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SpireTab.displayName = "SpireTab";
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { ELEMENTS, GUARDIANS } from '@/lib/game/constants';
|
|
||||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
|
||||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
|
||||||
import { fmt, fmtDec, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/stores';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { FlaskConical, Trophy, RotateCcw } from 'lucide-react';
|
|
||||||
import { ManaStatsSection } from '../stats/ManaStatsSection';
|
|
||||||
import { ManaTypeBreakdown } from '../stats/ManaTypeBreakdown';
|
|
||||||
import { CombatStatsSection } from '../stats/CombatStatsSection';
|
|
||||||
import { StudyStatsSection } from '../stats/StudyStatsSection';
|
|
||||||
import { UpgradeEffectsSection } from '../stats/UpgradeEffectsSection';
|
|
||||||
|
|
||||||
// Modular stores
|
|
||||||
import { useCombatStore, useManaStore, useSkillStore, usePrestigeStore, useGameStore } from '@/lib/game/stores';
|
|
||||||
import { useCraftingStore } from '@/lib/game/stores/craftingStore';
|
|
||||||
import { DebugName } from '@/lib/game/debug-context';
|
|
||||||
|
|
||||||
export function StatsTab() {
|
|
||||||
// Get state from modular stores
|
|
||||||
const skills = useSkillStore((s) => s.skills);
|
|
||||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
|
||||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
|
||||||
|
|
||||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
|
||||||
const loopCount = usePrestigeStore((s) => s.loopCount);
|
|
||||||
const insight = usePrestigeStore((s) => s.insight);
|
|
||||||
const totalInsight = usePrestigeStore((s) => s.totalInsight);
|
|
||||||
const memorySlots = usePrestigeStore((s) => s.memorySlots);
|
|
||||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
|
||||||
|
|
||||||
const elements = useManaStore((s) => s.elements);
|
|
||||||
const totalManaGathered = useManaStore((s) => s.totalManaGathered);
|
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
|
||||||
const meditateTicks = useManaStore((s) => s.meditateTicks);
|
|
||||||
|
|
||||||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
|
||||||
const spells = useCombatStore((s) => s.spells);
|
|
||||||
|
|
||||||
// Get equipment state from crafting store
|
|
||||||
const equippedInstances = useCraftingStore(s => s.equippedInstances);
|
|
||||||
const equipmentInstances = useCraftingStore(s => s.equipmentInstances);
|
|
||||||
|
|
||||||
// Compute unified effects
|
|
||||||
const upgradeEffects = getUnifiedEffects({
|
|
||||||
skillUpgrades,
|
|
||||||
skillTiers,
|
|
||||||
equippedInstances,
|
|
||||||
equipmentInstances
|
|
||||||
});
|
|
||||||
|
|
||||||
// Compute derived stats
|
|
||||||
const maxMana = computeMaxMana({
|
|
||||||
skills,
|
|
||||||
prestigeUpgrades,
|
|
||||||
skillUpgrades,
|
|
||||||
skillTiers
|
|
||||||
}, upgradeEffects);
|
|
||||||
|
|
||||||
const baseRegen = computeRegen({
|
|
||||||
skills,
|
|
||||||
prestigeUpgrades,
|
|
||||||
skillUpgrades,
|
|
||||||
skillTiers
|
|
||||||
}, upgradeEffects);
|
|
||||||
|
|
||||||
const clickMana = computeClickMana({
|
|
||||||
skills,
|
|
||||||
prestigeUpgrades,
|
|
||||||
skillUpgrades,
|
|
||||||
skillTiers
|
|
||||||
});
|
|
||||||
|
|
||||||
const meditationMultiplier = getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency);
|
|
||||||
|
|
||||||
const day = useGameStore((s) => s.day);
|
|
||||||
const hour = useGameStore((s) => s.hour);
|
|
||||||
const incursionStrength = getIncursionStrength(day, hour);
|
|
||||||
|
|
||||||
// Effective regen with incursion penalty
|
|
||||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
|
||||||
|
|
||||||
// Mana Cascade bonus
|
|
||||||
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
|
||||||
? Math.floor(maxMana / 100) * 0.1
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Mana Waterfall bonus
|
|
||||||
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
|
||||||
? Math.floor(maxMana / 100) * 0.25
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// Effective regen
|
|
||||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
|
||||||
|
|
||||||
// Get study speed/cost multipliers
|
|
||||||
const studySpeedMult = getStudySpeedMultiplier(skills);
|
|
||||||
const studyCostMult = getStudyCostMultiplier(skills);
|
|
||||||
|
|
||||||
// Check special effects
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Compute element max
|
|
||||||
const elemMax = (() => {
|
|
||||||
const ea = skillTiers?.elemAttune || 1;
|
|
||||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
|
||||||
const level = skills[tieredSkillId] || skills.elemAttune || 0;
|
|
||||||
const tierMult = getTierMultiplier(tieredSkillId);
|
|
||||||
return 10 + level * 50 * tierMult + (prestigeUpgrades.elementalAttune || 0) * 25;
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DebugName name="StatsTab">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Mana Stats */}
|
|
||||||
<ManaStatsSection
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Mana Type Breakdown */}
|
|
||||||
<ManaTypeBreakdown />
|
|
||||||
|
|
||||||
{/* Combat Stats */}
|
|
||||||
<CombatStatsSection />
|
|
||||||
|
|
||||||
{/* Study Stats */}
|
|
||||||
<StudyStatsSection
|
|
||||||
studySpeedMult={studySpeedMult}
|
|
||||||
studyCostMult={studyCostMult}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 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 = skillTiers?.elemAttune || 1;
|
|
||||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
|
||||||
const level = skills[tieredSkillId] || 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">+{(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(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 + (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(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 */}
|
|
||||||
<UpgradeEffectsSection />
|
|
||||||
|
|
||||||
{/* Enchantment Power */}
|
|
||||||
<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">
|
|
||||||
✨ Enchantment Power
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-400">Enchantment Power:</span>
|
|
||||||
<span className="text-blue-300 font-[var(--font-mono)]">
|
|
||||||
{upgradeEffects?.enchantmentPowerMultiplier?.toFixed(2) || '1.0'}×
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
|
||||||
Increases the power of all enchantments by {((upgradeEffects?.enchantmentPowerMultiplier || 1) - 1) * 100}%. Multiplier applied to all enchantment effects.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Pact Bonuses */}
|
|
||||||
<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" />
|
|
||||||
Signed Pacts ({signedPacts.length}/10)
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{signedPacts.length === 0 ? (
|
|
||||||
<div className="text-gray-500 text-sm">No pacts signed yet. Defeat guardians to earn pacts.</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{signedPacts.map((floor) => {
|
|
||||||
const guardian = GUARDIANS[floor];
|
|
||||||
if (!guardian) return null;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={floor}
|
|
||||||
className="flex items-center justify-between p-2 rounded border"
|
|
||||||
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
|
||||||
{guardian.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400">Floor {floor}</div>
|
|
||||||
</div>
|
|
||||||
<Badge className="bg-amber-900/50 text-amber-300">
|
|
||||||
{guardian.pact}x multiplier
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2 mt-2">
|
|
||||||
<span className="text-gray-300">Combined Pact Multiplier:</span>
|
|
||||||
<span className="text-amber-400">×{fmtDec(signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}</span>
|
|
||||||
</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">{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(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(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">{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(spells || {}).filter((s: any) => 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(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(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">{memorySlots}</div>
|
|
||||||
<div className="text-xs text-gray-400">Memory Slots</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DebugName>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StatsTab.displayName = "StatsTab";
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
|
||||||
import { formatStudyTime } from '@/lib/game/formatting';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import type { StudyTarget } from '@/lib/game/types';
|
|
||||||
|
|
||||||
export interface StudyProgressProps {
|
|
||||||
currentStudyTarget: StudyTarget;
|
|
||||||
skills: Record<string, number>;
|
|
||||||
studySpeedMult: number;
|
|
||||||
cancelStudy: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StudyProgress({
|
|
||||||
currentStudyTarget,
|
|
||||||
skills,
|
|
||||||
studySpeedMult,
|
|
||||||
cancelStudy
|
|
||||||
}: StudyProgressProps) {
|
|
||||||
const { id, progress, required } = currentStudyTarget;
|
|
||||||
|
|
||||||
// Get skill name
|
|
||||||
const baseId = id.includes('_t') ? id.split('_t')[0] : id;
|
|
||||||
const skillDef = SKILLS_DEF[baseId];
|
|
||||||
const skillName = skillDef?.name || id;
|
|
||||||
|
|
||||||
// Get current level
|
|
||||||
const currentLevel = skills[id] || skills[baseId] || 0;
|
|
||||||
|
|
||||||
// Calculate progress percentage
|
|
||||||
const progressPercent = Math.min((progress / required) * 100, 100);
|
|
||||||
|
|
||||||
// Estimated completion
|
|
||||||
const remainingHours = required - progress;
|
|
||||||
const effectiveSpeed = studySpeedMult;
|
|
||||||
const realTimeRemaining = remainingHours / effectiveSpeed;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<span className="text-purple-300 font-semibold">{skillName}</span>
|
|
||||||
<span className="text-gray-400 ml-2">
|
|
||||||
Level {currentLevel} → {currentLevel + 1}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
onClick={cancelStudy}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex justify-between text-xs text-gray-400">
|
|
||||||
<span>{formatStudyTime(progress)} / {formatStudyTime(required)}</span>
|
|
||||||
<span>{progressPercent.toFixed(1)}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={progressPercent} className="h-2" />
|
|
||||||
{studySpeedMult > 1 && (
|
|
||||||
<div className="text-xs text-green-400">
|
|
||||||
Speed: {(studySpeedMult * 100).toFixed(0)}% • ETA: {formatStudyTime(realTimeRemaining)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StudyProgress.displayName = "StudyProgress";
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
|
||||||
|
|
||||||
export interface UpgradeDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
skillId: string | null;
|
|
||||||
milestone: 5 | 10;
|
|
||||||
pendingSelections: string[];
|
|
||||||
available: SkillUpgradeChoice[];
|
|
||||||
alreadySelected: string[];
|
|
||||||
onToggle: (upgradeId: string) => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UpgradeDialog({
|
|
||||||
open,
|
|
||||||
skillId,
|
|
||||||
milestone,
|
|
||||||
pendingSelections,
|
|
||||||
available,
|
|
||||||
alreadySelected,
|
|
||||||
onToggle,
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
onOpenChange,
|
|
||||||
}: UpgradeDialogProps) {
|
|
||||||
if (!skillId) return null;
|
|
||||||
|
|
||||||
// Get skill name
|
|
||||||
const baseId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
|
||||||
const skillDef = SKILLS_DEF[baseId];
|
|
||||||
const skillName = skillDef?.name || skillId;
|
|
||||||
|
|
||||||
const currentSelections = pendingSelections.length > 0 ? pendingSelections : alreadySelected;
|
|
||||||
const canConfirm = currentSelections.length === 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="sm:max-w-md bg-gray-900 border-purple-600/50">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-amber-400">
|
|
||||||
Level {milestone} Milestone: {skillName}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription className="text-gray-400">
|
|
||||||
Choose 2 upgrades for this skill. These choices are permanent.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-2 py-4">
|
|
||||||
{available.map((upgrade) => {
|
|
||||||
const isSelected = currentSelections.includes(upgrade.id);
|
|
||||||
const canSelect = isSelected || currentSelections.length < 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={upgrade.id}
|
|
||||||
onClick={() => canSelect && onToggle(upgrade.id)}
|
|
||||||
className={`p-3 rounded border cursor-pointer transition-all ${
|
|
||||||
isSelected
|
|
||||||
? 'border-amber-500 bg-amber-900/30'
|
|
||||||
: canSelect
|
|
||||||
? 'border-gray-700 hover:border-gray-500 bg-gray-800/30'
|
|
||||||
: 'border-gray-800 bg-gray-900/30 opacity-50 cursor-not-allowed'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className={`font-semibold text-sm ${isSelected ? 'text-amber-300' : 'text-gray-200'}`}>
|
|
||||||
{upgrade.name}
|
|
||||||
</span>
|
|
||||||
{isSelected && (
|
|
||||||
<Badge className="bg-amber-600/50 text-amber-200 text-xs">Selected</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-400 mt-1">{upgrade.desc}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{available.length === 0 && (
|
|
||||||
<div className="text-center text-gray-500 py-4">
|
|
||||||
No upgrades available at this milestone.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className="flex gap-2 sm:gap-0">
|
|
||||||
<Button variant="outline" onClick={onCancel}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="bg-amber-600 hover:bg-amber-700"
|
|
||||||
disabled={!canConfirm}
|
|
||||||
onClick={onConfirm}
|
|
||||||
>
|
|
||||||
Confirm ({currentSelections.length}/2)
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpgradeDialog.displayName = "UpgradeDialog";
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// ─── Tab Components Index ──────────────────────────────────────────────────────
|
|
||||||
// Re-exports all tab components for cleaner imports
|
|
||||||
|
|
||||||
export { CraftingTab } from './CraftingTab';
|
|
||||||
export { SpireTab } from './SpireTab';
|
|
||||||
export { SpellsTab } from './SpellsTab';
|
|
||||||
// SkillsTab is now exported from src/components/game/index.ts
|
|
||||||
export { SkillsTab } from '../SkillsTab';
|
|
||||||
export { StatsTab } from './StatsTab';
|
|
||||||
export { EquipmentTab } from './EquipmentTab';
|
|
||||||
export { AttunementsTab } from './AttunementsTab';
|
|
||||||
export { DebugTab } from './DebugTab';
|
|
||||||
export { LootTab } from './LootTab';
|
|
||||||
export { AchievementsTab } from './AchievementsTab';
|
|
||||||
export { GolemancyTab } from './GolemancyTab';
|
|
||||||
|
|
||||||
// Spire sub-components
|
|
||||||
export { SpireHeader } from './SpireHeader';
|
|
||||||
export { GuardianPanel } from './GuardianPanel';
|
|
||||||
export { RoomDisplay } from './RoomDisplay';
|
|
||||||
export { FloorControls } from './FloorControls';
|
|
||||||
export { CombatStatsPanel } from './CombatStatsPanel';
|
|
||||||
export { ActivityLog } from './ActivityLog';
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { ActionButton } from "./action-button";
|
|
||||||
import { TooltipInfo } from "./tooltip-info";
|
|
||||||
import { Progress } from "./progress";
|
|
||||||
import { Lock } from "lucide-react";
|
|
||||||
|
|
||||||
interface SkillRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
level: number;
|
|
||||||
maxLevel: number;
|
|
||||||
manaType?: string; // For coloring level dots
|
|
||||||
tier?: number;
|
|
||||||
studying?: boolean;
|
|
||||||
maxed?: boolean;
|
|
||||||
canTierUp?: boolean;
|
|
||||||
hasMilestone?: boolean;
|
|
||||||
milestoneLevel?: 5 | 10;
|
|
||||||
onStudy?: () => void;
|
|
||||||
onUpgrade?: () => void;
|
|
||||||
onTierUp?: () => void;
|
|
||||||
onMilestoneClick?: () => void;
|
|
||||||
cost?: number | string | React.ReactNode;
|
|
||||||
time?: string;
|
|
||||||
prereqMet?: boolean;
|
|
||||||
prereqText?: string;
|
|
||||||
showParallelStudy?: boolean;
|
|
||||||
onParallelStudy?: () => void;
|
|
||||||
selectedL5?: number;
|
|
||||||
selectedL10?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SkillRow({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
level,
|
|
||||||
maxLevel,
|
|
||||||
manaType = 'light',
|
|
||||||
tier,
|
|
||||||
studying = false,
|
|
||||||
maxed = false,
|
|
||||||
canTierUp = false,
|
|
||||||
hasMilestone = false,
|
|
||||||
milestoneLevel,
|
|
||||||
onStudy,
|
|
||||||
onUpgrade,
|
|
||||||
onTierUp,
|
|
||||||
onMilestoneClick,
|
|
||||||
cost,
|
|
||||||
time,
|
|
||||||
prereqMet = true,
|
|
||||||
prereqText,
|
|
||||||
showParallelStudy = false,
|
|
||||||
onParallelStudy,
|
|
||||||
selectedL5 = 0,
|
|
||||||
selectedL10 = 0,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: SkillRowProps) {
|
|
||||||
const manaColor = `var(--mana-${manaType})`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="skill-row"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col sm:flex-row sm:items-start gap-4 p-4 rounded-[var(--radius)] border transition-all duration-100",
|
|
||||||
"sm:flex-wrap",
|
|
||||||
studying && "border-[var(--mana-light)]/50 bg-[var(--mana-light)]/5",
|
|
||||||
hasMilestone && "border-amber-500/50 bg-amber-900/10 relative",
|
|
||||||
canTierUp && "border-amber-600/30",
|
|
||||||
!studying && !hasMilestone && !canTierUp && "border-[var(--border-subtle)] bg-[var(--bg-surface)] hover:border-[var(--border-default)]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="flex-1 min-w-0 space-y-2">
|
|
||||||
{/* Skill Header: Name + Tier Badge + Milestone Indicator + Prereq Lock */}
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<h4 className="font-[var(--font-heading)] text-[var(--text-primary)] text-sm">
|
|
||||||
{name}
|
|
||||||
</h4>
|
|
||||||
{tier !== undefined && tier > 1 && (
|
|
||||||
<span
|
|
||||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium border"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${manaColor}20`,
|
|
||||||
borderColor: `${manaColor}60`,
|
|
||||||
color: manaColor
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
T{tier}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{hasMilestone && onMilestoneClick && (
|
|
||||||
<button
|
|
||||||
onClick={onMilestoneClick}
|
|
||||||
className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-amber-500/20 text-amber-400 border border-amber-500/30 text-xs hover:bg-amber-500/30 transition-colors"
|
|
||||||
title="Milestone reached! Choose upgrades"
|
|
||||||
>
|
|
||||||
!
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!prereqMet && prereqText && (
|
|
||||||
<TooltipInfo content={prereqText}>
|
|
||||||
<Lock size={14} className="text-red-400" />
|
|
||||||
</TooltipInfo>
|
|
||||||
)}
|
|
||||||
{(selectedL5 > 0 || selectedL10 > 0) && (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{selectedL5 > 0 && (
|
|
||||||
<span className="text-[10px] px-1 py-0.5 rounded bg-amber-700/50 text-amber-200 border border-amber-600/30">
|
|
||||||
L5: {selectedL5}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{selectedL10 > 0 && (
|
|
||||||
<span className="text-[10px] px-1 py-0.5 rounded bg-purple-700/50 text-purple-200 border border-purple-600/30">
|
|
||||||
L10: {selectedL10}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-xs text-[var(--text-secondary)]">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Level Dots - colored by mana type */}
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{Array.from({ length: maxLevel }).map((_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="w-2 h-2 rounded-full transition-all duration-100 sm:w-2 sm:h-2"
|
|
||||||
style={i < level ? { backgroundColor: manaColor } : { backgroundColor: 'var(--text-disabled)' }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<span className="text-xs text-[var(--text-muted)] ml-2">
|
|
||||||
{level}/{maxLevel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cost and Time */}
|
|
||||||
{(cost !== undefined || time) && (
|
|
||||||
<div className="flex items-center gap-3 text-xs text-[var(--text-muted)] font-[var(--font-mono)] tabular-nums flex-wrap">
|
|
||||||
{time && <span>Time: {time}</span>}
|
|
||||||
{cost !== undefined && <span>Cost: {cost}</span>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Study Progress */}
|
|
||||||
{studying && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<Progress value={33} className="h-1.5" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 w-full sm:w-auto sm:ml-auto">
|
|
||||||
{studying ? (
|
|
||||||
<span className="text-xs text-[var(--mana-light)] font-[var(--font-mono)]">
|
|
||||||
Studying...
|
|
||||||
</span>
|
|
||||||
) : hasMilestone && onMilestoneClick ? (
|
|
||||||
<ActionButton
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={onMilestoneClick}
|
|
||||||
className="bg-amber-600 hover:bg-amber-700 w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
Choose Upgrades
|
|
||||||
</ActionButton>
|
|
||||||
) : canTierUp && onTierUp ? (
|
|
||||||
<ActionButton
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={onTierUp}
|
|
||||||
className="border-amber-500 text-amber-400 hover:bg-amber-900/30 w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
⬆️ Tier Up
|
|
||||||
</ActionButton>
|
|
||||||
) : maxed ? (
|
|
||||||
<span className="text-xs text-green-400 bg-green-900/50 px-2 py-1 rounded">
|
|
||||||
Maxed
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-1 w-full sm:w-auto">
|
|
||||||
{onStudy && (
|
|
||||||
<ActionButton
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={onStudy}
|
|
||||||
disabled={!prereqMet}
|
|
||||||
className="flex-1 sm:flex-none"
|
|
||||||
>
|
|
||||||
Study
|
|
||||||
{cost !== undefined && ` (${cost})`}
|
|
||||||
</ActionButton>
|
|
||||||
)}
|
|
||||||
{/* Parallel Study button */}
|
|
||||||
{showParallelStudy && onParallelStudy && (
|
|
||||||
<TooltipInfo content="Study in parallel (50% speed)">
|
|
||||||
<ActionButton
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={onParallelStudy}
|
|
||||||
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
|
||||||
>
|
|
||||||
⚡
|
|
||||||
</ActionButton>
|
|
||||||
</TooltipInfo>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
SKILL_EVOLUTION_PATHS,
|
|
||||||
getBaseSkillId,
|
|
||||||
generateTierSkillDef,
|
|
||||||
getUpgradesForSkillAtMilestone,
|
|
||||||
getNextTierSkill,
|
|
||||||
getTierMultiplier,
|
|
||||||
canTierUp,
|
|
||||||
getAvailableUpgrades,
|
|
||||||
} from '../skill-evolution';
|
|
||||||
import { SKILLS_DEF } from '../constants';
|
|
||||||
|
|
||||||
describe('Skill Evolution Paths', () => {
|
|
||||||
it('should have evolution paths for all skills with max > 1', () => {
|
|
||||||
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('should have at least one tier for each evolution path', () => {
|
|
||||||
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
||||||
expect(path.tiers.length, `${skillId} should have at least one tier`).toBeGreaterThanOrEqual(1);
|
|
||||||
expect(path.baseSkillId, `${skillId} baseSkillId should match`).toBe(skillId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have correct tier multipliers (10x per tier)', () => {
|
|
||||||
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
||||||
for (const tier of path.tiers) {
|
|
||||||
const expectedMultiplier = Math.pow(10, tier.tier - 1);
|
|
||||||
expect(tier.multiplier, `${skillId} tier ${tier.tier} multiplier`).toBe(expectedMultiplier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have valid skill IDs for each tier', () => {
|
|
||||||
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
||||||
for (const tier of path.tiers) {
|
|
||||||
if (tier.tier === 1) {
|
|
||||||
expect(tier.skillId, `${skillId} tier 1 skillId`).toBe(skillId);
|
|
||||||
} else {
|
|
||||||
expect(tier.skillId, `${skillId} tier ${tier.tier} skillId`).toContain('_t');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getBaseSkillId', () => {
|
|
||||||
it('should return the same ID for base skills', () => {
|
|
||||||
expect(getBaseSkillId('manaWell')).toBe('manaWell');
|
|
||||||
expect(getBaseSkillId('manaFlow')).toBe('manaFlow');
|
|
||||||
expect(getBaseSkillId('enchanting')).toBe('enchanting');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extract base ID from tiered skills', () => {
|
|
||||||
expect(getBaseSkillId('manaWell_t2')).toBe('manaWell');
|
|
||||||
expect(getBaseSkillId('manaFlow_t3')).toBe('manaFlow');
|
|
||||||
expect(getBaseSkillId('enchanting_t5')).toBe('enchanting');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('generateTierSkillDef', () => {
|
|
||||||
it('should return null for non-existent skills', () => {
|
|
||||||
expect(generateTierSkillDef('nonexistent', 1)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null for non-existent tiers', () => {
|
|
||||||
// Most skills don't have tier 10
|
|
||||||
expect(generateTierSkillDef('manaWell', 10)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return correct tier definition', () => {
|
|
||||||
const tier1 = generateTierSkillDef('manaWell', 1);
|
|
||||||
expect(tier1).not.toBeNull();
|
|
||||||
expect(tier1?.name).toBe('Mana Well');
|
|
||||||
expect(tier1?.tier).toBe(1);
|
|
||||||
expect(tier1?.multiplier).toBe(1);
|
|
||||||
|
|
||||||
const tier2 = generateTierSkillDef('manaWell', 2);
|
|
||||||
expect(tier2).not.toBeNull();
|
|
||||||
expect(tier2?.name).toBe('Deep Reservoir');
|
|
||||||
expect(tier2?.tier).toBe(2);
|
|
||||||
expect(tier2?.multiplier).toBe(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getUpgradesForSkillAtMilestone', () => {
|
|
||||||
it('should return empty array for non-existent skills', () => {
|
|
||||||
const upgrades = getUpgradesForSkillAtMilestone('nonexistent', 5, {});
|
|
||||||
expect(upgrades).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return upgrades for manaWell at milestone 5', () => {
|
|
||||||
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 1 });
|
|
||||||
expect(upgrades.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// All should be milestone 5
|
|
||||||
for (const upgrade of upgrades) {
|
|
||||||
expect(upgrade.milestone).toBe(5);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return upgrades for manaWell at milestone 10', () => {
|
|
||||||
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, { manaWell: 1 });
|
|
||||||
expect(upgrades.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// All should be milestone 10
|
|
||||||
for (const upgrade of upgrades) {
|
|
||||||
expect(upgrade.milestone).toBe(10);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return tier 2 upgrades when at tier 2', () => {
|
|
||||||
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 2 });
|
|
||||||
expect(upgrades.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Should have tier 2 specific upgrades
|
|
||||||
const upgradeIds = upgrades.map(u => u.id);
|
|
||||||
expect(upgradeIds.some(id => id.startsWith('mw_t2'))).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getNextTierSkill', () => {
|
|
||||||
it('should return null for non-existent skills', () => {
|
|
||||||
expect(getNextTierSkill('nonexistent')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return next tier for tier 1 skills', () => {
|
|
||||||
expect(getNextTierSkill('manaWell')).toBe('manaWell_t2');
|
|
||||||
expect(getNextTierSkill('manaFlow')).toBe('manaFlow_t2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return next tier for higher tier skills', () => {
|
|
||||||
expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3');
|
|
||||||
expect(getNextTierSkill('manaWell_t3')).toBe('manaWell_t4');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null for max tier skills', () => {
|
|
||||||
// manaWell has 5 tiers
|
|
||||||
expect(getNextTierSkill('manaWell_t5')).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTierMultiplier', () => {
|
|
||||||
it('should return 1 for tier 1 skills', () => {
|
|
||||||
expect(getTierMultiplier('manaWell')).toBe(1);
|
|
||||||
expect(getTierMultiplier('manaFlow')).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return 10 for tier 2 skills', () => {
|
|
||||||
expect(getTierMultiplier('manaWell_t2')).toBe(10);
|
|
||||||
expect(getTierMultiplier('manaFlow_t2')).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return correct multiplier for higher tiers', () => {
|
|
||||||
expect(getTierMultiplier('manaWell_t3')).toBe(100);
|
|
||||||
expect(getTierMultiplier('manaWell_t4')).toBe(1000);
|
|
||||||
expect(getTierMultiplier('manaWell_t5')).toBe(10000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('canTierUp', () => {
|
|
||||||
const mockAttunements = {
|
|
||||||
enchanter: { level: 10, active: true },
|
|
||||||
fabricator: { level: 10, active: true },
|
|
||||||
invoker: { level: 10, active: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should return false for non-existent skills', () => {
|
|
||||||
const result = canTierUp('nonexistent', 10, {}, mockAttunements);
|
|
||||||
expect(result.canTierUp).toBe(false);
|
|
||||||
expect(result.reason).toBe('No evolution path');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if not at max level', () => {
|
|
||||||
const result = canTierUp('manaWell', 5, { manaWell: 1 }, mockAttunements);
|
|
||||||
expect(result.canTierUp).toBe(false);
|
|
||||||
expect(result.reason).toBe('Need level 10 to tier up');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true when at max level with attunement', () => {
|
|
||||||
const result = canTierUp('manaWell', 10, { manaWell: 1 }, mockAttunements);
|
|
||||||
expect(result.canTierUp).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if already at max tier', () => {
|
|
||||||
const result = canTierUp('manaWell_t5', 10, { manaWell: 5 }, mockAttunements);
|
|
||||||
expect(result.canTierUp).toBe(false);
|
|
||||||
expect(result.reason).toBe('Already at max tier');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAvailableUpgrades', () => {
|
|
||||||
const manaWellTier1Tree = SKILL_EVOLUTION_PATHS.manaWell.tiers[0].upgrades;
|
|
||||||
|
|
||||||
it('should return only upgrades for specified milestone', () => {
|
|
||||||
const available = getAvailableUpgrades(
|
|
||||||
manaWellTier1Tree as any[],
|
|
||||||
[],
|
|
||||||
5,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const upgrade of available) {
|
|
||||||
expect(upgrade.milestone).toBe(5);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should exclude already chosen upgrades', () => {
|
|
||||||
const available = getAvailableUpgrades(
|
|
||||||
manaWellTier1Tree as any[],
|
|
||||||
['mw_t1_l5_capacity'],
|
|
||||||
5,
|
|
||||||
['mw_t1_l5_capacity']
|
|
||||||
);
|
|
||||||
|
|
||||||
const ids = available.map(u => u.id);
|
|
||||||
expect(ids).not.toContain('mw_t1_l5_capacity');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Skill Definitions', () => {
|
|
||||||
it('should have valid max levels for all skills', () => {
|
|
||||||
for (const [skillId, def] of Object.entries(SKILLS_DEF)) {
|
|
||||||
expect(def.max, `${skillId} max level`).toBeGreaterThanOrEqual(1);
|
|
||||||
expect(def.max, `${skillId} max level`).toBeLessThanOrEqual(10);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have study time defined for all skills', () => {
|
|
||||||
for (const [skillId, def] of Object.entries(SKILLS_DEF)) {
|
|
||||||
expect(def.studyTime, `${skillId} study time`).toBeGreaterThanOrEqual(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have base cost defined for all skills', () => {
|
|
||||||
for (const [skillId, def] of Object.entries(SKILLS_DEF)) {
|
|
||||||
expect(def.base, `${skillId} base cost`).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have correct categories for skills', () => {
|
|
||||||
const validCategories = ['mana', 'study', 'research', 'ascension', 'enchant',
|
|
||||||
'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'craft', 'hybrid'];
|
|
||||||
|
|
||||||
for (const [skillId, def] of Object.entries(SKILLS_DEF)) {
|
|
||||||
expect(validCategories, `${skillId} category`).toContain(def.cat);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Upgrade Tree Structure', () => {
|
|
||||||
it('should have valid effect types for all upgrades', () => {
|
|
||||||
const validTypes = ['multiplier', 'bonus', 'special'];
|
|
||||||
|
|
||||||
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
||||||
for (const tier of path.tiers) {
|
|
||||||
for (const upgrade of tier.upgrades) {
|
|
||||||
expect(validTypes, `${upgrade.id} effect type`).toContain(upgrade.effect.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have milestone 5 or 10 for all upgrades', () => {
|
|
||||||
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
||||||
for (const tier of path.tiers) {
|
|
||||||
for (const upgrade of tier.upgrades) {
|
|
||||||
expect([5, 10], `${upgrade.id} milestone`).toContain(upgrade.milestone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have unique upgrade IDs within each skill', () => {
|
|
||||||
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
||||||
const allIds: string[] = [];
|
|
||||||
for (const tier of path.tiers) {
|
|
||||||
for (const upgrade of tier.upgrades) {
|
|
||||||
expect(allIds, `${upgrade.id} should be unique in ${skillId}`).not.toContain(upgrade.id);
|
|
||||||
allIds.push(upgrade.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have descriptions for all upgrades', () => {
|
|
||||||
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
||||||
for (const tier of path.tiers) {
|
|
||||||
for (const upgrade of tier.upgrades) {
|
|
||||||
expect(upgrade.desc, `${upgrade.id} should have description`).toBeTruthy();
|
|
||||||
expect(upgrade.desc.length, `${upgrade.id} description length`).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have special descriptions for special effects', () => {
|
|
||||||
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
||||||
for (const tier of path.tiers) {
|
|
||||||
for (const upgrade of tier.upgrades) {
|
|
||||||
if (upgrade.effect.type === 'special') {
|
|
||||||
expect(upgrade.effect.specialDesc, `${upgrade.id} should have special description`).toBeTruthy();
|
|
||||||
expect(upgrade.effect.specialId, `${upgrade.id} should have special ID`).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Skill Level and Tier Calculations', () => {
|
|
||||||
it('should calculate tier 2 level 1 as equivalent to tier 1 level 10', () => {
|
|
||||||
// Base skill gives +100 max mana per level
|
|
||||||
// At tier 1 level 10: 10 * 100 = 1000 max mana
|
|
||||||
// At tier 2 level 1 with 10x multiplier: 1 * 100 * 10 = 1000 max mana
|
|
||||||
const tier1Level10Effect = 10 * 100; // level * base
|
|
||||||
const tier2Level1Effect = 1 * 100 * 10; // level * base * tierMultiplier
|
|
||||||
|
|
||||||
expect(tier2Level1Effect).toBe(tier1Level10Effect);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have increasing power with tiers', () => {
|
|
||||||
// Tier 3 level 1 should be stronger than tier 2 level 10
|
|
||||||
const tier2Level10 = 10 * 100 * 10; // level * base * tierMultiplier
|
|
||||||
const tier3Level1 = 1 * 100 * 100; // level * base * tierMultiplier
|
|
||||||
|
|
||||||
expect(tier3Level1).toBe(tier2Level10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have correct max tier for skills', () => {
|
|
||||||
// Skills with max 5 should typically have 2-3 tiers
|
|
||||||
// Skills with max 10 should typically have 3-5 tiers
|
|
||||||
|
|
||||||
const manaWellPath = SKILL_EVOLUTION_PATHS.manaWell;
|
|
||||||
expect(manaWellPath.tiers.length).toBe(5); // manaWell has 5 tiers
|
|
||||||
|
|
||||||
const manaFlowPath = SKILL_EVOLUTION_PATHS.manaFlow;
|
|
||||||
expect(manaFlowPath.tiers.length).toBe(5); // manaFlow has 5 tiers
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/**
|
|
||||||
* Ascension Skills Tests
|
|
||||||
*
|
|
||||||
* Tests for ascension-related skills: Insight Harvest, Guardian Bane
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
|
||||||
import { calcInsight } from '@/lib/game/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.');
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/**
|
|
||||||
* Skill Integration Tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SKILLS_DEF, SKILL_EVOLUTION_PATHS, getTierMultiplier, getNextTierSkill, generateTierSkillDef } from '@/lib/game/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.');
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 '@/lib/game/computed-stats';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/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.');
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
/**
|
|
||||||
* Prestige Upgrade Tests for Skills
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { PRESTIGE_DEF } from '@/lib/game/constants';
|
|
||||||
import { computeMaxMana, computeElementMax } from '@/lib/game/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.');
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* Skill Prerequisites Tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/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.');
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/**
|
|
||||||
* Specialized Skills Tests
|
|
||||||
*
|
|
||||||
* Tests for Enchanter and Golemancy skills
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/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.');
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 '@/lib/game/computed-stats';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/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.');
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Study Times Tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/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,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Skills Tests - Main Index
|
|
||||||
*
|
|
||||||
* This file re-exports all individual skill test files.
|
|
||||||
* 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 './skills-tests/mana-skills.test';
|
|
||||||
import './skills-tests/study-skills.test';
|
|
||||||
import './skills-tests/ascension-skills.test';
|
|
||||||
import './skills-tests/specialized-skills.test';
|
|
||||||
import './skills-tests/skill-prerequisites.test';
|
|
||||||
import './skills-tests/study-times.test';
|
|
||||||
import './skills-tests/prestige-upgrades.test';
|
|
||||||
import './skills-tests/integration-and-evolution.test';
|
|
||||||
|
|
||||||
console.log('✅ All skills tests complete (refactored from 589 lines to 8 focused test files).');
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// COMBAT SKILLS
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const arcaneFury: SkillV2Def = {
|
|
||||||
id: 'arcaneFury', name: 'Arcane Fury', description: 'Increases spell damage per level',
|
|
||||||
category: 'combat', maxLevel: 10, costPerLevel: 200, studyHours: 4,
|
|
||||||
effects: [{ stat: 'damageMultiplier', mode: 'multiply', valuePerLevel: 0.10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const combatTraining: SkillV2Def = {
|
|
||||||
id: 'combatTraining', name: 'Combat Training', description: 'Base damage bonus per level',
|
|
||||||
category: 'combat', maxLevel: 10, costPerLevel: 250, studyHours: 5,
|
|
||||||
effects: [{ stat: 'baseDamage', mode: 'add', valuePerLevel: 5 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const precision: SkillV2Def = {
|
|
||||||
id: 'precision', name: 'Precision', description: '+5% crit chance per level',
|
|
||||||
category: 'combat', maxLevel: 10, costPerLevel: 300, studyHours: 6,
|
|
||||||
effects: [{ stat: 'critChance', mode: 'add', valuePerLevel: 0.05 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const elementalMastery: SkillV2Def = {
|
|
||||||
id: 'elementalMastery', name: 'Elemental Mastery', description: '+15% elemental damage per level',
|
|
||||||
category: 'combat', maxLevel: 10, costPerLevel: 350, studyHours: 6,
|
|
||||||
effects: [{ stat: 'elementalDamage', mode: 'multiply', valuePerLevel: 0.15 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const attackSpeed: SkillV2Def = {
|
|
||||||
id: 'attackSpeed', name: 'Attack Speed', description: '-10% attack time per level (faster)',
|
|
||||||
category: 'combat', maxLevel: 5, costPerLevel: 400, studyHours: 6,
|
|
||||||
effects: [{ stat: 'attackSpeed', mode: 'multiply', valuePerLevel: -0.10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const armorPiercing: SkillV2Def = {
|
|
||||||
id: 'armorPiercing', name: 'Armor Piercing', description: '+5% armor penetration per level',
|
|
||||||
category: 'combat', maxLevel: 5, costPerLevel: 400, studyHours: 6,
|
|
||||||
effects: [{ stat: 'armorPierce', mode: 'add', valuePerLevel: 0.05 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const spellDamage: SkillV2Def = {
|
|
||||||
id: 'spellDamage', name: 'Spell Damage', description: '+5% spell damage per level',
|
|
||||||
category: 'combat', maxLevel: 10, costPerLevel: 250, studyHours: 5,
|
|
||||||
effects: [{ stat: 'spellDamage', mode: 'multiply', valuePerLevel: 0.05 }],
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// CORE MANA SKILLS
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const manaWell: SkillV2Def = {
|
|
||||||
id: 'manaWell', name: 'Mana Well', description: 'Increases maximum mana capacity',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 100, studyHours: 4,
|
|
||||||
effects: [{ stat: 'maxMana', mode: 'add', valuePerLevel: 100 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const manaFlow: SkillV2Def = {
|
|
||||||
id: 'manaFlow', name: 'Mana Flow', description: 'Increases mana regeneration rate',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 150, studyHours: 5,
|
|
||||||
effects: [{ stat: 'manaRegen', mode: 'add', valuePerLevel: 1 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const manaOverflow: SkillV2Def = {
|
|
||||||
id: 'manaOverflow', name: 'Mana Overflow', description: 'Increases mana gained from clicks',
|
|
||||||
category: 'mana', maxLevel: 5, costPerLevel: 400, studyHours: 6,
|
|
||||||
prerequisites: { manaWell: 3 },
|
|
||||||
effects: [{ stat: 'clickMana', mode: 'multiply', valuePerLevel: 0.25 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const manaTap: SkillV2Def = {
|
|
||||||
id: 'manaTap', name: 'Mana Tap', description: '+1 mana per click',
|
|
||||||
category: 'mana', maxLevel: 1, costPerLevel: 300, studyHours: 12,
|
|
||||||
effects: [{ stat: 'clickMana', mode: 'add', valuePerLevel: 1 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const manaSurge: SkillV2Def = {
|
|
||||||
id: 'manaSurge', name: 'Mana Surge', description: '+3 mana per click',
|
|
||||||
category: 'mana', maxLevel: 1, costPerLevel: 800, studyHours: 36,
|
|
||||||
prerequisites: { manaTap: 1 },
|
|
||||||
effects: [{ stat: 'clickMana', mode: 'add', valuePerLevel: 3 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const manaSpring: SkillV2Def = {
|
|
||||||
id: 'manaSpring', name: 'Mana Spring', description: '+2 mana regen',
|
|
||||||
category: 'mana', maxLevel: 1, costPerLevel: 600, studyHours: 24,
|
|
||||||
effects: [{ stat: 'manaRegen', mode: 'add', valuePerLevel: 2 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// STUDY SKILLS
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const quickLearner: SkillV2Def = {
|
|
||||||
id: 'quickLearner', name: 'Quick Learner', description: 'Increases study speed',
|
|
||||||
category: 'study', maxLevel: 10, costPerLevel: 250, studyHours: 4,
|
|
||||||
effects: [{ stat: 'studySpeed', mode: 'multiply', valuePerLevel: 0.10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const focusedMind: SkillV2Def = {
|
|
||||||
id: 'focusedMind', name: 'Focused Mind', description: 'Reduces study mana cost',
|
|
||||||
category: 'study', maxLevel: 10, costPerLevel: 300, studyHours: 5,
|
|
||||||
effects: [{ stat: 'studyCostMult', mode: 'multiply', valuePerLevel: -0.05 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const knowledgeRetention: SkillV2Def = {
|
|
||||||
id: 'knowledgeRetention', name: 'Knowledge Retention', description: 'Preserves study progress on cancellation',
|
|
||||||
category: 'study', maxLevel: 3, costPerLevel: 350, studyHours: 5,
|
|
||||||
effects: [{ stat: 'progressRetention', mode: 'add', valuePerLevel: 0.20 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// MEDITATION SKILLS
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const meditation: SkillV2Def = {
|
|
||||||
id: 'meditation', name: 'Meditation Focus', description: 'Unlocks meditation regen boost (2.5x at 4hrs)',
|
|
||||||
category: 'mana', maxLevel: 1, costPerLevel: 400, studyHours: 6,
|
|
||||||
effects: [{ stat: 'meditationEfficiency', mode: 'multiply', valuePerLevel: 1.5 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deepTrance: SkillV2Def = {
|
|
||||||
id: 'deepTrance', name: 'Deep Trance', description: 'Extends meditation to 6hrs for 3x',
|
|
||||||
category: 'mana', maxLevel: 1, costPerLevel: 900, studyHours: 48,
|
|
||||||
prerequisites: { meditation: 1 },
|
|
||||||
effects: [{ stat: 'meditationEfficiency', mode: 'multiply', valuePerLevel: 1.8 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const voidMeditation: SkillV2Def = {
|
|
||||||
id: 'voidMeditation', name: 'Void Meditation', description: 'Extends meditation to 8hrs for 5x',
|
|
||||||
category: 'mana', maxLevel: 1, costPerLevel: 1500, studyHours: 72,
|
|
||||||
prerequisites: { deepTrance: 1 },
|
|
||||||
effects: [{ stat: 'meditationEfficiency', mode: 'multiply', valuePerLevel: 2.5 }],
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// CRAFTING SKILLS (Legacy — kept for store compat)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const effCrafting: SkillV2Def = {
|
|
||||||
id: 'effCrafting', name: 'Eff. Crafting', description: '-10% craft time',
|
|
||||||
category: 'craft', maxLevel: 1, costPerLevel: 300, studyHours: 4,
|
|
||||||
effects: [{ stat: 'craftSpeed', mode: 'multiply', valuePerLevel: -0.10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fieldRepair: SkillV2Def = {
|
|
||||||
id: 'fieldRepair', name: 'Field Repair', description: '+20% repair efficiency',
|
|
||||||
category: 'craft', maxLevel: 1, costPerLevel: 350, studyHours: 4,
|
|
||||||
effects: [{ stat: 'repairSpeed', mode: 'multiply', valuePerLevel: -0.10 }],
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// PER-ELEMENT CAPACITY SKILLS
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const fireManaCap: SkillV2Def = {
|
|
||||||
id: 'fireManaCap', name: 'Fire Mana Capacity', description: '+10% fire capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 200, studyHours: 4,
|
|
||||||
effects: [{ stat: 'fireCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const waterManaCap: SkillV2Def = {
|
|
||||||
id: 'waterManaCap', name: 'Water Mana Capacity', description: '+10% water capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 200, studyHours: 4,
|
|
||||||
effects: [{ stat: 'waterCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const airManaCap: SkillV2Def = {
|
|
||||||
id: 'airManaCap', name: 'Air Mana Capacity', description: '+10% air capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 200, studyHours: 4,
|
|
||||||
effects: [{ stat: 'airCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const earthManaCap: SkillV2Def = {
|
|
||||||
id: 'earthManaCap', name: 'Earth Mana Capacity', description: '+10% earth capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 200, studyHours: 4,
|
|
||||||
effects: [{ stat: 'earthCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const lightManaCap: SkillV2Def = {
|
|
||||||
id: 'lightManaCap', name: 'Light Mana Capacity', description: '+10% light capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 250, studyHours: 5,
|
|
||||||
effects: [{ stat: 'lightCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const darkManaCap: SkillV2Def = {
|
|
||||||
id: 'darkManaCap', name: 'Dark Mana Capacity', description: '+10% dark capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 250, studyHours: 5,
|
|
||||||
effects: [{ stat: 'darkCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deathManaCap: SkillV2Def = {
|
|
||||||
id: 'deathManaCap', name: 'Death Mana Capacity', description: '+10% death capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 300, studyHours: 6,
|
|
||||||
effects: [{ stat: 'deathCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const metalManaCap: SkillV2Def = {
|
|
||||||
id: 'metalManaCap', name: 'Metal Mana Capacity', description: '+10% metal capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 350, studyHours: 6,
|
|
||||||
effects: [{ stat: 'metalCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sandManaCap: SkillV2Def = {
|
|
||||||
id: 'sandManaCap', name: 'Sand Mana Capacity', description: '+10% sand capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 350, studyHours: 6,
|
|
||||||
effects: [{ stat: 'sandCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const lightningManaCap: SkillV2Def = {
|
|
||||||
id: 'lightningManaCap', name: 'Lightning Mana Capacity', description: '+10% lightning capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 350, studyHours: 6,
|
|
||||||
effects: [{ stat: 'lightningCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const transferenceManaCap: SkillV2Def = {
|
|
||||||
id: 'transferenceManaCap', name: 'Transference Mana Capacity', description: '+10% transference capacity per level',
|
|
||||||
category: 'mana', maxLevel: 10, costPerLevel: 200, studyHours: 4,
|
|
||||||
effects: [{ stat: 'transferenceCap', mode: 'add', valuePerLevel: 10 }],
|
|
||||||
};
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// ENCHANTING SKILLS
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const enchanting: SkillV2Def = {
|
|
||||||
id: 'enchanting', name: 'Enchanting', description: 'Unlocks enchantment design',
|
|
||||||
category: 'enchant', maxLevel: 10, costPerLevel: 200, studyHours: 5,
|
|
||||||
attunementRequired: 'enchanter',
|
|
||||||
effects: [
|
|
||||||
{ stat: 'enchantCapacity', mode: 'add', valuePerLevel: 10 },
|
|
||||||
{ stat: 'enchantSpeed', mode: 'multiply', valuePerLevel: -0.02 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const efficientEnchant: SkillV2Def = {
|
|
||||||
id: 'efficientEnchant', name: 'Efficient Enchant', description: 'Reduces capacity cost',
|
|
||||||
category: 'enchant', maxLevel: 5, costPerLevel: 350, studyHours: 6,
|
|
||||||
prerequisites: { enchanting: 3 }, attunementRequired: 'enchanter',
|
|
||||||
effects: [{ stat: 'enchantCapacity', mode: 'multiply', valuePerLevel: -0.05 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const enchantSpeed: SkillV2Def = {
|
|
||||||
id: 'enchantSpeed', name: 'Enchant Speed', description: 'Reduces enchantment time',
|
|
||||||
category: 'enchant', maxLevel: 5, costPerLevel: 300, studyHours: 4,
|
|
||||||
prerequisites: { enchanting: 2 }, attunementRequired: 'enchanter',
|
|
||||||
effects: [{ stat: 'enchantSpeed', mode: 'multiply', valuePerLevel: -0.10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const essenceRefining: SkillV2Def = {
|
|
||||||
id: 'essenceRefining', name: 'Essence Refining', description: 'Increases enchantment power',
|
|
||||||
category: 'enchant', maxLevel: 3, costPerLevel: 400, studyHours: 7,
|
|
||||||
prerequisites: { enchanting: 4 }, attunementRequired: 'enchanter',
|
|
||||||
effects: [{ stat: 'enchantPower', mode: 'multiply', valuePerLevel: 0.10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const disenchanting: SkillV2Def = {
|
|
||||||
id: 'disenchanting', name: 'Disenchanting', description: 'Recover mana on removal',
|
|
||||||
category: 'enchant', maxLevel: 3, costPerLevel: 300, studyHours: 3,
|
|
||||||
prerequisites: { enchanting: 2 }, attunementRequired: 'enchanter',
|
|
||||||
effects: [{ stat: 'disenchantRecovery', mode: 'add', valuePerLevel: 0.20 }],
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// GOLEMANCY SKILLS
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const golemMastery: SkillV2Def = {
|
|
||||||
id: 'golemMastery', name: 'Golem Mastery', description: '+10% golem damage per level',
|
|
||||||
category: 'golemancy', maxLevel: 10, costPerLevel: 300, studyHours: 6,
|
|
||||||
attunementRequired: 'fabricator',
|
|
||||||
effects: [{ stat: 'golemDamage', mode: 'multiply', valuePerLevel: 0.10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const golemEfficiency: SkillV2Def = {
|
|
||||||
id: 'golemEfficiency', name: 'Golem Efficiency', description: '+5% golem attack speed per level',
|
|
||||||
category: 'golemancy', maxLevel: 10, costPerLevel: 350, studyHours: 6,
|
|
||||||
attunementRequired: 'fabricator',
|
|
||||||
effects: [{ stat: 'attackSpeed', mode: 'multiply', valuePerLevel: -0.05 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const golemLongevity: SkillV2Def = {
|
|
||||||
id: 'golemLongevity', name: 'Golem Longevity', description: '+1 floor duration per level',
|
|
||||||
category: 'golemancy', maxLevel: 10, costPerLevel: 500, studyHours: 8,
|
|
||||||
attunementRequired: 'fabricator',
|
|
||||||
effects: [{ stat: 'golemDuration', mode: 'add', valuePerLevel: 1 }],
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// HYBRID SKILLS (require 2 attunements at level 5+)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const pactWeaving: SkillV2Def = {
|
|
||||||
id: 'pactWeaving', name: 'Pact-Weaving', description: 'Weave guardian essence into enchantments',
|
|
||||||
category: 'hybrid', maxLevel: 10, costPerLevel: 750, studyHours: 15,
|
|
||||||
effects: [{ stat: 'enchantPower', mode: 'multiply', valuePerLevel: 0.10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const guardianConstructs: SkillV2Def = {
|
|
||||||
id: 'guardianConstructs', name: 'Guardian Constructs', description: 'Build durable singular golems',
|
|
||||||
category: 'hybrid', maxLevel: 10, costPerLevel: 800, studyHours: 18,
|
|
||||||
effects: [
|
|
||||||
{ stat: 'golemDamage', mode: 'multiply', valuePerLevel: 0.15 },
|
|
||||||
{ stat: 'golemDuration', mode: 'add', valuePerLevel: 0.25 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const enchantedGolemancy: SkillV2Def = {
|
|
||||||
id: 'enchantedGolemancy', name: 'Enchanted Golemancy', description: 'Imbue golems with spell logic',
|
|
||||||
category: 'hybrid', maxLevel: 10, costPerLevel: 850, studyHours: 20,
|
|
||||||
effects: [
|
|
||||||
{ stat: 'enchantPower', mode: 'multiply', valuePerLevel: 0.05 },
|
|
||||||
{ stat: 'golemDamage', mode: 'multiply', valuePerLevel: 0.10 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// INVOCATION / PACT SKILLS (Invoker Attunement)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const invocation: SkillV2Def = {
|
|
||||||
id: 'invocation', name: 'Invocation', description: 'Enhances spell invocation',
|
|
||||||
category: 'invocation', maxLevel: 10, costPerLevel: 300, studyHours: 6,
|
|
||||||
attunementRequired: 'invoker',
|
|
||||||
effects: [{ stat: 'spellDamage', mode: 'multiply', valuePerLevel: 0.05 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pactMastery: SkillV2Def = {
|
|
||||||
id: 'pactMastery', name: 'Pact Mastery', description: 'Enhances pact signing bonuses',
|
|
||||||
category: 'pact', maxLevel: 10, costPerLevel: 350, studyHours: 6,
|
|
||||||
attunementRequired: 'invoker',
|
|
||||||
effects: [{ stat: 'pactMultiplier', mode: 'multiply', valuePerLevel: 0.10 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const guardianLore: SkillV2Def = {
|
|
||||||
id: 'guardianLore', name: 'Guardian Lore', description: '+20% damage vs guardians',
|
|
||||||
category: 'invocation', maxLevel: 5, costPerLevel: 400, studyHours: 8,
|
|
||||||
attunementRequired: 'invoker',
|
|
||||||
effects: [{ stat: 'guardianDamage', mode: 'multiply', valuePerLevel: 0.20 }],
|
|
||||||
};
|
|
||||||
@@ -1,385 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
// ELEMENT RESEARCH SKILLS (max:1, long study – unlock enchantment effects)
|
|
||||||
// Kept for store/UI compat; effect unlocks handled via EFFECT_RESEARCH_MAPPING
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
|
||||||
export const researchManaSpells: SkillV2Def = {
|
|
||||||
id: 'researchManaSpells', name: 'Mana Spell Research', description: 'Unlock Mana Strike spell enchantment',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 200, studyHours: 4,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchFireSpells: SkillV2Def = {
|
|
||||||
id: 'researchFireSpells', name: 'Fire Spell Research', description: 'Unlock Ember Shot, Fireball spell enchantments',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 300, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchWaterSpells: SkillV2Def = {
|
|
||||||
id: 'researchWaterSpells', name: 'Water Spell Research', description: 'Unlock Water Jet, Ice Shard spell enchantments',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 300, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAirSpells: SkillV2Def = {
|
|
||||||
id: 'researchAirSpells', name: 'Air Spell Research', description: 'Unlock Gust, Wind Slash spell enchantments',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 300, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchEarthSpells: SkillV2Def = {
|
|
||||||
id: 'researchEarthSpells', name: 'Earth Spell Research', description: 'Unlock Stone Bullet, Rock Spike spell enchantments',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 350, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchLightSpells: SkillV2Def = {
|
|
||||||
id: 'researchLightSpells', name: 'Light Spell Research', description: 'Unlock Light Lance, Radiance spell enchantments',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 8,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchDarkSpells: SkillV2Def = {
|
|
||||||
id: 'researchDarkSpells', name: 'Dark Spell Research', description: 'Unlock Shadow Bolt, Dark Pulse spell enchantments',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 8,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchLifeDeathSpells: SkillV2Def = {
|
|
||||||
id: 'researchLifeDeathSpells', name: 'Death Research', description: 'Unlock Drain spell enchantment',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 8,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tier 2 advanced spell research
|
|
||||||
export const researchAdvancedFire: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedFire', name: 'Advanced Fire Research', description: 'Unlock Inferno, Flame Wave',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 600, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedWater: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedWater', name: 'Advanced Water Research', description: 'Unlock Tidal Wave, Ice Storm',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 600, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedAir: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedAir', name: 'Advanced Air Research', description: 'Unlock Hurricane, Wind Blade',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 600, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedEarth: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedEarth', name: 'Advanced Earth Research', description: 'Unlock Earthquake, Stone Barrage',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 600, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedLight: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedLight', name: 'Advanced Light Research', description: 'Unlock Solar Flare, Divine Smite',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 14,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedDark: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedDark', name: 'Advanced Dark Research', description: 'Unlock Void Rift, Shadow Storm',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 14,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tier 3 master spell research
|
|
||||||
export const researchMasterFire: SkillV2Def = {
|
|
||||||
id: 'researchMasterFire', name: 'Master Fire Research', description: 'Unlock Pyroclasm',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1200, studyHours: 24,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterWater: SkillV2Def = {
|
|
||||||
id: 'researchMasterWater', name: 'Master Water Research', description: 'Unlock Tsunami',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1200, studyHours: 24,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterEarth: SkillV2Def = {
|
|
||||||
id: 'researchMasterEarth', name: 'Master Earth Research', description: 'Unlock Meteor Strike',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1300, studyHours: 26,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Combat/misc research
|
|
||||||
export const researchDamageEffects: SkillV2Def = {
|
|
||||||
id: 'researchDamageEffects', name: 'Damage Effect Research', description: 'Unlock Minor/Moderate Power, Amplification',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 250, studyHours: 5,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchCombatEffects: SkillV2Def = {
|
|
||||||
id: 'researchCombatEffects', name: 'Combat Effect Research', description: 'Unlock Sharp Edge, Swift Casting',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 350, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchManaEffects: SkillV2Def = {
|
|
||||||
id: 'researchManaEffects', name: 'Mana Effect Research', description: 'Unlock Mana Reserve, Trickle, Mana Tap, weapon mana effects',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 200, studyHours: 4,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedManaEffects: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedManaEffects', name: 'Advanced Mana Research', description: 'Unlock Mana Reservoir, Stream, River, Surge',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 8,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchUtilityEffects: SkillV2Def = {
|
|
||||||
id: 'researchUtilityEffects', name: 'Utility Effect Research', description: 'Unlock Meditative Focus, Quick Study, Insightful',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 300, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchSpecialEffects: SkillV2Def = {
|
|
||||||
id: 'researchSpecialEffects', name: 'Special Effect Research', description: 'Unlock Echo Chamber, Siphoning, Bane',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 500, studyHours: 10,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchOverpower: SkillV2Def = {
|
|
||||||
id: 'researchOverpower', name: 'Overpower Research', description: 'Unlock Overpower effect',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compound spell research
|
|
||||||
export const researchMetalSpells: SkillV2Def = {
|
|
||||||
id: 'researchMetalSpells', name: 'Metal Spell Research', description: 'Unlock Metal Shard, Iron Fist',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchSandSpells: SkillV2Def = {
|
|
||||||
id: 'researchSandSpells', name: 'Sand Spell Research', description: 'Unlock Sand Blast, Sandstorm',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchLightningSpells: SkillV2Def = {
|
|
||||||
id: 'researchLightningSpells', name: 'Lightning Spell Research', description: 'Unlock Spark, Lightning Bolt',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedMetal: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedMetal', name: 'Advanced Metal Research', description: 'Unlock Steel Tempest',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedSand: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedSand', name: 'Advanced Sand Research', description: 'Unlock Desert Wind',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedLightning: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedLightning', name: 'Advanced Lightning Research', description: 'Unlock Chain Lightning, Storm Call',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterMetal: SkillV2Def = {
|
|
||||||
id: 'researchMasterMetal', name: 'Master Metal Research', description: 'Unlock Furnace Blast',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1300, studyHours: 26,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterSand: SkillV2Def = {
|
|
||||||
id: 'researchMasterSand', name: 'Master Sand Research', description: 'Unlock Dune Collapse',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1300, studyHours: 26,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterLightning: SkillV2Def = {
|
|
||||||
id: 'researchMasterLightning', name: 'Master Lightning Research', description: 'Unlock Thunder Strike',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1300, studyHours: 26,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchTransferenceSpells: SkillV2Def = {
|
|
||||||
id: 'researchTransferenceSpells', name: 'Transference Spell Research', description: 'Unlock Transfer Strike, Mana Rip',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 350, studyHours: 5,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedTransference: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedTransference', name: 'Advanced Transference Research', description: 'Unlock Essence Drain',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 650, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterTransference: SkillV2Def = {
|
|
||||||
id: 'researchMasterTransference', name: 'Master Transference Research', description: 'Unlock Soul Transfer',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1300, studyHours: 26,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Element capacity research (Tier 2 — +25 per stack)
|
|
||||||
export const researchAdvancedFireCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedFireCap', name: 'Advanced Fire Capacity Research', description: '+25 fire cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedWaterCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedWaterCap', name: 'Advanced Water Capacity Research', description: '+25 water cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedAirCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedAirCap', name: 'Advanced Air Capacity Research', description: '+25 air cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedEarthCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedEarthCap', name: 'Advanced Earth Capacity Research', description: '+25 earth cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedLightCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedLightCap', name: 'Advanced Light Capacity Research', description: '+25 light cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 800, studyHours: 14,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedDarkCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedDarkCap', name: 'Advanced Dark Capacity Research', description: '+25 dark cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 800, studyHours: 14,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedDeathCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedDeathCap', name: 'Advanced Death Capacity Research', description: '+25 death cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 900, studyHours: 16,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tier 3 — Master (+50 per stack)
|
|
||||||
export const researchMasterFireCap: SkillV2Def = {
|
|
||||||
id: 'researchMasterFireCap', name: 'Master Fire Capacity Research', description: '+50 fire cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1300, studyHours: 24,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterWaterCap: SkillV2Def = {
|
|
||||||
id: 'researchMasterWaterCap', name: 'Master Water Capacity Research', description: '+50 water cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1300, studyHours: 24,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterAirCap: SkillV2Def = {
|
|
||||||
id: 'researchMasterAirCap', name: 'Master Air Capacity Research', description: '+50 air cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1300, studyHours: 24,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterEarthCap: SkillV2Def = {
|
|
||||||
id: 'researchMasterEarthCap', name: 'Master Earth Capacity Research', description: '+50 earth cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1300, studyHours: 24,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterLightCap: SkillV2Def = {
|
|
||||||
id: 'researchMasterLightCap', name: 'Master Light Capacity Research', description: '+50 light cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1500, studyHours: 28,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterDarkCap: SkillV2Def = {
|
|
||||||
id: 'researchMasterDarkCap', name: 'Master Dark Capacity Research', description: '+50 dark cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1500, studyHours: 28,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchMasterDeathCap: SkillV2Def = {
|
|
||||||
id: 'researchMasterDeathCap', name: 'Master Death Capacity Research', description: '+50 death cap per level',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1600, studyHours: 30,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compound element capacity research
|
|
||||||
export const researchMetalCapacity: SkillV2Def = {
|
|
||||||
id: 'researchMetalCapacity', name: 'Metal Capacity Research', description: '+10 metal cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedMetalCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedMetalCap', name: 'Advanced Metal Capacity Research', description: '+25/+50 metal cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchSandCapacity: SkillV2Def = {
|
|
||||||
id: 'researchSandCapacity', name: 'Sand Capacity Research', description: '+10 sand cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedSandCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedSandCap', name: 'Advanced Sand Capacity Research', description: '+25/+50 sand cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchLightningCapacity: SkillV2Def = {
|
|
||||||
id: 'researchLightningCapacity', name: 'Lightning Capacity Research', description: '+10 lightning cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 400, studyHours: 6,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedLightningCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedLightningCap', name: 'Advanced Lightning Capacity Research', description: '+25/+50 lightning cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 700, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Exotic capacity research
|
|
||||||
export const researchCrystalCapacity: SkillV2Def = {
|
|
||||||
id: 'researchCrystalCapacity', name: 'Crystal Capacity Research', description: '+10 crystal cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1000, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedCrystalCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedCrystalCap', name: 'Advanced Crystal Capacity Research', description: '+25/+50 crystal cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 2000, studyHours: 24,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchStellarCapacity: SkillV2Def = {
|
|
||||||
id: 'researchStellarCapacity', name: 'Stellar Capacity Research', description: '+10 stellar cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1000, studyHours: 12,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedStellarCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedStellarCap', name: 'Advanced Stellar Capacity Research', description: '+25/+50 stellar cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 2000, studyHours: 24,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchVoidCapacity: SkillV2Def = {
|
|
||||||
id: 'researchVoidCapacity', name: 'Void Capacity Research', description: '+10 void cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 1500, studyHours: 16,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const researchAdvancedVoidCap: SkillV2Def = {
|
|
||||||
id: 'researchAdvancedVoidCap', name: 'Advanced Void Capacity Research', description: '+25/+50 void cap',
|
|
||||||
category: 'effectResearch', maxLevel: 1, costPerLevel: 3000, studyHours: 30,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
// @ts-expect-error - circular refs resolved at runtime
|
|
||||||
export { SKILLS_V2 } from './skills-v2-registry';
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
import type { SkillV2Def } from './skills-v2-types';
|
|
||||||
import {
|
|
||||||
manaWell,
|
|
||||||
manaFlow,
|
|
||||||
manaOverflow,
|
|
||||||
manaTap,
|
|
||||||
manaSurge,
|
|
||||||
manaSpring,
|
|
||||||
quickLearner,
|
|
||||||
focusedMind,
|
|
||||||
knowledgeRetention,
|
|
||||||
meditation,
|
|
||||||
deepTrance,
|
|
||||||
voidMeditation,
|
|
||||||
} from './skills-core';
|
|
||||||
import {
|
|
||||||
enchanting,
|
|
||||||
efficientEnchant,
|
|
||||||
enchantSpeed,
|
|
||||||
essenceRefining,
|
|
||||||
disenchanting,
|
|
||||||
} from './skills-enchant';
|
|
||||||
import {
|
|
||||||
arcaneFury,
|
|
||||||
combatTraining,
|
|
||||||
precision,
|
|
||||||
elementalMastery,
|
|
||||||
attackSpeed,
|
|
||||||
armorPiercing,
|
|
||||||
spellDamage,
|
|
||||||
} from './skills-combat';
|
|
||||||
import {
|
|
||||||
golemMastery,
|
|
||||||
golemEfficiency,
|
|
||||||
golemLongevity,
|
|
||||||
} from './skills-golemancy';
|
|
||||||
import {
|
|
||||||
invocation,
|
|
||||||
pactMastery,
|
|
||||||
guardianLore,
|
|
||||||
} from './skills-invocation';
|
|
||||||
import {
|
|
||||||
effCrafting,
|
|
||||||
fieldRepair,
|
|
||||||
} from './skills-crafting';
|
|
||||||
import {
|
|
||||||
fireManaCap,
|
|
||||||
waterManaCap,
|
|
||||||
airManaCap,
|
|
||||||
earthManaCap,
|
|
||||||
lightManaCap,
|
|
||||||
darkManaCap,
|
|
||||||
deathManaCap,
|
|
||||||
metalManaCap,
|
|
||||||
sandManaCap,
|
|
||||||
lightningManaCap,
|
|
||||||
transferenceManaCap,
|
|
||||||
} from './skills-element-caps';
|
|
||||||
import {
|
|
||||||
pactWeaving,
|
|
||||||
guardianConstructs,
|
|
||||||
enchantedGolemancy,
|
|
||||||
} from './skills-hybrid';
|
|
||||||
import {
|
|
||||||
researchManaSpells,
|
|
||||||
researchFireSpells,
|
|
||||||
researchWaterSpells,
|
|
||||||
researchAirSpells,
|
|
||||||
researchEarthSpells,
|
|
||||||
researchLightSpells,
|
|
||||||
researchDarkSpells,
|
|
||||||
researchLifeDeathSpells,
|
|
||||||
researchAdvancedFire,
|
|
||||||
researchAdvancedWater,
|
|
||||||
researchAdvancedAir,
|
|
||||||
researchAdvancedEarth,
|
|
||||||
researchAdvancedLight,
|
|
||||||
researchAdvancedDark,
|
|
||||||
researchMasterFire,
|
|
||||||
researchMasterWater,
|
|
||||||
researchMasterEarth,
|
|
||||||
researchDamageEffects,
|
|
||||||
researchCombatEffects,
|
|
||||||
researchManaEffects,
|
|
||||||
researchAdvancedManaEffects,
|
|
||||||
researchUtilityEffects,
|
|
||||||
researchSpecialEffects,
|
|
||||||
researchOverpower,
|
|
||||||
researchMetalSpells,
|
|
||||||
researchSandSpells,
|
|
||||||
researchLightningSpells,
|
|
||||||
researchAdvancedMetal,
|
|
||||||
researchAdvancedSand,
|
|
||||||
researchAdvancedLightning,
|
|
||||||
researchMasterMetal,
|
|
||||||
researchMasterSand,
|
|
||||||
researchMasterLightning,
|
|
||||||
researchTransferenceSpells,
|
|
||||||
researchAdvancedTransference,
|
|
||||||
researchMasterTransference,
|
|
||||||
researchAdvancedFireCap,
|
|
||||||
researchAdvancedWaterCap,
|
|
||||||
researchAdvancedAirCap,
|
|
||||||
researchAdvancedEarthCap,
|
|
||||||
researchAdvancedLightCap,
|
|
||||||
researchAdvancedDarkCap,
|
|
||||||
researchAdvancedDeathCap,
|
|
||||||
researchMasterFireCap,
|
|
||||||
researchMasterWaterCap,
|
|
||||||
researchMasterAirCap,
|
|
||||||
researchMasterEarthCap,
|
|
||||||
researchMasterLightCap,
|
|
||||||
researchMasterDarkCap,
|
|
||||||
researchMasterDeathCap,
|
|
||||||
researchMetalCapacity,
|
|
||||||
researchAdvancedMetalCap,
|
|
||||||
researchSandCapacity,
|
|
||||||
researchAdvancedSandCap,
|
|
||||||
researchLightningCapacity,
|
|
||||||
researchAdvancedLightningCap,
|
|
||||||
researchCrystalCapacity,
|
|
||||||
researchAdvancedCrystalCap,
|
|
||||||
researchStellarCapacity,
|
|
||||||
researchAdvancedStellarCap,
|
|
||||||
researchVoidCapacity,
|
|
||||||
researchAdvancedVoidCap,
|
|
||||||
} from './skills-research';
|
|
||||||
|
|
||||||
// Re-export individual skills
|
|
||||||
export {
|
|
||||||
manaWell,
|
|
||||||
manaFlow,
|
|
||||||
manaOverflow,
|
|
||||||
manaTap,
|
|
||||||
manaSurge,
|
|
||||||
manaSpring,
|
|
||||||
quickLearner,
|
|
||||||
focusedMind,
|
|
||||||
knowledgeRetention,
|
|
||||||
meditation,
|
|
||||||
deepTrance,
|
|
||||||
voidMeditation,
|
|
||||||
} from './skills-core';
|
|
||||||
export {
|
|
||||||
enchanting,
|
|
||||||
efficientEnchant,
|
|
||||||
enchantSpeed,
|
|
||||||
essenceRefining,
|
|
||||||
disenchanting,
|
|
||||||
} from './skills-enchant';
|
|
||||||
export {
|
|
||||||
arcaneFury,
|
|
||||||
combatTraining,
|
|
||||||
precision,
|
|
||||||
elementalMastery,
|
|
||||||
attackSpeed,
|
|
||||||
armorPiercing,
|
|
||||||
spellDamage,
|
|
||||||
} from './skills-combat';
|
|
||||||
export {
|
|
||||||
golemMastery,
|
|
||||||
golemEfficiency,
|
|
||||||
golemLongevity,
|
|
||||||
} from './skills-golemancy';
|
|
||||||
export {
|
|
||||||
invocation,
|
|
||||||
pactMastery,
|
|
||||||
guardianLore,
|
|
||||||
} from './skills-invocation';
|
|
||||||
export {
|
|
||||||
effCrafting,
|
|
||||||
fieldRepair,
|
|
||||||
} from './skills-crafting';
|
|
||||||
export {
|
|
||||||
fireManaCap,
|
|
||||||
waterManaCap,
|
|
||||||
airManaCap,
|
|
||||||
earthManaCap,
|
|
||||||
lightManaCap,
|
|
||||||
darkManaCap,
|
|
||||||
deathManaCap,
|
|
||||||
metalManaCap,
|
|
||||||
sandManaCap,
|
|
||||||
lightningManaCap,
|
|
||||||
transferenceManaCap,
|
|
||||||
} from './skills-element-caps';
|
|
||||||
export {
|
|
||||||
pactWeaving,
|
|
||||||
guardianConstructs,
|
|
||||||
enchantedGolemancy,
|
|
||||||
} from './skills-hybrid';
|
|
||||||
export {
|
|
||||||
researchManaSpells,
|
|
||||||
researchFireSpells,
|
|
||||||
researchWaterSpells,
|
|
||||||
researchAirSpells,
|
|
||||||
researchEarthSpells,
|
|
||||||
researchLightSpells,
|
|
||||||
researchDarkSpells,
|
|
||||||
researchLifeDeathSpells,
|
|
||||||
researchAdvancedFire,
|
|
||||||
researchAdvancedWater,
|
|
||||||
researchAdvancedAir,
|
|
||||||
researchAdvancedEarth,
|
|
||||||
researchAdvancedLight,
|
|
||||||
researchAdvancedDark,
|
|
||||||
researchMasterFire,
|
|
||||||
researchMasterWater,
|
|
||||||
researchMasterEarth,
|
|
||||||
researchDamageEffects,
|
|
||||||
researchCombatEffects,
|
|
||||||
researchManaEffects,
|
|
||||||
researchAdvancedManaEffects,
|
|
||||||
researchUtilityEffects,
|
|
||||||
researchSpecialEffects,
|
|
||||||
researchOverpower,
|
|
||||||
researchMetalSpells,
|
|
||||||
researchSandSpells,
|
|
||||||
researchLightningSpells,
|
|
||||||
researchAdvancedMetal,
|
|
||||||
researchAdvancedSand,
|
|
||||||
researchAdvancedLightning,
|
|
||||||
researchMasterMetal,
|
|
||||||
researchMasterSand,
|
|
||||||
researchMasterLightning,
|
|
||||||
researchTransferenceSpells,
|
|
||||||
researchAdvancedTransference,
|
|
||||||
researchMasterTransference,
|
|
||||||
researchAdvancedFireCap,
|
|
||||||
researchAdvancedWaterCap,
|
|
||||||
researchAdvancedAirCap,
|
|
||||||
researchAdvancedEarthCap,
|
|
||||||
researchAdvancedLightCap,
|
|
||||||
researchAdvancedDarkCap,
|
|
||||||
researchAdvancedDeathCap,
|
|
||||||
researchMasterFireCap,
|
|
||||||
researchMasterWaterCap,
|
|
||||||
researchMasterAirCap,
|
|
||||||
researchMasterEarthCap,
|
|
||||||
researchMasterLightCap,
|
|
||||||
researchMasterDarkCap,
|
|
||||||
researchMasterDeathCap,
|
|
||||||
researchMetalCapacity,
|
|
||||||
researchAdvancedMetalCap,
|
|
||||||
researchSandCapacity,
|
|
||||||
researchAdvancedSandCap,
|
|
||||||
researchLightningCapacity,
|
|
||||||
researchAdvancedLightningCap,
|
|
||||||
researchCrystalCapacity,
|
|
||||||
researchAdvancedCrystalCap,
|
|
||||||
researchStellarCapacity,
|
|
||||||
researchAdvancedStellarCap,
|
|
||||||
researchVoidCapacity,
|
|
||||||
researchAdvancedVoidCap,
|
|
||||||
} from './skills-research';
|
|
||||||
|
|
||||||
// Build the flat SKILLS_V2 record (legacy compat)
|
|
||||||
export const SKILLS_V2: Record<string, SkillV2Def> = {
|
|
||||||
manaWell, manaFlow, manaOverflow, manaTap, manaSurge, manaSpring,
|
|
||||||
quickLearner, focusedMind, knowledgeRetention,
|
|
||||||
meditation, deepTrance, voidMeditation,
|
|
||||||
enchanting, efficientEnchant, enchantSpeed, essenceRefining, disenchanting,
|
|
||||||
arcaneFury, combatTraining, precision, elementalMastery, attackSpeed, armorPiercing, spellDamage,
|
|
||||||
golemMastery, golemEfficiency, golemLongevity,
|
|
||||||
invocation, pactMastery, guardianLore,
|
|
||||||
effCrafting, fieldRepair,
|
|
||||||
fireManaCap, waterManaCap, airManaCap, earthManaCap,
|
|
||||||
lightManaCap, darkManaCap, deathManaCap, metalManaCap,
|
|
||||||
sandManaCap, lightningManaCap, transferenceManaCap,
|
|
||||||
pactWeaving, guardianConstructs, enchantedGolemancy,
|
|
||||||
researchManaSpells, researchFireSpells, researchWaterSpells, researchAirSpells,
|
|
||||||
researchEarthSpells, researchLightSpells, researchDarkSpells, researchLifeDeathSpells,
|
|
||||||
researchAdvancedFire, researchAdvancedWater, researchAdvancedAir, researchAdvancedEarth,
|
|
||||||
researchAdvancedLight, researchAdvancedDark,
|
|
||||||
researchMasterFire, researchMasterWater, researchMasterEarth,
|
|
||||||
researchDamageEffects, researchCombatEffects, researchManaEffects,
|
|
||||||
researchAdvancedManaEffects, researchUtilityEffects, researchSpecialEffects, researchOverpower,
|
|
||||||
researchMetalSpells, researchSandSpells, researchLightningSpells,
|
|
||||||
researchAdvancedMetal, researchAdvancedSand, researchAdvancedLightning,
|
|
||||||
researchMasterMetal, researchMasterSand, researchMasterLightning,
|
|
||||||
researchTransferenceSpells, researchAdvancedTransference, researchMasterTransference,
|
|
||||||
researchAdvancedFireCap, researchAdvancedWaterCap, researchAdvancedAirCap, researchAdvancedEarthCap,
|
|
||||||
researchAdvancedLightCap, researchAdvancedDarkCap, researchAdvancedDeathCap,
|
|
||||||
researchMasterFireCap, researchMasterWaterCap, researchMasterAirCap, researchMasterEarthCap,
|
|
||||||
researchMasterLightCap, researchMasterDarkCap, researchMasterDeathCap,
|
|
||||||
researchMetalCapacity, researchAdvancedMetalCap,
|
|
||||||
researchSandCapacity, researchAdvancedSandCap,
|
|
||||||
researchLightningCapacity, researchAdvancedLightningCap,
|
|
||||||
researchCrystalCapacity, researchAdvancedCrystalCap,
|
|
||||||
researchStellarCapacity, researchAdvancedStellarCap,
|
|
||||||
researchVoidCapacity, researchAdvancedVoidCap,
|
|
||||||
};
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
// ─── Skill System v2 Types ─────────────────────────────────────────────────────
|
|
||||||
// Shared types for the new flat skill system.
|
|
||||||
|
|
||||||
export type StatKey =
|
|
||||||
| 'maxMana'
|
|
||||||
| 'manaRegen'
|
|
||||||
| 'clickMana'
|
|
||||||
| 'elementCap'
|
|
||||||
| 'studySpeed'
|
|
||||||
| 'studyCostMult'
|
|
||||||
| 'meditationEfficiency'
|
|
||||||
| 'enchantCapacity'
|
|
||||||
| 'enchantSpeed'
|
|
||||||
| 'enchantPower'
|
|
||||||
| 'disenchantRecovery'
|
|
||||||
| 'baseDamage'
|
|
||||||
| 'damageMultiplier'
|
|
||||||
| 'attackSpeed'
|
|
||||||
| 'critChance'
|
|
||||||
| 'critMultiplier'
|
|
||||||
| 'armorPierce'
|
|
||||||
| 'insightGain'
|
|
||||||
| 'golemDamage'
|
|
||||||
| 'golemDuration'
|
|
||||||
| 'pactMultiplier'
|
|
||||||
| 'conversionRate'
|
|
||||||
| 'spellDamage'
|
|
||||||
| 'guardianDamage'
|
|
||||||
| 'craftSpeed'
|
|
||||||
| 'repairSpeed'
|
|
||||||
// Per-element capacity stats
|
|
||||||
| 'fireCap'
|
|
||||||
| 'waterCap'
|
|
||||||
| 'airCap'
|
|
||||||
| 'earthCap'
|
|
||||||
| 'lightCap'
|
|
||||||
| 'darkCap'
|
|
||||||
| 'deathCap'
|
|
||||||
| 'metalCap'
|
|
||||||
| 'sandCap'
|
|
||||||
| 'lightningCap'
|
|
||||||
| 'transferenceCap'
|
|
||||||
| 'crystalCap'
|
|
||||||
| 'stellarCap'
|
|
||||||
| 'voidCap';
|
|
||||||
|
|
||||||
export interface SkillEffect {
|
|
||||||
stat: StatKey;
|
|
||||||
mode: 'add' | 'multiply';
|
|
||||||
valuePerLevel: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SkillV2Def {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
category: string;
|
|
||||||
maxLevel: number;
|
|
||||||
costPerLevel: number;
|
|
||||||
studyHours: number;
|
|
||||||
effects: SkillEffect[];
|
|
||||||
attunementRequired?: string;
|
|
||||||
prerequisites?: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ComputedStats {
|
|
||||||
maxMana: number;
|
|
||||||
manaRegen: number;
|
|
||||||
clickMana: number;
|
|
||||||
elementCap: number;
|
|
||||||
studySpeed: number;
|
|
||||||
studyCostMult: number;
|
|
||||||
meditationEfficiency: number;
|
|
||||||
enchantCapacity: number;
|
|
||||||
enchantSpeed: number;
|
|
||||||
enchantPower: number;
|
|
||||||
disenchantRecovery: number;
|
|
||||||
baseDamage: number;
|
|
||||||
damageMultiplier: number;
|
|
||||||
attackSpeed: number;
|
|
||||||
critChance: number;
|
|
||||||
critMultiplier: number;
|
|
||||||
armorPierce: number;
|
|
||||||
insightGain: number;
|
|
||||||
golemDamage: number;
|
|
||||||
golemDuration: number;
|
|
||||||
pactMultiplier: number;
|
|
||||||
conversionRate: number;
|
|
||||||
spellDamage: number;
|
|
||||||
guardianDamage: number;
|
|
||||||
craftSpeed: number;
|
|
||||||
repairSpeed: number;
|
|
||||||
// Per-element capacity (for direct skill contributions)
|
|
||||||
fireCap: number;
|
|
||||||
waterCap: number;
|
|
||||||
airCap: number;
|
|
||||||
earthCap: number;
|
|
||||||
lightCap: number;
|
|
||||||
darkCap: number;
|
|
||||||
deathCap: number;
|
|
||||||
metalCap: number;
|
|
||||||
sandCap: number;
|
|
||||||
lightningCap: number;
|
|
||||||
transferenceCap: number;
|
|
||||||
crystalCap: number;
|
|
||||||
stellarCap: number;
|
|
||||||
voidCap: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default base stats (all skills at zero, no equipment, no prestige)
|
|
||||||
export const BASE_STATS: ComputedStats = {
|
|
||||||
maxMana: 100,
|
|
||||||
manaRegen: 2,
|
|
||||||
clickMana: 1,
|
|
||||||
elementCap: 10,
|
|
||||||
studySpeed: 1,
|
|
||||||
studyCostMult: 1,
|
|
||||||
meditationEfficiency: 1,
|
|
||||||
enchantCapacity: 100,
|
|
||||||
enchantSpeed: 1,
|
|
||||||
enchantPower: 1,
|
|
||||||
disenchantRecovery: 1,
|
|
||||||
baseDamage: 5,
|
|
||||||
damageMultiplier: 1,
|
|
||||||
attackSpeed: 1,
|
|
||||||
critChance: 0,
|
|
||||||
critMultiplier: 1.5,
|
|
||||||
armorPierce: 0,
|
|
||||||
insightGain: 1,
|
|
||||||
golemDamage: 1,
|
|
||||||
golemDuration: 1,
|
|
||||||
pactMultiplier: 1,
|
|
||||||
conversionRate: 1,
|
|
||||||
spellDamage: 1,
|
|
||||||
guardianDamage: 1,
|
|
||||||
craftSpeed: 1,
|
|
||||||
repairSpeed: 1,
|
|
||||||
fireCap: 0, waterCap: 0, airCap: 0, earthCap: 0,
|
|
||||||
lightCap: 0, darkCap: 0, deathCap: 0,
|
|
||||||
metalCap: 0, sandCap: 0, lightningCap: 0,
|
|
||||||
transferenceCap: 0, crystalCap: 0, stellarCap: 0, voidCap: 0,
|
|
||||||
};
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
import type { SkillV2Def, SkillEffect, StatKey, ComputedStats } from './skills-v2-types';
|
|
||||||
import { SKILLS_V2 } from './skills-v2-defs';
|
|
||||||
|
|
||||||
// Re-export individual skills from category modules
|
|
||||||
export {
|
|
||||||
manaWell,
|
|
||||||
manaFlow,
|
|
||||||
manaOverflow,
|
|
||||||
manaTap,
|
|
||||||
manaSurge,
|
|
||||||
manaSpring,
|
|
||||||
quickLearner,
|
|
||||||
focusedMind,
|
|
||||||
knowledgeRetention,
|
|
||||||
meditation,
|
|
||||||
deepTrance,
|
|
||||||
voidMeditation,
|
|
||||||
} from './skills-core';
|
|
||||||
|
|
||||||
export {
|
|
||||||
enchanting,
|
|
||||||
efficientEnchant,
|
|
||||||
enchantSpeed,
|
|
||||||
essenceRefining,
|
|
||||||
disenchanting,
|
|
||||||
} from './skills-enchant';
|
|
||||||
|
|
||||||
export {
|
|
||||||
arcaneFury,
|
|
||||||
combatTraining,
|
|
||||||
precision,
|
|
||||||
elementalMastery,
|
|
||||||
attackSpeed,
|
|
||||||
armorPiercing,
|
|
||||||
spellDamage,
|
|
||||||
} from './skills-combat';
|
|
||||||
|
|
||||||
export {
|
|
||||||
golemMastery,
|
|
||||||
golemEfficiency,
|
|
||||||
golemLongevity,
|
|
||||||
} from './skills-golemancy';
|
|
||||||
|
|
||||||
export {
|
|
||||||
invocation,
|
|
||||||
pactMastery,
|
|
||||||
guardianLore,
|
|
||||||
} from './skills-invocation';
|
|
||||||
|
|
||||||
export {
|
|
||||||
effCrafting,
|
|
||||||
fieldRepair,
|
|
||||||
} from './skills-crafting';
|
|
||||||
|
|
||||||
export {
|
|
||||||
fireManaCap,
|
|
||||||
waterManaCap,
|
|
||||||
airManaCap,
|
|
||||||
earthManaCap,
|
|
||||||
lightManaCap,
|
|
||||||
darkManaCap,
|
|
||||||
deathManaCap,
|
|
||||||
metalManaCap,
|
|
||||||
sandManaCap,
|
|
||||||
lightningManaCap,
|
|
||||||
transferenceManaCap,
|
|
||||||
} from './skills-element-caps';
|
|
||||||
|
|
||||||
export {
|
|
||||||
pactWeaving,
|
|
||||||
guardianConstructs,
|
|
||||||
enchantedGolemancy,
|
|
||||||
} from './skills-hybrid';
|
|
||||||
|
|
||||||
export {
|
|
||||||
researchManaSpells,
|
|
||||||
researchFireSpells,
|
|
||||||
researchWaterSpells,
|
|
||||||
researchAirSpells,
|
|
||||||
researchEarthSpells,
|
|
||||||
researchLightSpells,
|
|
||||||
researchDarkSpells,
|
|
||||||
researchLifeDeathSpells,
|
|
||||||
researchAdvancedFire,
|
|
||||||
researchAdvancedWater,
|
|
||||||
researchAdvancedAir,
|
|
||||||
researchAdvancedEarth,
|
|
||||||
researchAdvancedLight,
|
|
||||||
researchAdvancedDark,
|
|
||||||
researchMasterFire,
|
|
||||||
researchMasterWater,
|
|
||||||
researchMasterEarth,
|
|
||||||
researchDamageEffects,
|
|
||||||
researchCombatEffects,
|
|
||||||
researchManaEffects,
|
|
||||||
researchAdvancedManaEffects,
|
|
||||||
researchUtilityEffects,
|
|
||||||
researchSpecialEffects,
|
|
||||||
researchOverpower,
|
|
||||||
researchMetalSpells,
|
|
||||||
researchSandSpells,
|
|
||||||
researchLightningSpells,
|
|
||||||
researchAdvancedMetal,
|
|
||||||
researchAdvancedSand,
|
|
||||||
researchAdvancedLightning,
|
|
||||||
researchMasterMetal,
|
|
||||||
researchMasterSand,
|
|
||||||
researchMasterLightning,
|
|
||||||
researchTransferenceSpells,
|
|
||||||
researchAdvancedTransference,
|
|
||||||
researchMasterTransference,
|
|
||||||
researchAdvancedFireCap,
|
|
||||||
researchAdvancedWaterCap,
|
|
||||||
researchAdvancedAirCap,
|
|
||||||
researchAdvancedEarthCap,
|
|
||||||
researchAdvancedLightCap,
|
|
||||||
researchAdvancedDarkCap,
|
|
||||||
researchAdvancedDeathCap,
|
|
||||||
researchMasterFireCap,
|
|
||||||
researchMasterWaterCap,
|
|
||||||
researchMasterAirCap,
|
|
||||||
researchMasterEarthCap,
|
|
||||||
researchMasterLightCap,
|
|
||||||
researchMasterDarkCap,
|
|
||||||
researchMasterDeathCap,
|
|
||||||
researchMetalCapacity,
|
|
||||||
researchAdvancedMetalCap,
|
|
||||||
researchSandCapacity,
|
|
||||||
researchAdvancedSandCap,
|
|
||||||
researchLightningCapacity,
|
|
||||||
researchAdvancedLightningCap,
|
|
||||||
researchCrystalCapacity,
|
|
||||||
researchAdvancedCrystalCap,
|
|
||||||
researchStellarCapacity,
|
|
||||||
researchAdvancedStellarCap,
|
|
||||||
researchVoidCapacity,
|
|
||||||
researchAdvancedVoidCap,
|
|
||||||
} from './skills-research';
|
|
||||||
|
|
||||||
export { SKILLS_V2 };
|
|
||||||
|
|
||||||
// Default Base Stats
|
|
||||||
export const BASE_STATS: ComputedStats = {
|
|
||||||
maxMana: 100,
|
|
||||||
manaRegen: 2,
|
|
||||||
clickMana: 1,
|
|
||||||
elementCap: 10,
|
|
||||||
studySpeed: 1,
|
|
||||||
studyCostMult: 1,
|
|
||||||
meditationEfficiency: 1,
|
|
||||||
enchantCapacity: 100,
|
|
||||||
enchantSpeed: 1,
|
|
||||||
enchantPower: 1,
|
|
||||||
disenchantRecovery: 1,
|
|
||||||
baseDamage: 5,
|
|
||||||
damageMultiplier: 1,
|
|
||||||
attackSpeed: 1,
|
|
||||||
critChance: 0,
|
|
||||||
critMultiplier: 1.5,
|
|
||||||
armorPierce: 0,
|
|
||||||
insightGain: 1,
|
|
||||||
golemDamage: 1,
|
|
||||||
golemDuration: 1,
|
|
||||||
pactMultiplier: 1,
|
|
||||||
conversionRate: 1,
|
|
||||||
spellDamage: 1,
|
|
||||||
guardianDamage: 1,
|
|
||||||
craftSpeed: 1,
|
|
||||||
repairSpeed: 1,
|
|
||||||
fireCap: 0, waterCap: 0, airCap: 0, earthCap: 0,
|
|
||||||
lightCap: 0, darkCap: 0, deathCap: 0,
|
|
||||||
metalCap: 0, sandCap: 0, lightningCap: 0,
|
|
||||||
transferenceCap: 0, crystalCap: 0, stellarCap: 0, voidCap: 0,
|
|
||||||
elementalDamage: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ELEMENT_CAP_STATS: Record<string, keyof ComputedStats> = {
|
|
||||||
fire: 'fireCap', water: 'waterCap', air: 'airCap', earth: 'earthCap',
|
|
||||||
light: 'lightCap', dark: 'darkCap', death: 'deathCap', metal: 'metalCap',
|
|
||||||
sand: 'sandCap', lightning: 'lightningCap', transference: 'transferenceCap',
|
|
||||||
crystal: 'crystalCap', stellar: 'stellarCap', void: 'voidCap',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute all game stats from skill levels.
|
|
||||||
*/
|
|
||||||
export function computeStats(
|
|
||||||
skills: Record<string, number>,
|
|
||||||
prestigeUpgrades: Record<string, number> = {}
|
|
||||||
): ComputedStats {
|
|
||||||
const stats: ComputedStats = { ...BASE_STATS };
|
|
||||||
|
|
||||||
for (const [skillId, level] of Object.entries(skills)) {
|
|
||||||
if (level <= 0) continue;
|
|
||||||
const def = SKILLS_V2[skillId];
|
|
||||||
if (!def) continue;
|
|
||||||
for (const effect of def.effects) {
|
|
||||||
const key = effect.stat as keyof ComputedStats;
|
|
||||||
const currentVal = stats[key] as number;
|
|
||||||
if (effect.mode === 'add') {
|
|
||||||
(stats[key] as number) = currentVal + effect.valuePerLevel * level;
|
|
||||||
} else {
|
|
||||||
const perLevelMultiplier = 1 + effect.valuePerLevel;
|
|
||||||
let result = currentVal;
|
|
||||||
for (let i = 0; i < level; i++) result *= perLevelMultiplier;
|
|
||||||
(stats[key] as number) = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prestigeUpgrades.manaWell) stats.maxMana += prestigeUpgrades.manaWell * 500;
|
|
||||||
if (prestigeUpgrades.manaFlow) stats.manaRegen += prestigeUpgrades.manaFlow * 0.5;
|
|
||||||
if (prestigeUpgrades.elementalAttune) stats.elementCap += prestigeUpgrades.elementalAttune * 25;
|
|
||||||
if (prestigeUpgrades.pactBinding) stats.pactMultiplier += prestigeUpgrades.pactBinding * 0.1;
|
|
||||||
if (prestigeUpgrades.insightAmp) stats.insightGain *= 1 + prestigeUpgrades.insightAmp * 0.25;
|
|
||||||
|
|
||||||
let elementCapFromSkills = 0;
|
|
||||||
for (const [, statKey] of Object.entries(ELEMENT_CAP_STATS)) {
|
|
||||||
const val = stats[statKey] as number;
|
|
||||||
if (val > 0) elementCapFromSkills += val;
|
|
||||||
}
|
|
||||||
if (elementCapFromSkills > 0) stats.elementCap += elementCapFromSkills;
|
|
||||||
|
|
||||||
stats.maxMana = Math.max(1, Math.round(stats.maxMana));
|
|
||||||
stats.manaRegen = Math.round(stats.manaRegen * 100) / 100;
|
|
||||||
stats.clickMana = Math.max(1, stats.clickMana);
|
|
||||||
stats.elementCap = Math.max(10, Math.round(stats.elementCap));
|
|
||||||
stats.baseDamage = Math.max(1, Math.round(stats.baseDamage));
|
|
||||||
stats.critChance = Math.min(1, Math.max(0, stats.critChance));
|
|
||||||
stats.armorPierce = Math.min(1, Math.max(0, stats.armorPierce));
|
|
||||||
stats.attackSpeed = Math.max(0.1, stats.attackSpeed);
|
|
||||||
stats.insightGain = Math.max(0, stats.insightGain);
|
|
||||||
stats.golemDamage = Math.max(0.1, stats.golemDamage);
|
|
||||||
stats.golemDuration = Math.max(1, stats.golemDuration);
|
|
||||||
stats.enchantCapacity = Math.max(10, Math.round(stats.enchantCapacity));
|
|
||||||
stats.conversionRate = Math.max(0, stats.conversionRate);
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the base key for a tiered skill (strips _tN suffix).
|
|
||||||
*/
|
|
||||||
export function getBaseSkillId(skillId: string): string {
|
|
||||||
const match = skillId.match(/^(.+?)_t(\d+)$/);
|
|
||||||
return match ? match[1] : skillId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a skill has prerequisites that are met.
|
|
||||||
*/
|
|
||||||
export function hasPrerequisites(
|
|
||||||
skills: Record<string, number>,
|
|
||||||
prerequisites?: Record<string, number>
|
|
||||||
): boolean {
|
|
||||||
if (!prerequisites) return true;
|
|
||||||
for (const [reqId, reqLevel] of Object.entries(prerequisites)) {
|
|
||||||
if ((skills[reqId] || 0) < reqLevel) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Legacy compat wrapper */
|
|
||||||
export function computeStatsLegacy(state: {
|
|
||||||
skills: Record<string, number>;
|
|
||||||
prestigeUpgrades?: Record<string, number>;
|
|
||||||
}): ComputedStats {
|
|
||||||
return computeStats(state.skills, state.prestigeUpgrades || {});
|
|
||||||
}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
// ─── Skills ───────────────────────────────────────────────────────────────────
|
|
||||||
import type { SkillDef } from '../types';
|
|
||||||
|
|
||||||
export const SKILLS_DEF: Record<string, SkillDef> = {
|
|
||||||
// Mana Skills (4-8 hours study) - Core, no attunement required
|
|
||||||
manaWell: { name: "Mana Well", desc: "+100 max mana", cat: "mana", max: 10, base: 100, studyTime: 4 },
|
|
||||||
manaFlow: { name: "Mana Flow", desc: "+1 regen/hr", cat: "mana", max: 10, base: 150, studyTime: 5 },
|
|
||||||
// Per-mana-type capacity upgrades (Bug 9)
|
|
||||||
fireManaCap: { name: "Fire Mana Capacity +10%", desc: "+10% fire mana capacity", cat: "mana", max: 10, base: 200, studyTime: 4, cost: { type: 'element', element: 'fire', amount: 100 } },
|
|
||||||
waterManaCap: { name: "Water Mana Capacity +10%", desc: "+10% water mana capacity", cat: "mana", max: 10, base: 200, studyTime: 4, cost: { type: 'element', element: 'water', amount: 100 } },
|
|
||||||
airManaCap: { name: "Air Mana Capacity +10%", desc: "+10% air mana capacity", cat: "mana", max: 10, base: 200, studyTime: 4, cost: { type: 'element', element: 'air', amount: 100 } },
|
|
||||||
earthManaCap: { name: "Earth Mana Capacity +10%", desc: "+10% earth mana capacity", cat: "mana", max: 10, base: 200, studyTime: 4, cost: { type: 'element', element: 'earth', amount: 100 } },
|
|
||||||
lightManaCap: { name: "Light Mana Capacity +10%", desc: "+10% light mana capacity", cat: "mana", max: 10, base: 250, studyTime: 5, cost: { type: 'element', element: 'light', amount: 150 } },
|
|
||||||
darkManaCap: { name: "Dark Mana Capacity +10%", desc: "+10% dark mana capacity", cat: "mana", max: 10, base: 250, studyTime: 5, cost: { type: 'element', element: 'dark', amount: 150 } },
|
|
||||||
deathManaCap: { name: "Death Mana Capacity +10%", desc: "+10% death mana capacity", cat: "mana", max: 10, base: 300, studyTime: 6, cost: { type: 'element', element: 'death', amount: 200 } },
|
|
||||||
// Composite element capacity upgrades
|
|
||||||
metalManaCap: { name: "Metal Mana Capacity +10%", desc: "+10% metal mana capacity", cat: "mana", max: 10, base: 350, studyTime: 6, cost: { type: 'element', element: 'metal', amount: 250 } },
|
|
||||||
sandManaCap: { name: "Sand Mana Capacity +10%", desc: "+10% sand mana capacity", cat: "mana", max: 10, base: 350, studyTime: 6, cost: { type: 'element', element: 'sand', amount: 250 } },
|
|
||||||
lightningManaCap: { name: "Lightning Mana Capacity +10%", desc: "+10% lightning mana capacity", cat: "mana", max: 10, base: 350, studyTime: 6, cost: { type: 'element', element: 'lightning', amount: 250 } },
|
|
||||||
// Utility mana capacity upgrades
|
|
||||||
transferenceManaCap: { name: "Transference Mana Capacity +10%", desc: "+10% transference mana capacity", cat: "mana", max: 10, base: 200, studyTime: 4, cost: { type: 'element', element: 'transference', amount: 100 } },
|
|
||||||
manaOverflow: { name: "Mana Overflow", desc: "+25% mana from clicks", cat: "mana", max: 5, base: 400, req: { manaWell: 3 }, studyTime: 6 },
|
|
||||||
|
|
||||||
// Study Skills (3-6 hours study) - Core, no attunement required
|
|
||||||
quickLearner: { name: "Quick Learner", desc: "+10% study speed", cat: "study", max: 10, base: 250, studyTime: 4 },
|
|
||||||
focusedMind: { name: "Focused Mind", desc: "-5% study mana cost", cat: "study", max: 10, base: 300, studyTime: 5 },
|
|
||||||
meditation: { name: "Meditation Focus", desc: "Up to 2.5x regen after 4hrs meditating", cat: "mana", max: 1, base: 400, studyTime: 6 },
|
|
||||||
knowledgeRetention: { name: "Knowledge Retention", desc: "+20% study progress saved on cancel", cat: "study", max: 3, base: 350, studyTime: 5 },
|
|
||||||
|
|
||||||
// Enchanting Skills (4-8 hours study) - Requires Enchanter attunement levels
|
|
||||||
enchanting: { name: "Enchanting", desc: "Unlocks enchantment design", cat: "enchant", max: 10, base: 200, studyTime: 5, attunement: 'enchanter', attunementReq: { enchanter: 1 } },
|
|
||||||
efficientEnchant:{ name: "Efficient Enchant", desc: "-5% enchantment capacity cost", cat: "enchant", max: 5, base: 350, studyTime: 6, req: { enchanting: 3 }, attunementReq: { enchanter: 2 } },
|
|
||||||
|
|
||||||
enchantSpeed: { name: "Enchant Speed", desc: "-10% enchantment time", cat: "enchant", max: 5, base: 300, studyTime: 4, req: { enchanting: 2 }, attunementReq: { enchanter: 1 } },
|
|
||||||
essenceRefining: { name: "Essence Refining", desc: "+10% enchantment effect power", cat: "enchant", max: 1, base: 450, studyTime: 7, req: { enchanting: 4 }, attunementReq: { enchanter: 2 } },
|
|
||||||
|
|
||||||
// Crafting Skills (4-6 hours study) - Some require Fabricator
|
|
||||||
effCrafting: { name: "Eff. Crafting", desc: "-10% craft time", cat: "craft", max: 1, base: 300, studyTime: 4 },
|
|
||||||
fieldRepair: { name: "Field Repair", desc: "+15% repair efficiency", cat: "craft", max: 1, base: 350, studyTime: 4 },
|
|
||||||
elemCrafting: { name: "Elem. Crafting", desc: "+25% craft output", cat: "craft", max: 1, base: 500, req: { effCrafting: 1 }, studyTime: 8, attunementReq: { enchanter: 1 } },
|
|
||||||
|
|
||||||
// Effect Research Skills (unlock enchantment effects for designing) - Requires Enchanter
|
|
||||||
// Tier 1 - Basic Spell Effects
|
|
||||||
researchManaSpells: { name: "Mana Spell Research", desc: "Unlock Mana Strike spell enchantment", cat: "effectResearch", max: 1, base: 200, studyTime: 4, req: { enchanting: 1 }, cost: { type: 'element', element: 'transference', amount: 100 }, attunementReq: { enchanter: 1 } },
|
|
||||||
researchFireSpells: { name: "Fire Spell Research", desc: "Unlock Ember Shot, Fireball spell enchantments", cat: "effectResearch", max: 1, base: 300, studyTime: 6, req: { enchanting: 2 }, cost: { type: 'element', element: 'fire', amount: 100 }, attunementReq: { enchanter: 1 } },
|
|
||||||
researchWaterSpells: { name: "Water Spell Research", desc: "Unlock Water Jet, Ice Shard spell enchantments", cat: "effectResearch", max: 1, base: 300, studyTime: 6, req: { enchanting: 2 }, cost: { type: 'element', element: 'water', amount: 100 }, attunementReq: { enchanter: 1 } },
|
|
||||||
researchAirSpells: { name: "Air Spell Research", desc: "Unlock Gust, Wind Slash spell enchantments", cat: "effectResearch", max: 1, base: 300, studyTime: 6, req: { enchanting: 2 }, cost: { type: 'element', element: 'air', amount: 100 }, attunementReq: { enchanter: 1 } },
|
|
||||||
researchEarthSpells: { name: "Earth Spell Research", desc: "Unlock Stone Bullet, Rock Spike spell enchantments", cat: "effectResearch", max: 1, base: 350, studyTime: 6, req: { enchanting: 2 }, cost: { type: 'element', element: 'earth', amount: 100 }, attunementReq: { enchanter: 1 } },
|
|
||||||
researchLightSpells: { name: "Light Spell Research", desc: "Unlock Light Lance, Radiance spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 }, cost: { type: 'element', element: 'light', amount: 100 }, attunementReq: { enchanter: 2 } },
|
|
||||||
researchDarkSpells: { name: "Dark Spell Research", desc: "Unlock Shadow Bolt, Dark Pulse spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 }, cost: { type: 'element', element: 'dark', amount: 100 }, attunementReq: { enchanter: 2 } },
|
|
||||||
researchLifeDeathSpells: { name: "Death Research", desc: "Unlock Drain spell enchantment", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 }, cost: { type: 'element', element: 'death', amount: 100 }, attunementReq: { enchanter: 2 } },
|
|
||||||
|
|
||||||
// Tier 2 - Advanced Spell Effects - Require Enchanter 3
|
|
||||||
researchAdvancedFire: { name: "Advanced Fire Research", desc: "Unlock Inferno, Flame Wave spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchFireSpells: 1, enchanting: 4 }, cost: { type: 'element', element: 'fire', amount: 100 }, attunementReq: { enchanter: 3 } },
|
|
||||||
researchAdvancedWater: { name: "Advanced Water Research", desc: "Unlock Tidal Wave, Ice Storm spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchWaterSpells: 1, enchanting: 4 }, cost: { type: 'element', element: 'water', amount: 100 }, attunementReq: { enchanter: 3 } },
|
|
||||||
researchAdvancedAir: { name: "Advanced Air Research", desc: "Unlock Hurricane, Wind Blade spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchAirSpells: 1, enchanting: 4 }, cost: { type: 'element', element: 'air', amount: 100 }, attunementReq: { enchanter: 3 } },
|
|
||||||
researchAdvancedEarth: { name: "Advanced Earth Research", desc: "Unlock Earthquake, Stone Barrage spell enchantments", cat: "effectResearch", max: 1, base: 600, studyTime: 12, req: { researchEarthSpells: 1, enchanting: 4 }, cost: { type: 'element', element: 'earth', amount: 100 }, attunementReq: { enchanter: 3 } },
|
|
||||||
researchAdvancedLight: { name: "Advanced Light Research", desc: "Unlock Solar Flare, Divine Smite spell enchantments", cat: "effectResearch", max: 1, base: 700, studyTime: 14, req: { researchLightSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'light', amount: 100 }, attunementReq: { enchanter: 4 } },
|
|
||||||
researchAdvancedDark: { name: "Advanced Dark Research", desc: "Unlock Void Rift, Shadow Storm spell enchantments", cat: "effectResearch", max: 1, base: 700, studyTime: 14, req: { researchDarkSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'dark', amount: 100 }, attunementReq: { enchanter: 4 } },
|
|
||||||
|
|
||||||
// Tier 3 - Master Spell Effects - Require Enchanter 5
|
|
||||||
researchMasterFire: { name: "Master Fire Research", desc: "Unlock Pyroclasm spell enchantment", cat: "effectResearch", max: 1, base: 1200, studyTime: 24, req: { researchAdvancedFire: 1, enchanting: 7 }, cost: { type: 'element', element: 'fire', amount: 200 }, attunementReq: { enchanter: 5 } },
|
|
||||||
researchMasterWater: { name: "Master Water Research", desc: "Unlock Tsunami spell enchantment", cat: "effectResearch", max: 1, base: 1200, studyTime: 24, req: { researchAdvancedWater: 1, enchanting: 7 }, cost: { type: 'element', element: 'water', amount: 200 }, attunementReq: { enchanter: 5 } },
|
|
||||||
researchMasterEarth: { name: "Master Earth Research", desc: "Unlock Meteor Strike spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedEarth: 1, enchanting: 8 }, cost: { type: 'element', element: 'earth', amount: 200 }, attunementReq: { enchanter: 5 } },
|
|
||||||
|
|
||||||
// Combat Effect Research
|
|
||||||
researchDamageEffects: { name: "Damage Effect Research", desc: "Unlock Minor/Moderate Power, Amplification effects", cat: "effectResearch", max: 1, base: 250, studyTime: 5, req: { enchanting: 1 }, attunementReq: { enchanter: 1 } },
|
|
||||||
researchCombatEffects: { name: "Combat Effect Research", desc: "Unlock Sharp Edge, Swift Casting effects", cat: "effectResearch", max: 1, base: 350, studyTime: 6, req: { researchDamageEffects: 1, enchanting: 3 }, attunementReq: { enchanter: 2 } },
|
|
||||||
|
|
||||||
// Mana Effect Research - Also unlocks weapon mana effects at Enchanter 3
|
|
||||||
researchManaEffects: { name: "Mana Effect Research", desc: "Unlock Mana Reserve, Trickle, Mana Tap, and weapon mana effects", cat: "effectResearch", max: 1, base: 200, studyTime: 4, req: { enchanting: 1 }, attunementReq: { enchanter: 1 } },
|
|
||||||
researchAdvancedManaEffects: { name: "Advanced Mana Research", desc: "Unlock Mana Reservoir, Stream, River, Mana Surge, and advanced weapon mana effects", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { researchManaEffects: 1, enchanting: 3 }, attunementReq: { enchanter: 3 } },
|
|
||||||
|
|
||||||
// Utility Effect Research
|
|
||||||
researchUtilityEffects: { name: "Utility Effect Research", desc: "Unlock Meditative Focus, Quick Study, Insightful effects", cat: "effectResearch", max: 1, base: 300, studyTime: 6, req: { enchanting: 2 }, attunementReq: { enchanter: 1 } },
|
|
||||||
|
|
||||||
// Special Effect Research
|
|
||||||
researchSpecialEffects: { name: "Special Effect Research", desc: "Unlock Echo Chamber, Siphoning, Bane effects", cat: "effectResearch", max: 1, base: 500, studyTime: 10, req: { enchanting: 4 }, attunementReq: { enchanter: 3 } },
|
|
||||||
researchOverpower: { name: "Overpower Research", desc: "Unlock Overpower effect", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchSpecialEffects: 1, enchanting: 5 }, attunementReq: { enchanter: 4 } },
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// COMPOUND MANA SPELL RESEARCH - Metal, Sand, Lightning
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Tier 1 - Basic Compound Spells
|
|
||||||
researchMetalSpells: { name: "Metal Spell Research", desc: "Unlock Metal Shard, Iron Fist spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 6, req: { researchFireSpells: 1, researchEarthSpells: 1, enchanting: 3 }, cost: { type: 'element', element: 'metal', amount: 100 }, attunementReq: { enchanter: 2 } },
|
|
||||||
researchSandSpells: { name: "Sand Spell Research", desc: "Unlock Sand Blast, Sandstorm spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 6, req: { researchEarthSpells: 1, researchWaterSpells: 1, enchanting: 3 }, cost: { type: 'element', element: 'sand', amount: 100 }, attunementReq: { enchanter: 2 } },
|
|
||||||
researchLightningSpells: { name: "Lightning Spell Research", desc: "Unlock Spark, Lightning Bolt spell enchantments", cat: "effectResearch", max: 1, base: 400, studyTime: 6, req: { researchFireSpells: 1, researchAirSpells: 1, enchanting: 3 }, cost: { type: 'element', element: 'lightning', amount: 100 }, attunementReq: { enchanter: 2 } },
|
|
||||||
|
|
||||||
// Tier 2 - Advanced Compound Spells
|
|
||||||
researchAdvancedMetal: { name: "Advanced Metal Research", desc: "Unlock Steel Tempest spell enchantment", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchMetalSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'metal', amount: 100 }, attunementReq: { enchanter: 3 } },
|
|
||||||
researchAdvancedSand: { name: "Advanced Sand Research", desc: "Unlock Desert Wind spell enchantment", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchSandSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'sand', amount: 100 }, attunementReq: { enchanter: 3 } },
|
|
||||||
researchAdvancedLightning: { name: "Advanced Lightning Research", desc: "Unlock Chain Lightning, Storm Call spell enchantments", cat: "effectResearch", max: 1, base: 700, studyTime: 12, req: { researchLightningSpells: 1, enchanting: 5 }, cost: { type: 'element', element: 'lightning', amount: 100 }, attunementReq: { enchanter: 3 } },
|
|
||||||
|
|
||||||
// Tier 3 - Master Compound Spells
|
|
||||||
researchMasterMetal: { name: "Master Metal Research", desc: "Unlock Furnace Blast spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedMetal: 1, enchanting: 7 }, cost: { type: 'element', element: 'metal', amount: 200 }, attunementReq: { enchanter: 5 } },
|
|
||||||
researchMasterSand: { name: "Master Sand Research", desc: "Unlock Dune Collapse spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedSand: 1, enchanting: 7 }, cost: { type: 'element', element: 'sand', amount: 200 }, attunementReq: { enchanter: 5 } },
|
|
||||||
researchMasterLightning: { name: "Master Lightning Research", desc: "Unlock Thunder Strike spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedLightning: 1, enchanting: 7 }, cost: { type: 'element', element: 'lightning', amount: 200 }, attunementReq: { enchanter: 5 } },
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// UTILITY MANA SPELL RESEARCH - Transference
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Tier 1 - Basic Utility Spells
|
|
||||||
researchTransferenceSpells: { name: "Transference Spell Research", desc: "Unlock Transfer Strike, Mana Rip spell enchantments", cat: "effectResearch", max: 1, base: 350, studyTime: 5, req: { enchanting: 3 }, attunementReq: { enchanter: 1 } },
|
|
||||||
|
|
||||||
// Tier 2 - Advanced Utility Spells
|
|
||||||
researchAdvancedTransference: { name: "Advanced Transference Research", desc: "Unlock Essence Drain spell enchantment", cat: "effectResearch", max: 1, base: 650, studyTime: 12, req: { researchTransferenceSpells: 1, enchanting: 5 }, attunementReq: { enchanter: 3 } },
|
|
||||||
|
|
||||||
// Tier 3 - Master Utility Spells
|
|
||||||
researchMasterTransference: { name: "Master Transference Research", desc: "Unlock Soul Transfer spell enchantment", cat: "effectResearch", max: 1, base: 1300, studyTime: 26, req: { researchAdvancedTransference: 1, enchanting: 7 }, attunementReq: { enchanter: 5 } },
|
|
||||||
|
|
||||||
// Research Skills (longer study times: 12-72 hours) - Core skills, any attunement level 3
|
|
||||||
manaTap: { name: "Mana Tap", desc: "+1 mana/click", cat: "mana", max: 1, base: 300, studyTime: 12 },
|
|
||||||
manaSurge: { name: "Mana Surge", desc: "+3 mana/click", cat: "mana", max: 1, base: 800, studyTime: 36, req: { manaTap: 1 } },
|
|
||||||
manaSpring: { name: "Mana Spring", desc: "+2 mana regen", cat: "mana", max: 1, base: 600, studyTime: 24 },
|
|
||||||
deepTrance: { name: "Deep Trance", desc: "Extend meditation bonus to 6hrs for 3x", cat: "mana", max: 1, base: 900, studyTime: 48, req: { meditation: 1 } },
|
|
||||||
voidMeditation:{ name: "Void Meditation", desc: "Extend meditation bonus to 8hrs for 5x", cat: "mana", max: 1, base: 1500, studyTime: 72, req: { deepTrance: 1 } },
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// INVOKER SKILLS - Require Invoker attunement
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Invocation - Invoker attunement skill
|
|
||||||
invocation: {
|
|
||||||
name: "Invocation",
|
|
||||||
desc: "Enhances spell invocation and guardian pacts",
|
|
||||||
cat: "invocation",
|
|
||||||
attunement: 'invoker',
|
|
||||||
max: 10,
|
|
||||||
base: 300,
|
|
||||||
studyTime: 6,
|
|
||||||
attunementReq: { invoker: 1 }
|
|
||||||
},
|
|
||||||
// Pact Mastery - Invoker attunement skill
|
|
||||||
pactMastery: {
|
|
||||||
name: "Pact Mastery",
|
|
||||||
desc: "Enhances pact signing and guardian bonuses",
|
|
||||||
cat: "pact",
|
|
||||||
attunement: 'invoker',
|
|
||||||
max: 10,
|
|
||||||
base: 350,
|
|
||||||
studyTime: 6,
|
|
||||||
attunementReq: { invoker: 1 }
|
|
||||||
},
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// GOLEMANCY SKILLS - Require Fabricator attunement
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Core Golemancy
|
|
||||||
golemMastery: { name: "Golem Mastery", desc: "+10% golem damage", cat: "golemancy", max: 1, base: 300, studyTime: 6, attunementReq: { fabricator: 2 } },
|
|
||||||
golemEfficiency: { name: "Golem Efficiency", desc: "+5% golem attack speed", cat: "golemancy", max: 1, base: 350, studyTime: 6, attunementReq: { fabricator: 2 } },
|
|
||||||
golemLongevity: { name: "Golem Longevity", desc: "+1 floor duration", cat: "golemancy", max: 1, base: 500, studyTime: 8, attunementReq: { fabricator: 3 } },
|
|
||||||
golemSiphon: { name: "Golem Siphon", desc: "-10% golem maintenance", cat: "golemancy", max: 1, base: 400, studyTime: 8, attunementReq: { fabricator: 3 } },
|
|
||||||
|
|
||||||
// Advanced Golemancy
|
|
||||||
advancedGolemancy: { name: "Advanced Golemancy", desc: "Unlock hybrid golem recipes", cat: "golemancy", max: 1, base: 800, studyTime: 16, req: { golemMastery: 1 }, attunementReq: { fabricator: 5 } },
|
|
||||||
golemResonance: { name: "Golem Resonance", desc: "+1 golem slot at Fabricator 10", cat: "golemancy", max: 1, base: 1200, studyTime: 24, req: { golemMastery: 1 }, attunementReq: { fabricator: 8 } },
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// HYBRID SKILLS - Require TWO attunements at level 5+
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Pact-Weaving (Invoker + Enchanter)
|
|
||||||
pactWeaving: {
|
|
||||||
name: "Pact-Weaving",
|
|
||||||
desc: "Weave Guardian essence into weapon enchantments OR world-effects",
|
|
||||||
cat: "hybrid",
|
|
||||||
max: 10,
|
|
||||||
base: 750,
|
|
||||||
studyTime: 15,
|
|
||||||
attunementReq: { invoker: 5, enchanter: 5 }
|
|
||||||
},
|
|
||||||
|
|
||||||
// Guardian Constructs (Fabricator + Invoker)
|
|
||||||
guardianConstructs: {
|
|
||||||
name: "Guardian Constructs",
|
|
||||||
desc: "Build monumental, singular golems. Only 1 active at a time, vastly more durable, costs less maintenance.",
|
|
||||||
cat: "hybrid",
|
|
||||||
max: 10,
|
|
||||||
base: 800,
|
|
||||||
studyTime: 18,
|
|
||||||
attunementReq: { fabricator: 5, invoker: 5 }
|
|
||||||
},
|
|
||||||
|
|
||||||
// Enchanted Golemancy (Fabricator + Enchanter)
|
|
||||||
enchantedGolemancy: {
|
|
||||||
name: "Enchanted Golemancy",
|
|
||||||
desc: "Imbuing golems with elemental spell logic",
|
|
||||||
cat: "hybrid",
|
|
||||||
max: 10,
|
|
||||||
base: 850,
|
|
||||||
studyTime: 20,
|
|
||||||
attunementReq: { fabricator: 5, enchanter: 5 }
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Skill Categories ─────────────────────────────────────────────────────────
|
|
||||||
// Skills are now organized by attunement - each attunement grants access to specific skill categories
|
|
||||||
export const SKILL_CATEGORIES = [
|
|
||||||
// Core categories (always available)
|
|
||||||
{ id: 'mana', name: 'Mana', icon: '💧', attunement: null },
|
|
||||||
{ id: 'study', name: 'Study', icon: '📚', attunement: null },
|
|
||||||
// Research category moved to Mana (Bug 12)
|
|
||||||
|
|
||||||
// Enchanter attunement (Right Hand)
|
|
||||||
{ id: 'enchant', name: 'Enchanting', icon: '✨', attunement: 'enchanter' },
|
|
||||||
{ id: 'effectResearch', name: 'Effect Research', icon: '🔬', attunement: 'enchanter' },
|
|
||||||
|
|
||||||
// Invoker attunement (Chest)
|
|
||||||
{ id: 'invocation', name: 'Invocation', icon: '💜', attunement: 'invoker' },
|
|
||||||
{ id: 'pact', name: 'Pact Mastery', icon: '🤝', attunement: 'invoker' },
|
|
||||||
|
|
||||||
// Fabricator attunement (Left Hand)
|
|
||||||
{ id: 'fabrication', name: 'Fabrication', icon: '⚒️', attunement: 'fabricator' },
|
|
||||||
{ id: 'golemancy', name: 'Golemancy', icon: '🗿', attunement: 'fabricator' },
|
|
||||||
|
|
||||||
// Legacy category (for backward compatibility)
|
|
||||||
{ id: 'craft', name: 'Crafting', icon: '🔧', attunement: null },
|
|
||||||
];
|
|
||||||
|
|
||||||
// ─── Effect Research Mapping ───────────────────────────────────────────────────
|
|
||||||
// Maps research skill IDs to the effect IDs they unlock
|
|
||||||
export const EFFECT_RESEARCH_MAPPING: Record<string, string[]> = {
|
|
||||||
// Tier 1 - Basic Spell Effects
|
|
||||||
researchManaSpells: ['spell_manaStrike'],
|
|
||||||
researchFireSpells: ['spell_emberShot', 'spell_fireball'],
|
|
||||||
researchWaterSpells: ['spell_waterJet', 'spell_iceShard'],
|
|
||||||
researchAirSpells: ['spell_gust', 'spell_windSlash'],
|
|
||||||
researchEarthSpells: ['spell_stoneBullet', 'spell_rockSpike'],
|
|
||||||
researchLightSpells: ['spell_lightLance', 'spell_radiance'],
|
|
||||||
researchDarkSpells: ['spell_shadowBolt', 'spell_darkPulse'],
|
|
||||||
researchLifeDeathSpells: ['spell_drain'],
|
|
||||||
|
|
||||||
// Tier 2 - Advanced Spell Effects
|
|
||||||
researchAdvancedFire: ['spell_inferno', 'spell_flameWave'],
|
|
||||||
researchAdvancedWater: ['spell_tidalWave', 'spell_iceStorm'],
|
|
||||||
researchAdvancedAir: ['spell_hurricane', 'spell_windBlade'],
|
|
||||||
researchAdvancedEarth: ['spell_earthquake', 'spell_stoneBarrage'],
|
|
||||||
researchAdvancedLight: ['spell_solarFlare', 'spell_divineSmite'],
|
|
||||||
researchAdvancedDark: ['spell_voidRift', 'spell_shadowStorm'],
|
|
||||||
|
|
||||||
// Tier 3 - Master Spell Effects
|
|
||||||
researchMasterFire: ['spell_pyroclasm'],
|
|
||||||
researchMasterWater: ['spell_tsunami'],
|
|
||||||
researchMasterEarth: ['spell_meteorStrike'],
|
|
||||||
|
|
||||||
// Combat Effect Research
|
|
||||||
researchDamageEffects: ['damage_5', 'damage_10', 'damage_pct_10'],
|
|
||||||
researchCombatEffects: ['crit_5', 'attack_speed_10'],
|
|
||||||
|
|
||||||
// Mana Effect Research
|
|
||||||
researchManaEffects: ['mana_cap_50', 'mana_regen_1', 'click_mana_1'],
|
|
||||||
researchAdvancedManaEffects: ['mana_cap_100', 'mana_regen_2', 'mana_regen_5', 'click_mana_3'],
|
|
||||||
|
|
||||||
// Utility Effect Research
|
|
||||||
researchUtilityEffects: ['meditate_10', 'study_10', 'insight_5'],
|
|
||||||
|
|
||||||
// Special Effect Research
|
|
||||||
researchSpecialEffects: ['spell_echo_10', 'guardian_dmg_10'],
|
|
||||||
researchOverpower: ['overpower_80'],
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// COMPOUND MANA SPELL RESEARCH - Metal, Sand, Lightning
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Tier 1 - Basic Compound Spells
|
|
||||||
researchMetalSpells: ['spell_metalShard', 'spell_ironFist'],
|
|
||||||
researchSandSpells: ['spell_sandBlast', 'spell_sandstorm'],
|
|
||||||
researchLightningSpells: ['spell_spark', 'spell_lightningBolt'],
|
|
||||||
|
|
||||||
// Tier 2 - Advanced Compound Spells
|
|
||||||
researchAdvancedMetal: ['spell_steelTempest'],
|
|
||||||
researchAdvancedSand: ['spell_desertWind'],
|
|
||||||
researchAdvancedLightning: ['spell_chainLightning', 'spell_stormCall'],
|
|
||||||
|
|
||||||
// Tier 3 - Master Compound Spells
|
|
||||||
researchMasterMetal: ['spell_furnaceBlast'],
|
|
||||||
researchMasterSand: ['spell_duneCollapse'],
|
|
||||||
researchMasterLightning: ['spell_thunderStrike'],
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// UTILITY MANA SPELL RESEARCH - Transference
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Tier 1 - Basic Utility Spells
|
|
||||||
researchTransferenceSpells: ['spell_transferStrike', 'spell_manaRip'],
|
|
||||||
|
|
||||||
// Tier 2 - Advanced Utility Spells
|
|
||||||
researchAdvancedTransference: ['spell_essenceDrain'],
|
|
||||||
|
|
||||||
// Tier 3 - Master Utility Spells
|
|
||||||
researchMasterTransference: ['spell_soulTransfer'],
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// PER-ELEMENT CAPACITY RESEARCH - Unlocks per-element capacity effects
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Basic Element Capacity Effects (Tier 1 - +10 per stack)
|
|
||||||
researchFireCapacity: ['fire_cap_10'],
|
|
||||||
researchWaterCapacity: ['water_cap_10'],
|
|
||||||
researchAirCapacity: ['air_cap_10'],
|
|
||||||
researchEarthCapacity: ['earth_cap_10'],
|
|
||||||
researchLightCapacity: ['light_cap_10'],
|
|
||||||
researchDarkCapacity: ['dark_cap_10'],
|
|
||||||
researchDeathCapacity: ['death_cap_10'],
|
|
||||||
|
|
||||||
// Advanced Element Capacity Effects (Tier 2 - +25 per stack)
|
|
||||||
researchAdvancedFireCap: ['fire_cap_25'],
|
|
||||||
researchAdvancedWaterCap: ['water_cap_25'],
|
|
||||||
researchAdvancedAirCap: ['air_cap_25'],
|
|
||||||
researchAdvancedEarthCap: ['earth_cap_25'],
|
|
||||||
researchAdvancedLightCap: ['light_cap_25'],
|
|
||||||
researchAdvancedDarkCap: ['dark_cap_25'],
|
|
||||||
researchAdvancedDeathCap: ['death_cap_25'],
|
|
||||||
|
|
||||||
// Master Element Capacity Effects (Tier 3 - +50 per stack)
|
|
||||||
researchMasterFireCap: ['fire_cap_50'],
|
|
||||||
researchMasterWaterCap: ['water_cap_50'],
|
|
||||||
researchMasterAirCap: ['air_cap_50'],
|
|
||||||
researchMasterEarthCap: ['earth_cap_50'],
|
|
||||||
researchMasterLightCap: ['light_cap_50'],
|
|
||||||
researchMasterDarkCap: ['dark_cap_50'],
|
|
||||||
researchMasterDeathCap: ['death_cap_50'],
|
|
||||||
|
|
||||||
// Composite Element Capacity Effects
|
|
||||||
researchMetalCapacity: ['metal_cap_10'],
|
|
||||||
researchAdvancedMetalCap: ['metal_cap_25', 'metal_cap_50'],
|
|
||||||
researchSandCapacity: ['sand_cap_10'],
|
|
||||||
researchAdvancedSandCap: ['sand_cap_25', 'sand_cap_50'],
|
|
||||||
researchLightningCapacity: ['lightning_cap_10'],
|
|
||||||
researchAdvancedLightningCap: ['lightning_cap_25', 'lightning_cap_50'],
|
|
||||||
|
|
||||||
// Exotic Element Capacity Effects
|
|
||||||
researchCrystalCapacity: ['crystal_cap_10'],
|
|
||||||
researchAdvancedCrystalCap: ['crystal_cap_25', 'crystal_cap_50'],
|
|
||||||
researchStellarCapacity: ['stellar_cap_10'],
|
|
||||||
researchAdvancedStellarCap: ['stellar_cap_25', 'stellar_cap_50'],
|
|
||||||
researchVoidCapacity: ['void_cap_10'],
|
|
||||||
researchAdvancedVoidCap: ['void_cap_25', 'void_cap_50'],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Base effects unlocked when player gets enchanting skill level 1
|
|
||||||
export const BASE_UNLOCKED_EFFECTS: string[] = []; // No effects at game start
|
|
||||||
|
|
||||||
// Effects that unlock when getting enchanting skill level 1
|
|
||||||
export const ENCHANTING_UNLOCK_EFFECTS = ['spell_manaBolt'];
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// ─── Skill Upgrade Selection Hook ────────────────────────
|
|
||||||
// Hook for managing milestone upgrade selection state in SkillsTab
|
|
||||||
|
|
||||||
import { useState, useMemo, useCallback, SetStateAction, Dispatch } from 'react';
|
|
||||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
|
||||||
import { getUpgradesForSkillAtMilestone } from '@/lib/game/skill-evolution';
|
|
||||||
|
|
||||||
export interface UseSkillUpgradeSelectionResult {
|
|
||||||
pendingSelections: string[];
|
|
||||||
setPendingSelections: Dispatch<SetStateAction<string[]>>;
|
|
||||||
toggleUpgrade: (upgradeId: string, available: SkillUpgradeChoice[], alreadySelected: string[]) => void;
|
|
||||||
handleConfirm: (upgradeDialogSkill: string | null, upgradeDialogMilestone: 5 | 10, commitSkillUpgrades: (skillId: string, selections: string[], milestone: 5 | 10) => void, onClose: () => void) => void;
|
|
||||||
handleCancel: (onClose: () => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if skill has milestone available (also exported for use in SkillRow/CategorySkillsList)
|
|
||||||
*/
|
|
||||||
export function hasMilestoneUpgrade(
|
|
||||||
skillId: string,
|
|
||||||
level: number,
|
|
||||||
skillTiers: Record<string, number>,
|
|
||||||
skillUpgrades: Record<string, string[]>
|
|
||||||
): { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null {
|
|
||||||
// Check level 5 milestone
|
|
||||||
if (level >= 5) {
|
|
||||||
const upgrades5 = getUpgradesForSkillAtMilestone(skillId, 5, skillTiers);
|
|
||||||
const selected5 = (skillUpgrades[skillId] || []).filter((id) => id.includes('_l5'));
|
|
||||||
if (upgrades5.length > 0 && selected5.length < 2) {
|
|
||||||
return { milestone: 5, hasUpgrades: true, selectedCount: selected5.length };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check level 10 milestone
|
|
||||||
if (level >= 10) {
|
|
||||||
const upgrades10 = getUpgradesForSkillAtMilestone(skillId, 10, skillTiers);
|
|
||||||
const selected10 = (skillUpgrades[skillId] || []).filter((id) => id.includes('_l10'));
|
|
||||||
if (upgrades10.length > 0 && selected10.length < 2) {
|
|
||||||
return { milestone: 10, hasUpgrades: true, selectedCount: selected10.length };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for managing skill upgrade selection state in the SkillsTab milestone upgrade dialog.
|
|
||||||
* Manages pending selections across the dialog open/close cycle.
|
|
||||||
*/
|
|
||||||
export function useSkillUpgradeSelection(): UseSkillUpgradeSelectionResult {
|
|
||||||
const [pendingSelections, setPendingSelections] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const toggleUpgrade = useCallback((upgradeId: string, available: SkillUpgradeChoice[], alreadySelected: string[]) => {
|
|
||||||
const currentSelections = pendingSelections.length > 0 ? pendingSelections : alreadySelected;
|
|
||||||
if (currentSelections.includes(upgradeId)) {
|
|
||||||
setPendingSelections(currentSelections.filter(id => id !== upgradeId));
|
|
||||||
} else if (currentSelections.length < 2) {
|
|
||||||
setPendingSelections([...currentSelections, upgradeId]);
|
|
||||||
}
|
|
||||||
}, [pendingSelections]);
|
|
||||||
|
|
||||||
const handleConfirm = useCallback((upgradeDialogSkill: string | null, upgradeDialogMilestone: 5 | 10, commitSkillUpgrades: (skillId: string, selections: string[], milestone: 5 | 10) => void, onClose: () => void) => {
|
|
||||||
const currentSelections = pendingSelections.length > 0 ? pendingSelections : [];
|
|
||||||
if (currentSelections.length === 2 && upgradeDialogSkill) {
|
|
||||||
commitSkillUpgrades(upgradeDialogSkill, currentSelections, upgradeDialogMilestone);
|
|
||||||
}
|
|
||||||
setPendingSelections([]);
|
|
||||||
onClose();
|
|
||||||
}, [pendingSelections]);
|
|
||||||
|
|
||||||
const handleCancel = useCallback((onClose: () => void) => {
|
|
||||||
setPendingSelections([]);
|
|
||||||
onClose();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return useMemo(() => ({
|
|
||||||
pendingSelections,
|
|
||||||
setPendingSelections,
|
|
||||||
toggleUpgrade,
|
|
||||||
handleConfirm,
|
|
||||||
handleCancel,
|
|
||||||
}), [pendingSelections, toggleUpgrade, handleConfirm, handleCancel]);
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
/**
|
|
||||||
* Ascension and Specialized Skills Tests - skills.test.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
|
||||||
import { calcInsight } from '@/lib/game/store';
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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('✅ Ascension and specialized skills tests defined (from skills.test.ts).');
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mana Skills Tests - skills.test.ts
|
|
||||||
*
|
|
||||||
* Tests for mana-related skills from the old skills.test.ts file
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
computeMaxMana,
|
|
||||||
computeElementMax,
|
|
||||||
computeRegen,
|
|
||||||
computeClickMana,
|
|
||||||
} from '@/lib/game/store';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/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)).toBe(100);
|
|
||||||
expect(computeMaxMana(state1)).toBe(100 + 100);
|
|
||||||
expect(computeMaxMana(state5)).toBe(100 + 500);
|
|
||||||
expect(computeMaxMana(state10)).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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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 state10 = createMockState({ skills: { manaFlow: 10 } });
|
|
||||||
|
|
||||||
// With enchanter attunement giving +0.5 regen, base is 2.5
|
|
||||||
const baseRegen = computeRegen(state0);
|
|
||||||
expect(baseRegen).toBeGreaterThan(2); // Has attunement bonus
|
|
||||||
expect(computeRegen(state1)).toBe(baseRegen + 1);
|
|
||||||
expect(computeRegen(state5)).toBe(baseRegen + 5);
|
|
||||||
expect(computeRegen(state10)).toBe(baseRegen + 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 baseRegen = computeRegen(state0);
|
|
||||||
expect(baseRegen).toBeGreaterThan(2); // Has attunement bonus
|
|
||||||
expect(computeRegen(state1)).toBe(baseRegen + 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 } });
|
|
||||||
const state10 = createMockState({ skills: { elemAttune: 10 } });
|
|
||||||
|
|
||||||
expect(computeElementMax(state0)).toBe(10);
|
|
||||||
expect(computeElementMax(state1)).toBe(10 + 50);
|
|
||||||
expect(computeElementMax(state5)).toBe(10 + 250);
|
|
||||||
expect(computeElementMax(state10)).toBe(10 + 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
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)).toBe(1);
|
|
||||||
expect(computeClickMana(state1)).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 state0 = createMockState({ skills: { manaSurge: 0 } });
|
|
||||||
const state1 = createMockState({ skills: { manaSurge: 1 } });
|
|
||||||
|
|
||||||
expect(computeClickMana(state0)).toBe(1);
|
|
||||||
expect(computeClickMana(state1)).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack with Mana Tap', () => {
|
|
||||||
const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } });
|
|
||||||
expect(computeClickMana(state)).toBe(1 + 1 + 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require Mana Tap 1', () => {
|
|
||||||
expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Mana skills tests defined (from skills.test.ts).');
|
|
||||||
-212
@@ -1,212 +0,0 @@
|
|||||||
/**
|
|
||||||
* Skill Prerequisites, Study Times, Prestige Upgrades, and Integration Tests - skills.test.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SKILLS_DEF, PRESTIGE_DEF } from '@/lib/game/constants';
|
|
||||||
import { computeMaxMana, computeElementMax } from '@/lib/game/store';
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 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)).toBe(100);
|
|
||||||
expect(computeMaxMana(state1)).toBe(100 + 500);
|
|
||||||
expect(computeMaxMana(state5)).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)).toBe(10);
|
|
||||||
expect(computeElementMax(state1)).toBe(10 + 25);
|
|
||||||
expect(computeElementMax(state10)).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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Skill prerequisites, study times, prestige, and integration tests defined (from skills.test.ts).');
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
/**
|
|
||||||
* Study Skills Tests - skills.test.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
getStudySpeedMultiplier,
|
|
||||||
getStudyCostMultiplier,
|
|
||||||
getMeditationBonus,
|
|
||||||
} from '@/lib/game/store';
|
|
||||||
import { SKILLS_DEF } from '@/lib/game/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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── 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 (from skills.test.ts).');
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* Skills Tests (skills.test.ts) - Main Index
|
|
||||||
*
|
|
||||||
* This file re-exports all individual test files from the original skills.test.ts
|
|
||||||
* Each test file is focused on a specific area of functionality.
|
|
||||||
*
|
|
||||||
* Original file: skills.test.ts (543 lines)
|
|
||||||
* Refactored into 4 smaller test files.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import './skills-split-tests/mana-skills.test';
|
|
||||||
import './skills-split-tests/study-skills.test';
|
|
||||||
import './skills-split-tests/ascension-specialized-skills.test';
|
|
||||||
import './skills-split-tests/prerequisites-studytimes-prestige-integration.test';
|
|
||||||
|
|
||||||
console.log('✅ All skills tests from skills.test.ts complete (refactored from 543 lines to 4 focused test files).');
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Damage Calculation
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { calcDamage } from '../store';
|
|
||||||
import { createMockState } from './test-utils';
|
|
||||||
import { GUARDIANS } from '../constants';
|
|
||||||
|
|
||||||
describe('Damage Calculation', () => {
|
|
||||||
describe('calcDamage', () => {
|
|
||||||
it('should return spell base damage with no bonuses', () => {
|
|
||||||
const state = createMockState();
|
|
||||||
const dmg = calcDamage(state, 'manaBolt');
|
|
||||||
expect(dmg).toBeGreaterThanOrEqual(5); // Base damage
|
|
||||||
expect(dmg).toBeLessThanOrEqual(5 * 1.5); // No crit bonus, just base
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should multiply by signed pacts', () => {
|
|
||||||
const state = createMockState({ signedPacts: [10] });
|
|
||||||
// Pact multiplier is 1.5 for floor 10
|
|
||||||
const dmg = calcDamage(state, 'manaBolt');
|
|
||||||
const minDmg = 5 * 1.5;
|
|
||||||
expect(dmg).toBeGreaterThanOrEqual(minDmg);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack multiple pacts', () => {
|
|
||||||
const state = createMockState({ signedPacts: [10, 20] });
|
|
||||||
const pactMult = GUARDIANS[10].pact * GUARDIANS[20].pact;
|
|
||||||
const dmg = calcDamage(state, 'manaBolt');
|
|
||||||
const minDmg = 5 * pactMult;
|
|
||||||
expect(dmg).toBeGreaterThanOrEqual(minDmg);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Elemental bonuses', () => {
|
|
||||||
it('should give +25% for same element', () => {
|
|
||||||
const state = createMockState({ spells: { fireball: { learned: true, level: 1 } } });
|
|
||||||
// Floor 1 is fire element
|
|
||||||
const dmg = calcDamage(state, 'fireball', 'fire');
|
|
||||||
// Without crit: 15 * 1.25
|
|
||||||
expect(dmg).toBeGreaterThanOrEqual(15 * 1.25);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should give +50% for opposing element (super effective)', () => {
|
|
||||||
const state = createMockState({ spells: { waterJet: { learned: true, level: 1 } } });
|
|
||||||
// Water vs fire - water is the opposite of fire, so water is super effective
|
|
||||||
const dmg = calcDamage(state, 'waterJet', 'fire');
|
|
||||||
// Base 12 * 1.5 = 18 (without crit)
|
|
||||||
expect(dmg).toBeGreaterThanOrEqual(18 * 0.5); // Can crit, so min is lower
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should give +50% when attacking opposite element (fire vs water)', () => {
|
|
||||||
const state = createMockState({ spells: { fireball: { learned: true, level: 1 } } });
|
|
||||||
// Fire vs water - fire is the opposite of water, so fire is super effective
|
|
||||||
const dmg = calcDamage(state, 'fireball', 'water');
|
|
||||||
// Base 15 * 1.5 = 22.5 (without crit)
|
|
||||||
expect(dmg).toBeGreaterThanOrEqual(22.5 * 0.5); // Can crit
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be neutral for non-opposing elements', () => {
|
|
||||||
const state = createMockState({ spells: { fireball: { learned: true, level: 1 } } });
|
|
||||||
// Fire vs air (neutral - neither same nor opposite)
|
|
||||||
const dmg = calcDamage(state, 'fireball', 'air');
|
|
||||||
expect(dmg).toBeGreaterThanOrEqual(15 * 0.5); // No bonus, but could crit
|
|
||||||
expect(dmg).toBeLessThanOrEqual(15 * 1.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Element Crafting Recipes
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { ELEMENTS } from '../constants';
|
|
||||||
|
|
||||||
describe('Element Crafting Recipes', () => {
|
|
||||||
it('should have valid ingredient references', () => {
|
|
||||||
Object.entries(ELEMENTS).forEach(([id, def]) => {
|
|
||||||
if (def.recipe) {
|
|
||||||
def.recipe.forEach(ingredient => {
|
|
||||||
expect(ELEMENTS[ingredient]).toBeDefined();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not have circular recipes', () => {
|
|
||||||
const visited = new Set<string>();
|
|
||||||
const checkCircular = (id: string, path: string[]): boolean => {
|
|
||||||
if (path.includes(id)) return true;
|
|
||||||
const def = ELEMENTS[id];
|
|
||||||
if (!def.recipe) return false;
|
|
||||||
return def.recipe.some(ing => checkCircular(ing, [...path, id]));
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(ELEMENTS).forEach(id => {
|
|
||||||
expect(checkCircular(id, [])).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Floor Functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { getFloorMaxHP, getFloorElement } from '../store';
|
|
||||||
import { GUARDIANS, FLOOR_ELEM_CYCLE } from '../constants';
|
|
||||||
|
|
||||||
describe('Floor Functions', () => {
|
|
||||||
describe('getFloorMaxHP', () => {
|
|
||||||
it('should return guardian HP for guardian floors', () => {
|
|
||||||
expect(getFloorMaxHP(10)).toBe(GUARDIANS[10].hp);
|
|
||||||
expect(getFloorMaxHP(100)).toBe(GUARDIANS[100].hp);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should scale HP for non-guardian floors', () => {
|
|
||||||
expect(getFloorMaxHP(1)).toBeGreaterThan(0);
|
|
||||||
expect(getFloorMaxHP(5)).toBeGreaterThan(getFloorMaxHP(1));
|
|
||||||
expect(getFloorMaxHP(50)).toBeGreaterThan(getFloorMaxHP(25));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have increasing scaling', () => {
|
|
||||||
const hp1 = getFloorMaxHP(1);
|
|
||||||
const hp5 = getFloorMaxHP(5);
|
|
||||||
const hp10 = getFloorMaxHP(10); // Guardian floor
|
|
||||||
const hp50 = getFloorMaxHP(50); // Guardian floor
|
|
||||||
|
|
||||||
// HP should increase
|
|
||||||
expect(hp5).toBeGreaterThan(hp1);
|
|
||||||
expect(hp10).toBeGreaterThan(hp5);
|
|
||||||
expect(hp50).toBeGreaterThan(hp10);
|
|
||||||
|
|
||||||
// Guardian floors have much more HP
|
|
||||||
expect(hp10).toBeGreaterThan(1000);
|
|
||||||
expect(hp50).toBeGreaterThan(10000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getFloorElement', () => {
|
|
||||||
it('should cycle through elements in order (7 elements)', () => {
|
|
||||||
// FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "death"]
|
|
||||||
expect(getFloorElement(1)).toBe('fire');
|
|
||||||
expect(getFloorElement(2)).toBe('water');
|
|
||||||
expect(getFloorElement(3)).toBe('air');
|
|
||||||
expect(getFloorElement(4)).toBe('earth');
|
|
||||||
expect(getFloorElement(5)).toBe('light');
|
|
||||||
expect(getFloorElement(6)).toBe('dark');
|
|
||||||
expect(getFloorElement(7)).toBe('death');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should wrap around after 7 floors', () => {
|
|
||||||
expect(getFloorElement(8)).toBe('fire');
|
|
||||||
expect(getFloorElement(9)).toBe('water');
|
|
||||||
expect(getFloorElement(15)).toBe('fire'); // (15-1) % 7 = 0 -> fire
|
|
||||||
expect(getFloorElement(16)).toBe('water'); // (16-1) % 7 = 1 -> water
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Formatting Functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { fmt, fmtDec } from '../store';
|
|
||||||
|
|
||||||
describe('Formatting Functions', () => {
|
|
||||||
describe('fmt (format number)', () => {
|
|
||||||
it('should format numbers less than 1000 as integers', () => {
|
|
||||||
expect(fmt(0)).toBe('0');
|
|
||||||
expect(fmt(1)).toBe('1');
|
|
||||||
expect(fmt(999)).toBe('999');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format thousands with K suffix', () => {
|
|
||||||
expect(fmt(1000)).toBe('1.0K');
|
|
||||||
expect(fmt(1500)).toBe('1.5K');
|
|
||||||
expect(fmt(999999)).toBe('1000.0K');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format millions with M suffix', () => {
|
|
||||||
expect(fmt(1000000)).toBe('1.00M');
|
|
||||||
expect(fmt(1500000)).toBe('1.50M');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format billions with B suffix', () => {
|
|
||||||
expect(fmt(1000000000)).toBe('1.00B');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle non-finite numbers', () => {
|
|
||||||
expect(fmt(Infinity)).toBe('0');
|
|
||||||
expect(fmt(NaN)).toBe('0');
|
|
||||||
expect(fmt(-Infinity)).toBe('0');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fmtDec (format decimal)', () => {
|
|
||||||
it('should format numbers with specified decimal places', () => {
|
|
||||||
expect(fmtDec(1.234, 2)).toBe('1.23');
|
|
||||||
expect(fmtDec(1.5, 0)).toBe('2'); // toFixed rounds
|
|
||||||
expect(fmtDec(1.567, 1)).toBe('1.6');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle non-finite numbers', () => {
|
|
||||||
expect(fmtDec(Infinity, 2)).toBe('0');
|
|
||||||
expect(fmtDec(NaN, 2)).toBe('0');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Game Constants
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF } from '../constants';
|
|
||||||
|
|
||||||
describe('Game Constants', () => {
|
|
||||||
describe('ELEMENTS', () => {
|
|
||||||
it('should have all base elements', () => {
|
|
||||||
// Life, blood, wood were removed - we have 7 base elements now
|
|
||||||
expect(ELEMENTS.fire).toBeDefined();
|
|
||||||
expect(ELEMENTS.water).toBeDefined();
|
|
||||||
expect(ELEMENTS.air).toBeDefined();
|
|
||||||
expect(ELEMENTS.earth).toBeDefined();
|
|
||||||
expect(ELEMENTS.light).toBeDefined();
|
|
||||||
expect(ELEMENTS.dark).toBeDefined();
|
|
||||||
expect(ELEMENTS.death).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have composite elements with recipes', () => {
|
|
||||||
// blood and wood were removed
|
|
||||||
expect(ELEMENTS.metal.recipe).toEqual(['fire', 'earth']);
|
|
||||||
expect(ELEMENTS.sand.recipe).toEqual(['earth', 'water']);
|
|
||||||
expect(ELEMENTS.lightning.recipe).toEqual(['fire', 'air']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have exotic elements with 3-ingredient recipes', () => {
|
|
||||||
expect(ELEMENTS.crystal.recipe).toHaveLength(3);
|
|
||||||
expect(ELEMENTS.stellar.recipe).toHaveLength(3);
|
|
||||||
expect(ELEMENTS.void.recipe).toHaveLength(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have utility element transference', () => {
|
|
||||||
expect(ELEMENTS.transference).toBeDefined();
|
|
||||||
expect(ELEMENTS.transference.cat).toBe('utility');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GUARDIANS', () => {
|
|
||||||
it('should have guardians on expected floors', () => {
|
|
||||||
// Note: Floor 70 was removed
|
|
||||||
[10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => {
|
|
||||||
expect(GUARDIANS[floor]).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have increasing HP', () => {
|
|
||||||
let prevHP = 0;
|
|
||||||
[10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => {
|
|
||||||
expect(GUARDIANS[floor].hp).toBeGreaterThan(prevHP);
|
|
||||||
prevHP = GUARDIANS[floor].hp;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have increasing pact multipliers', () => {
|
|
||||||
let prevPact = 1;
|
|
||||||
[10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => {
|
|
||||||
expect(GUARDIANS[floor].pact).toBeGreaterThan(prevPact);
|
|
||||||
prevPact = GUARDIANS[floor].pact;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SPELLS_DEF', () => {
|
|
||||||
it('should have manaBolt as a basic spell', () => {
|
|
||||||
expect(SPELLS_DEF.manaBolt).toBeDefined();
|
|
||||||
expect(SPELLS_DEF.manaBolt.tier).toBe(0);
|
|
||||||
expect(SPELLS_DEF.manaBolt.cost.type).toBe('raw');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have spells for existing base elements', () => {
|
|
||||||
// Life was removed, death is present
|
|
||||||
const elements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'];
|
|
||||||
elements.forEach(elem => {
|
|
||||||
const hasSpell = Object.values(SPELLS_DEF).some(s => s.elem === elem);
|
|
||||||
expect(hasSpell).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have increasing damage for higher tiers', () => {
|
|
||||||
const tier0Avg = Object.values(SPELLS_DEF).filter(s => s.tier === 0).reduce((a, s) => a + s.dmg, 0);
|
|
||||||
const tier1Avg = Object.values(SPELLS_DEF).filter(s => s.tier === 1).reduce((a, s) => a + s.dmg, 0);
|
|
||||||
const tier2Avg = Object.values(SPELLS_DEF).filter(s => s.tier === 2).reduce((a, s) => a + s.dmg, 0);
|
|
||||||
|
|
||||||
expect(tier1Avg).toBeGreaterThan(tier0Avg);
|
|
||||||
expect(tier2Avg).toBeGreaterThan(tier1Avg);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SKILLS_DEF', () => {
|
|
||||||
it('should have skills with valid categories', () => {
|
|
||||||
const validCategories = ['mana', 'study', 'research', 'ascension', 'enchant', 'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'craft', 'hybrid'];
|
|
||||||
Object.values(SKILLS_DEF).forEach(skill => {
|
|
||||||
expect(validCategories).toContain(skill.cat);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have reasonable study times', () => {
|
|
||||||
Object.values(SKILLS_DEF).forEach(skill => {
|
|
||||||
expect(skill.studyTime).toBeGreaterThan(0);
|
|
||||||
expect(skill.studyTime).toBeLessThanOrEqual(72);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PRESTIGE_DEF', () => {
|
|
||||||
it('should have prestige upgrades with valid costs', () => {
|
|
||||||
Object.values(PRESTIGE_DEF).forEach(def => {
|
|
||||||
expect(def.cost).toBeGreaterThan(0);
|
|
||||||
expect(def.max).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Individual Skills (Current System)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
computeMaxMana,
|
|
||||||
computeElementMax,
|
|
||||||
computeRegen,
|
|
||||||
computeClickMana,
|
|
||||||
calcDamage,
|
|
||||||
calcInsight,
|
|
||||||
getMeditationBonus
|
|
||||||
} from '../store';
|
|
||||||
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '../constants';
|
|
||||||
import { createMockState } from './test-utils';
|
|
||||||
import { SKILLS_DEF, SPELLS_DEF } from '../constants';
|
|
||||||
import { SKILL_EVOLUTION_PATHS } from '../skill-evolution';
|
|
||||||
|
|
||||||
describe('Individual Skill Tests', () => {
|
|
||||||
|
|
||||||
// ─── Mana Skills ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('manaWell', () => {
|
|
||||||
it('should add +100 max mana per level', () => {
|
|
||||||
const state1 = createMockState({ skills: { manaWell: 1 } });
|
|
||||||
expect(computeMaxMana(state1)).toBe(100 + 100);
|
|
||||||
|
|
||||||
const state5 = createMockState({ skills: { manaWell: 5 } });
|
|
||||||
expect(computeMaxMana(state5)).toBe(100 + 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack with prestige manaWell', () => {
|
|
||||||
const state = createMockState({
|
|
||||||
skills: { manaWell: 3 },
|
|
||||||
prestigeUpgrades: { manaWell: 2 }
|
|
||||||
});
|
|
||||||
expect(computeMaxMana(state)).toBe(100 + 300 + 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have evolution path to Deep Reservoir at tier 2', () => {
|
|
||||||
const tier2 = SKILL_EVOLUTION_PATHS.manaWell.tiers.find((t: any) => t.tier === 2);
|
|
||||||
expect(tier2).toBeDefined();
|
|
||||||
expect(tier2?.name).toBe('Deep Reservoir');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('manaFlow', () => {
|
|
||||||
it('should add +1 regen/hr per level', () => {
|
|
||||||
// Base regen is 2 + enchanter 0.5 = 2.5
|
|
||||||
const state0 = createMockState();
|
|
||||||
expect(computeRegen(state0)).toBe(2.5);
|
|
||||||
|
|
||||||
const state3 = createMockState({ skills: { manaFlow: 3 } });
|
|
||||||
expect(computeRegen(state3)).toBe(2 + 0.5 + 3);
|
|
||||||
|
|
||||||
const state10 = createMockState({ skills: { manaFlow: 10 } });
|
|
||||||
expect(computeRegen(state10)).toBe(2 + 0.5 + 10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('elemAttune', () => {
|
|
||||||
it('should add +50 elem mana cap per level', () => {
|
|
||||||
const state0 = createMockState();
|
|
||||||
expect(computeElementMax(state0)).toBe(10);
|
|
||||||
|
|
||||||
const state3 = createMockState({ skills: { elemAttune: 3 } });
|
|
||||||
expect(computeElementMax(state3)).toBe(10 + 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('manaOverflow', () => {
|
|
||||||
it('should be defined with correct properties', () => {
|
|
||||||
expect(SKILLS_DEF.manaOverflow).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.manaOverflow.max).toBe(5);
|
|
||||||
expect(SKILLS_DEF.manaOverflow.desc).toContain('click');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require Mana Well 3', () => {
|
|
||||||
expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Study Skills ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('quickLearner', () => {
|
|
||||||
it('should add +10% study speed per level', () => {
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 0 })).toBe(1);
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 1 })).toBe(1.1);
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 5 })).toBe(1.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('focusedMind', () => {
|
|
||||||
it('should reduce study mana cost by 5% per level', () => {
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 0 })).toBe(1);
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 1 })).toBe(0.95);
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 5 })).toBe(0.75);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reduce skill study cost', () => {
|
|
||||||
const baseCost = SKILLS_DEF.manaWell.base;
|
|
||||||
const costMult5 = getStudyCostMultiplier({ focusedMind: 5 });
|
|
||||||
const reducedCost = Math.floor(baseCost * costMult5);
|
|
||||||
expect(reducedCost).toBe(75);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reduce spell study cost', () => {
|
|
||||||
const baseCost = SPELLS_DEF.fireball.unlock;
|
|
||||||
const costMult5 = getStudyCostMultiplier({ focusedMind: 5 });
|
|
||||||
const reducedCost = Math.floor(baseCost * costMult5);
|
|
||||||
expect(reducedCost).toBe(75);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('meditation', () => {
|
|
||||||
it('should give 2.5x regen after 4 hours meditating', () => {
|
|
||||||
const bonus = getMeditationBonus(100, { meditation: 1 }); // 100 ticks = 4 hours
|
|
||||||
expect(bonus).toBe(2.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not give bonus without enough time', () => {
|
|
||||||
const bonus = getMeditationBonus(50, { meditation: 1 }); // 2 hours
|
|
||||||
expect(bonus).toBeLessThan(2.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('knowledgeRetention', () => {
|
|
||||||
it('should save +20% study progress on cancel per level', () => {
|
|
||||||
expect(SKILLS_DEF.knowledgeRetention).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.knowledgeRetention.max).toBe(3);
|
|
||||||
expect(SKILLS_DEF.knowledgeRetention.desc).toContain('20% study progress saved');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Research Skills ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('manaTap', () => {
|
|
||||||
it('should add +1 mana per click', () => {
|
|
||||||
const state0 = createMockState();
|
|
||||||
expect(computeClickMana(state0)).toBe(1);
|
|
||||||
|
|
||||||
const state1 = createMockState({ skills: { manaTap: 1 } });
|
|
||||||
expect(computeClickMana(state1)).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('manaSurge', () => {
|
|
||||||
it('should add +3 mana per click', () => {
|
|
||||||
const state = createMockState({ skills: { manaSurge: 1 } });
|
|
||||||
expect(computeClickMana(state)).toBe(1 + 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack with manaTap', () => {
|
|
||||||
const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } });
|
|
||||||
expect(computeClickMana(state)).toBe(1 + 1 + 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require manaTap', () => {
|
|
||||||
expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('manaSpring', () => {
|
|
||||||
it('should add +2 mana regen', () => {
|
|
||||||
// Base 2 + enchanter 0.5 + manaSpring 2
|
|
||||||
const state = createMockState({ skills: { manaSpring: 1 } });
|
|
||||||
expect(computeRegen(state)).toBe(2 + 0.5 + 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deepTrance', () => {
|
|
||||||
it('should extend meditation bonus to 6hrs for 3x', () => {
|
|
||||||
const bonus = getMeditationBonus(150, { meditation: 1, deepTrance: 1 }); // 6 hours
|
|
||||||
expect(bonus).toBe(3.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require meditation', () => {
|
|
||||||
expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('voidMeditation', () => {
|
|
||||||
it('should extend meditation bonus to 8hrs for 5x', () => {
|
|
||||||
const bonus = getMeditationBonus(200, { meditation: 1, deepTrance: 1, voidMeditation: 1 }); // 8 hours
|
|
||||||
expect(bonus).toBe(5.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require deepTrance', () => {
|
|
||||||
expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Ascension Skills ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('insightHarvest', () => {
|
|
||||||
it('should add +10% insight gain per level', () => {
|
|
||||||
const state0 = createMockState({ maxFloorReached: 10 });
|
|
||||||
const insight0 = calcInsight(state0);
|
|
||||||
|
|
||||||
const state3 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 3 } });
|
|
||||||
const insight3 = calcInsight(state3);
|
|
||||||
|
|
||||||
// Level 3 = 1.3x insight
|
|
||||||
expect(insight3).toBe(Math.floor(insight0 * 1.3));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('guardianBane', () => {
|
|
||||||
it('should have evolution path in skill evolution system', () => {
|
|
||||||
expect(SKILL_EVOLUTION_PATHS.guardianBane).toBeDefined();
|
|
||||||
expect(SKILL_EVOLUTION_PATHS.guardianBane.baseSkillId).toBe('guardianBane');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Enchanter Skills ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('enchanting', () => {
|
|
||||||
it('should require enchanter attunement', () => {
|
|
||||||
expect(SKILLS_DEF.enchanting).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.enchanting.attunement).toBe('enchanter');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('efficientEnchant', () => {
|
|
||||||
it('should require enchanting 3', () => {
|
|
||||||
expect(SKILLS_DEF.efficientEnchant).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Fabricator/Golemancy Skills ────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('golemMastery', () => {
|
|
||||||
it('should be defined with correct properties', () => {
|
|
||||||
expect(SKILLS_DEF.golemMastery).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.golemMastery.max).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('golemEfficiency', () => {
|
|
||||||
it('should be defined with correct properties', () => {
|
|
||||||
expect(SKILLS_DEF.golemEfficiency).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.golemEfficiency.max).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Crafting Skills ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('effCrafting', () => {
|
|
||||||
it('should reduce craft time by 10% per level', () => {
|
|
||||||
expect(SKILLS_DEF.effCrafting).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.effCrafting.max).toBe(1);
|
|
||||||
expect(SKILLS_DEF.effCrafting.desc).toContain('10% craft time');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fieldRepair', () => {
|
|
||||||
it('should be defined with correct properties', () => {
|
|
||||||
expect(SKILLS_DEF.fieldRepair).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.fieldRepair.max).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('elemCrafting', () => {
|
|
||||||
it('should add +25% craft output per level', () => {
|
|
||||||
expect(SKILLS_DEF.elemCrafting).toBeDefined();
|
|
||||||
expect(SKILLS_DEF.elemCrafting.max).toBe(1);
|
|
||||||
expect(SKILLS_DEF.elemCrafting.desc).toContain('25% craft output');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Insight, Meditation, and Incursion
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { calcInsight, getMeditationBonus, getIncursionStrength } from '../store';
|
|
||||||
import { createMockState } from './test-utils';
|
|
||||||
import { MAX_DAY, INCURSION_START_DAY } from '../constants';
|
|
||||||
|
|
||||||
describe('Insight Calculation', () => {
|
|
||||||
describe('calcInsight', () => {
|
|
||||||
it('should calculate insight from floor progress', () => {
|
|
||||||
const state = createMockState({ maxFloorReached: 10 });
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
expect(insight).toBe(10 * 15);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should calculate insight from mana gathered', () => {
|
|
||||||
const state = createMockState({ totalManaGathered: 5000 });
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
// Formula: floor*15 + mana/500 + pacts*150
|
|
||||||
// With default maxFloorReached=1: 1*15 + 5000/500 + 0 = 15 + 10 = 25
|
|
||||||
expect(insight).toBe(25);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should calculate insight from signed pacts', () => {
|
|
||||||
const state = createMockState({ signedPacts: [10, 20] });
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
// Formula: floor*15 + mana/500 + pacts*150
|
|
||||||
// With default maxFloorReached=1: 1*15 + 0 + 2*150 = 15 + 300 = 315
|
|
||||||
expect(insight).toBe(315);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should multiply by insightAmp prestige', () => {
|
|
||||||
const state = createMockState({
|
|
||||||
maxFloorReached: 10,
|
|
||||||
prestigeUpgrades: { insightAmp: 2 },
|
|
||||||
});
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
expect(insight).toBe(Math.floor(10 * 15 * 1.5));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should multiply by insightHarvest skill', () => {
|
|
||||||
const state = createMockState({
|
|
||||||
maxFloorReached: 10,
|
|
||||||
skills: { insightHarvest: 3 },
|
|
||||||
});
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
expect(insight).toBe(Math.floor(10 * 15 * 1.3));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Meditation Bonus', () => {
|
|
||||||
describe('getMeditationBonus', () => {
|
|
||||||
it('should start at 1x with no meditation', () => {
|
|
||||||
expect(getMeditationBonus(0, {})).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ramp up over time', () => {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Incursion Strength', () => {
|
|
||||||
describe('getIncursionStrength', () => {
|
|
||||||
it('should be 0 before incursion start day', () => {
|
|
||||||
expect(getIncursionStrength(19, 0)).toBe(0);
|
|
||||||
expect(getIncursionStrength(19, 23)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start at incursion start day', () => {
|
|
||||||
// Incursion starts at day 20, hour 0
|
|
||||||
// Formula: totalHours / maxHours * 0.95
|
|
||||||
// At day 20, hour 0: totalHours = 0, so strength = 0
|
|
||||||
// Need hour > 0 to see incursion
|
|
||||||
expect(getIncursionStrength(INCURSION_START_DAY, 1)).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should increase over time', () => {
|
|
||||||
const early = getIncursionStrength(INCURSION_START_DAY, 12);
|
|
||||||
const late = getIncursionStrength(25, 12);
|
|
||||||
expect(late).toBeGreaterThan(early);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should cap at 95%', () => {
|
|
||||||
const strength = getIncursionStrength(MAX_DAY, 23);
|
|
||||||
expect(strength).toBeLessThanOrEqual(0.95);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* Integration Tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SPELLS_DEF, GUARDIANS, ELEMENTS, SKILLS_DEF } from '../constants';
|
|
||||||
|
|
||||||
describe('Integration Tests', () => {
|
|
||||||
it('should have consistent element references across all definitions', () => {
|
|
||||||
// All spell elements should exist
|
|
||||||
Object.values(SPELLS_DEF).forEach(spell => {
|
|
||||||
if (spell.elem !== 'raw') {
|
|
||||||
expect(ELEMENTS[spell.elem]).toBeDefined();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// All guardian elements should exist
|
|
||||||
Object.values(GUARDIANS).forEach(guardian => {
|
|
||||||
expect(ELEMENTS[guardian.element]).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have balanced spell costs relative to damage', () => {
|
|
||||||
Object.values(SPELLS_DEF).forEach(spell => {
|
|
||||||
const dmgPerCost = spell.dmg / spell.cost.amount;
|
|
||||||
// Damage per mana should be reasonable (between 0.5 and 50)
|
|
||||||
expect(dmgPerCost).toBeGreaterThan(0.5);
|
|
||||||
expect(dmgPerCost).toBeLessThan(50);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have balanced skill requirements', () => {
|
|
||||||
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
|
|
||||||
if (skill.req) {
|
|
||||||
Object.entries(skill.req).forEach(([reqId, level]) => {
|
|
||||||
expect(SKILLS_DEF[reqId]).toBeDefined();
|
|
||||||
expect(level).toBeGreaterThan(0);
|
|
||||||
expect(level).toBeLessThanOrEqual(SKILLS_DEF[reqId].max);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Mana Calculation Functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { computeMaxMana, computeElementMax, computeRegen, computeClickMana } from '../store';
|
|
||||||
import { createMockState } from './test-utils';
|
|
||||||
|
|
||||||
describe('Mana Calculation Functions', () => {
|
|
||||||
describe('computeMaxMana', () => {
|
|
||||||
it('should return base mana with no upgrades', () => {
|
|
||||||
const state = createMockState();
|
|
||||||
expect(computeMaxMana(state)).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add mana from manaWell skill', () => {
|
|
||||||
const state = createMockState({ skills: { manaWell: 5 } });
|
|
||||||
expect(computeMaxMana(state)).toBe(100 + 5 * 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add mana from prestige upgrades', () => {
|
|
||||||
const state = createMockState({ prestigeUpgrades: { manaWell: 3 } });
|
|
||||||
expect(computeMaxMana(state)).toBe(100 + 3 * 500);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('computeElementMax', () => {
|
|
||||||
it('should return base element cap with no upgrades', () => {
|
|
||||||
const state = createMockState();
|
|
||||||
expect(computeElementMax(state)).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add cap from elemAttune skill', () => {
|
|
||||||
const state = createMockState({ skills: { elemAttune: 5 } });
|
|
||||||
expect(computeElementMax(state)).toBe(10 + 5 * 50);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add cap from prestige upgrades', () => {
|
|
||||||
const state = createMockState({ prestigeUpgrades: { elementalAttune: 3 } });
|
|
||||||
expect(computeElementMax(state)).toBe(10 + 3 * 25);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('computeRegen', () => {
|
|
||||||
it('should return base regen with no upgrades', () => {
|
|
||||||
// Base regen is 2, but Enchanter attunement adds 0.5
|
|
||||||
const state = createMockState();
|
|
||||||
expect(computeRegen(state)).toBe(2.5); // 2 + 0.5 from enchanter
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add regen from manaFlow skill', () => {
|
|
||||||
// Base 2 + enchanter 0.5 + manaFlow 5
|
|
||||||
const state = createMockState({ skills: { manaFlow: 5 } });
|
|
||||||
expect(computeRegen(state)).toBe(2 + 0.5 + 5 * 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add regen from manaSpring skill', () => {
|
|
||||||
// Base 2 + enchanter 0.5 + manaSpring 2
|
|
||||||
const state = createMockState({ skills: { manaSpring: 1 } });
|
|
||||||
expect(computeRegen(state)).toBe(2 + 0.5 + 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should multiply by temporal echo prestige', () => {
|
|
||||||
const state = createMockState({ prestigeUpgrades: { temporalEcho: 2 } });
|
|
||||||
// Base 2 * 1.2 (temporal) = 2.4, + enchanter 0.5 = 2.9
|
|
||||||
// Note: temporal bonus applies to base, not attunement
|
|
||||||
expect(computeRegen(state)).toBe(2 * 1.2 + 0.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('computeClickMana', () => {
|
|
||||||
it('should return base click mana with no upgrades', () => {
|
|
||||||
const state = createMockState();
|
|
||||||
expect(computeClickMana(state)).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add mana from manaTap skill', () => {
|
|
||||||
const state = createMockState({ skills: { manaTap: 1 } });
|
|
||||||
expect(computeClickMana(state)).toBe(1 + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add mana from manaSurge skill', () => {
|
|
||||||
const state = createMockState({ skills: { manaSurge: 1 } });
|
|
||||||
expect(computeClickMana(state)).toBe(1 + 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack manaTap and manaSurge', () => {
|
|
||||||
const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } });
|
|
||||||
expect(computeClickMana(state)).toBe(1 + 1 + 3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Skill Evolution System
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
SKILL_EVOLUTION_PATHS,
|
|
||||||
getTierMultiplier,
|
|
||||||
getNextTierSkill,
|
|
||||||
} from '../skill-evolution';
|
|
||||||
|
|
||||||
describe('Skill Evolution System', () => {
|
|
||||||
describe('SKILL_EVOLUTION_PATHS', () => {
|
|
||||||
it('should have evolution paths for major skills', () => {
|
|
||||||
expect(SKILL_EVOLUTION_PATHS.manaWell).toBeDefined();
|
|
||||||
expect(SKILL_EVOLUTION_PATHS.manaFlow).toBeDefined();
|
|
||||||
expect(SKILL_EVOLUTION_PATHS.quickLearner).toBeDefined();
|
|
||||||
expect(SKILL_EVOLUTION_PATHS.focusedMind).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have multiple tiers for evolution', () => {
|
|
||||||
expect(SKILL_EVOLUTION_PATHS.manaWell.tiers.length).toBeGreaterThanOrEqual(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTierMultiplier', () => {
|
|
||||||
it('should return correct multipliers for tiered skills', () => {
|
|
||||||
expect(getTierMultiplier('manaWell')).toBe(1);
|
|
||||||
expect(getTierMultiplier('manaWell_t2')).toBe(10);
|
|
||||||
expect(getTierMultiplier('manaWell_t3')).toBe(100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getNextTierSkill', () => {
|
|
||||||
it('should return next tier skill ID', () => {
|
|
||||||
expect(getNextTierSkill('manaWell')).toBe('manaWell_t2');
|
|
||||||
expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Skill Requirements
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SKILLS_DEF } from '../constants';
|
|
||||||
|
|
||||||
describe('Skill Requirements', () => {
|
|
||||||
it('manaOverflow should require manaWell 3', () => {
|
|
||||||
expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('manaSurge should require manaTap 1', () => {
|
|
||||||
expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deepTrance should require meditation 1', () => {
|
|
||||||
expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('voidMeditation should require deepTrance 1', () => {
|
|
||||||
expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('efficientEnchant should require enchanting 3', () => {
|
|
||||||
expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Spell Cost System
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { rawCost, elemCost } from '../constants';
|
|
||||||
import { canAffordSpellCost } from '../store';
|
|
||||||
import type { SpellCost } from '../types';
|
|
||||||
|
|
||||||
describe('Spell Cost System', () => {
|
|
||||||
describe('rawCost', () => {
|
|
||||||
it('should create a raw mana cost', () => {
|
|
||||||
const cost = rawCost(10);
|
|
||||||
expect(cost.type).toBe('raw');
|
|
||||||
expect(cost.amount).toBe(10);
|
|
||||||
expect(cost.element).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('elemCost', () => {
|
|
||||||
it('should create an elemental mana cost', () => {
|
|
||||||
const cost = elemCost('fire', 5);
|
|
||||||
expect(cost.type).toBe('element');
|
|
||||||
expect(cost.element).toBe('fire');
|
|
||||||
expect(cost.amount).toBe(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('canAffordSpellCost', () => {
|
|
||||||
it('should allow raw mana costs when enough raw mana', () => {
|
|
||||||
const cost: SpellCost = rawCost(10);
|
|
||||||
const elements = { fire: { current: 0, max: 10, unlocked: true } };
|
|
||||||
expect(canAffordSpellCost(cost, 100, elements)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should deny raw mana costs when not enough raw mana', () => {
|
|
||||||
const cost: SpellCost = rawCost(100);
|
|
||||||
const elements = { fire: { current: 0, max: 10, unlocked: true } };
|
|
||||||
expect(canAffordSpellCost(cost, 50, elements)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow elemental costs when enough element mana', () => {
|
|
||||||
const cost: SpellCost = elemCost('fire', 5);
|
|
||||||
const elements = { fire: { current: 10, max: 10, unlocked: true } };
|
|
||||||
expect(canAffordSpellCost(cost, 0, elements)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should deny elemental costs when element not unlocked', () => {
|
|
||||||
const cost: SpellCost = elemCost('fire', 5);
|
|
||||||
const elements = { fire: { current: 10, max: 10, unlocked: false } };
|
|
||||||
expect(canAffordSpellCost(cost, 100, elements)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should deny elemental costs when not enough element mana', () => {
|
|
||||||
const cost: SpellCost = elemCost('fire', 10);
|
|
||||||
const elements = { fire: { current: 5, max: 10, unlocked: true } };
|
|
||||||
expect(canAffordSpellCost(cost, 100, elements)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests for Study Speed Functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '../constants';
|
|
||||||
|
|
||||||
describe('Study Speed Functions', () => {
|
|
||||||
describe('getStudySpeedMultiplier', () => {
|
|
||||||
it('should return 1 with no quickLearner skill', () => {
|
|
||||||
expect(getStudySpeedMultiplier({})).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should increase by 10% per level', () => {
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 1 })).toBe(1.1);
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 5 })).toBe(1.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getStudyCostMultiplier', () => {
|
|
||||||
it('should return 1 with no focusedMind skill', () => {
|
|
||||||
expect(getStudyCostMultiplier({})).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should decrease by 5% per level', () => {
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 1 })).toBe(0.95);
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 5 })).toBe(0.75);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test Utilities and Fixtures for Mana Loop Game Logic Tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { GameState } from '../types';
|
|
||||||
import {
|
|
||||||
ELEMENTS,
|
|
||||||
} from '../constants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a mock game state with sensible defaults.
|
|
||||||
* Override any properties via the `overrides` parameter.
|
|
||||||
*/
|
|
||||||
export function createMockState(overrides: Partial<GameState> = {}): GameState {
|
|
||||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
|
||||||
Object.keys(ELEMENTS).forEach((k) => {
|
|
||||||
elements[k] = { current: 0, max: 10, unlocked: false };
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
maxFloorReached: 1,
|
|
||||||
signedPacts: [],
|
|
||||||
activeSpell: 'manaBolt',
|
|
||||||
currentAction: 'meditate',
|
|
||||||
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
|
||||||
skills: {},
|
|
||||||
skillProgress: {},
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null },
|
|
||||||
inventory: [],
|
|
||||||
blueprints: {},
|
|
||||||
schedule: [],
|
|
||||||
autoSchedule: false,
|
|
||||||
studyQueue: [],
|
|
||||||
craftQueue: [],
|
|
||||||
currentStudyTarget: null,
|
|
||||||
insight: 0,
|
|
||||||
totalInsight: 0,
|
|
||||||
prestigeUpgrades: {},
|
|
||||||
memorySlots: 3,
|
|
||||||
memories: [],
|
|
||||||
incursionStrength: 0,
|
|
||||||
containmentWards: 0,
|
|
||||||
log: [],
|
|
||||||
loopInsight: 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,
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
} as GameState;
|
|
||||||
}
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
// ─── Skill Slice ──────────────────────────────────────────────────────────────
|
|
||||||
// Manages skills, studying, and skill progress
|
|
||||||
|
|
||||||
import type { StateCreator } from 'zustand';
|
|
||||||
import type { GameState, StudyTarget, SkillUpgradeChoice } from '../types';
|
|
||||||
import { SKILLS_DEF, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '../constants';
|
|
||||||
import { getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '../skill-evolution';
|
|
||||||
import { computeEffects } from '../upgrade-effects';
|
|
||||||
|
|
||||||
export interface SkillSlice {
|
|
||||||
// State
|
|
||||||
skills: Record<string, number>;
|
|
||||||
skillProgress: Record<string, number>;
|
|
||||||
skillUpgrades: Record<string, string[]>;
|
|
||||||
skillTiers: Record<string, number>;
|
|
||||||
currentStudyTarget: StudyTarget | null;
|
|
||||||
parallelStudyTarget: StudyTarget | null;
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
startStudyingSkill: (skillId: string) => void;
|
|
||||||
startStudyingSpell: (spellId: string) => void;
|
|
||||||
startParallelStudySkill: (skillId: string) => void;
|
|
||||||
cancelStudy: () => void;
|
|
||||||
cancelParallelStudy: () => void;
|
|
||||||
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
|
||||||
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
|
|
||||||
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone: 5 | 10) => void;
|
|
||||||
tierUpSkill: (skillId: string) => void;
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: SkillUpgradeChoice[]; selected: string[] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSkillSlice = (
|
|
||||||
set: StateCreator<GameState>['set'],
|
|
||||||
get: () => GameState
|
|
||||||
): SkillSlice => ({
|
|
||||||
skills: {},
|
|
||||||
skillProgress: {},
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
currentStudyTarget: null,
|
|
||||||
parallelStudyTarget: null,
|
|
||||||
|
|
||||||
startStudyingSkill: (skillId: string) => {
|
|
||||||
const state = get();
|
|
||||||
const sk = SKILLS_DEF[skillId];
|
|
||||||
if (!sk) return;
|
|
||||||
|
|
||||||
const currentLevel = state.skills[skillId] || 0;
|
|
||||||
if (currentLevel >= sk.max) return;
|
|
||||||
|
|
||||||
// Check prerequisites
|
|
||||||
if (sk.req) {
|
|
||||||
for (const [r, rl] of Object.entries(sk.req)) {
|
|
||||||
if ((state.skills[r] || 0) < rl) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const costMult = getStudyCostMultiplier(state.skills);
|
|
||||||
const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult);
|
|
||||||
const manaCostPerHour = totalCost / sk.studyTime;
|
|
||||||
|
|
||||||
set({
|
|
||||||
currentAction: 'study',
|
|
||||||
currentStudyTarget: {
|
|
||||||
type: 'skill',
|
|
||||||
id: skillId,
|
|
||||||
progress: state.skillProgress[skillId] || 0,
|
|
||||||
required: sk.studyTime,
|
|
||||||
manaCostPerHour,
|
|
||||||
},
|
|
||||||
log: [`📚 Started studying ${sk.name}... (${Math.floor(manaCostPerHour)} mana/hour)`, ...state.log.slice(0, 49)],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
startStudyingSpell: (spellId: string) => {
|
|
||||||
const state = get();
|
|
||||||
const sp = SPELLS_DEF[spellId];
|
|
||||||
if (!sp || state.spells[spellId]?.learned) return;
|
|
||||||
|
|
||||||
const costMult = getStudyCostMultiplier(state.skills);
|
|
||||||
const totalCost = Math.floor(sp.unlock * costMult);
|
|
||||||
const studyTime = sp.studyTime || (sp.tier * 4);
|
|
||||||
const manaCostPerHour = totalCost / studyTime;
|
|
||||||
|
|
||||||
set({
|
|
||||||
currentAction: 'study',
|
|
||||||
currentStudyTarget: {
|
|
||||||
type: 'spell',
|
|
||||||
id: spellId,
|
|
||||||
progress: state.spells[spellId]?.studyProgress || 0,
|
|
||||||
required: studyTime,
|
|
||||||
manaCostPerHour,
|
|
||||||
},
|
|
||||||
spells: {
|
|
||||||
...state.spells,
|
|
||||||
[spellId]: {
|
|
||||||
...(state.spells[spellId] || { learned: false, level: 0 }),
|
|
||||||
studyProgress: state.spells[spellId]?.studyProgress || 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
log: [`📚 Started studying ${sp.name}... (${Math.floor(manaCostPerHour)} mana/hour)`, ...state.log.slice(0, 49)],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
startParallelStudySkill: (skillId: string) => {
|
|
||||||
const state = get();
|
|
||||||
if (state.parallelStudyTarget) return;
|
|
||||||
if (!state.currentStudyTarget) return;
|
|
||||||
|
|
||||||
const sk = SKILLS_DEF[skillId];
|
|
||||||
if (!sk) return;
|
|
||||||
|
|
||||||
const currentLevel = state.skills[skillId] || 0;
|
|
||||||
if (currentLevel >= sk.max) return;
|
|
||||||
|
|
||||||
if (state.currentStudyTarget.id === skillId) return;
|
|
||||||
|
|
||||||
set({
|
|
||||||
parallelStudyTarget: {
|
|
||||||
type: 'skill',
|
|
||||||
id: skillId,
|
|
||||||
progress: state.skillProgress[skillId] || 0,
|
|
||||||
required: sk.studyTime,
|
|
||||||
manaCostPerHour: 0, // Parallel study doesn't cost extra
|
|
||||||
},
|
|
||||||
log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelStudy: () => {
|
|
||||||
const state = get();
|
|
||||||
if (!state.currentStudyTarget) return;
|
|
||||||
|
|
||||||
const savedProgress = state.currentStudyTarget.progress;
|
|
||||||
const log = ['📖 Study paused. Progress saved.', ...state.log.slice(0, 49)];
|
|
||||||
|
|
||||||
if (state.currentStudyTarget.type === 'skill') {
|
|
||||||
set({
|
|
||||||
currentStudyTarget: null,
|
|
||||||
currentAction: 'meditate',
|
|
||||||
skillProgress: {
|
|
||||||
...state.skillProgress,
|
|
||||||
[state.currentStudyTarget.id]: savedProgress,
|
|
||||||
},
|
|
||||||
log,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
set({
|
|
||||||
currentStudyTarget: null,
|
|
||||||
currentAction: 'meditate',
|
|
||||||
spells: {
|
|
||||||
...state.spells,
|
|
||||||
[state.currentStudyTarget.id]: {
|
|
||||||
...(state.spells[state.currentStudyTarget.id] || { learned: false, level: 0 }),
|
|
||||||
studyProgress: savedProgress,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
log,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelParallelStudy: () => {
|
|
||||||
set((state) => {
|
|
||||||
if (!state.parallelStudyTarget) return state;
|
|
||||||
return {
|
|
||||||
parallelStudyTarget: null,
|
|
||||||
log: ['📖 Parallel study cancelled.', ...state.log.slice(0, 49)],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
selectSkillUpgrade: (skillId: string, upgradeId: string) => {
|
|
||||||
set((state) => {
|
|
||||||
const current = state.skillUpgrades?.[skillId] || [];
|
|
||||||
if (current.includes(upgradeId)) return state;
|
|
||||||
if (current.length >= 2) return state;
|
|
||||||
return {
|
|
||||||
skillUpgrades: {
|
|
||||||
...state.skillUpgrades,
|
|
||||||
[skillId]: [...current, upgradeId],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
deselectSkillUpgrade: (skillId: string, upgradeId: string) => {
|
|
||||||
set((state) => {
|
|
||||||
const current = state.skillUpgrades?.[skillId] || [];
|
|
||||||
return {
|
|
||||||
skillUpgrades: {
|
|
||||||
...state.skillUpgrades,
|
|
||||||
[skillId]: current.filter(id => id !== upgradeId),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
commitSkillUpgrades: (skillId: string, upgradeIds: string[], milestone: 5 | 10) => {
|
|
||||||
set((state) => {
|
|
||||||
const existingUpgrades = state.skillUpgrades?.[skillId] || [];
|
|
||||||
const otherMilestoneUpgrades = existingUpgrades.filter(
|
|
||||||
id => milestone === 5 ? id.includes('_l10') : id.includes('_l5')
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
skillUpgrades: {
|
|
||||||
...state.skillUpgrades,
|
|
||||||
[skillId]: [...otherMilestoneUpgrades, ...upgradeIds],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
tierUpSkill: (skillId: string) => {
|
|
||||||
const state = get();
|
|
||||||
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
|
||||||
const currentTier = state.skillTiers?.[baseSkillId] || 1;
|
|
||||||
const nextTier = currentTier + 1;
|
|
||||||
|
|
||||||
if (nextTier > 5) return;
|
|
||||||
|
|
||||||
const nextTierSkillId = `${baseSkillId}_t${nextTier}`;
|
|
||||||
|
|
||||||
set({
|
|
||||||
skillTiers: {
|
|
||||||
...state.skillTiers,
|
|
||||||
[baseSkillId]: nextTier,
|
|
||||||
},
|
|
||||||
skills: {
|
|
||||||
...state.skills,
|
|
||||||
[nextTierSkillId]: 0,
|
|
||||||
[skillId]: 0,
|
|
||||||
},
|
|
||||||
skillProgress: {
|
|
||||||
...state.skillProgress,
|
|
||||||
[skillId]: 0,
|
|
||||||
[nextTierSkillId]: 0,
|
|
||||||
},
|
|
||||||
skillUpgrades: {
|
|
||||||
...state.skillUpgrades,
|
|
||||||
[nextTierSkillId]: [],
|
|
||||||
[skillId]: [],
|
|
||||||
},
|
|
||||||
log: [`🌟 ${SKILLS_DEF[baseSkillId]?.name || baseSkillId} evolved to Tier ${nextTier}!`, ...state.log.slice(0, 49)],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => {
|
|
||||||
const state = get();
|
|
||||||
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
|
||||||
const tier = state.skillTiers?.[baseSkillId] || 1;
|
|
||||||
|
|
||||||
const available = getUpgradesForSkillAtMilestone(skillId, milestone, state.skillTiers || {});
|
|
||||||
const selected = (state.skillUpgrades?.[skillId] || []).filter(id =>
|
|
||||||
available.some(u => u.id === id)
|
|
||||||
);
|
|
||||||
|
|
||||||
return { available, selected };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process study progress (called during tick)
|
|
||||||
export function processStudy(state: GameState, deltaHours: number): Partial<GameState> {
|
|
||||||
if (state.currentAction !== 'study' || !state.currentStudyTarget) return {};
|
|
||||||
|
|
||||||
const target = state.currentStudyTarget;
|
|
||||||
const studySpeedMult = getStudySpeedMultiplier(state.skills);
|
|
||||||
const progressGain = deltaHours * studySpeedMult;
|
|
||||||
const manaCost = progressGain * target.manaCostPerHour;
|
|
||||||
|
|
||||||
let rawMana = state.rawMana;
|
|
||||||
let totalManaGathered = state.totalManaGathered;
|
|
||||||
let skills = state.skills;
|
|
||||||
let skillProgress = state.skillProgress;
|
|
||||||
let spells = state.spells;
|
|
||||||
const log = [...state.log];
|
|
||||||
|
|
||||||
if (rawMana >= manaCost) {
|
|
||||||
rawMana -= manaCost;
|
|
||||||
totalManaGathered += manaCost;
|
|
||||||
|
|
||||||
const newProgress = target.progress + progressGain;
|
|
||||||
|
|
||||||
if (newProgress >= target.required) {
|
|
||||||
// Study complete
|
|
||||||
if (target.type === 'skill') {
|
|
||||||
const skillId = target.id;
|
|
||||||
const currentLevel = skills[skillId] || 0;
|
|
||||||
skills = { ...skills, [skillId]: currentLevel + 1 };
|
|
||||||
skillProgress = { ...skillProgress, [skillId]: 0 };
|
|
||||||
log.unshift(`✅ ${SKILLS_DEF[skillId]?.name} Lv.${currentLevel + 1} mastered!`);
|
|
||||||
} else if (target.type === 'spell') {
|
|
||||||
const spellId = target.id;
|
|
||||||
spells = {
|
|
||||||
...spells,
|
|
||||||
[spellId]: { learned: true, level: 1, studyProgress: 0 },
|
|
||||||
};
|
|
||||||
log.unshift(`📖 ${SPELLS_DEF[spellId]?.name} learned!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
rawMana,
|
|
||||||
totalManaGathered,
|
|
||||||
skills,
|
|
||||||
skillProgress,
|
|
||||||
spells,
|
|
||||||
currentStudyTarget: null,
|
|
||||||
currentAction: 'meditate',
|
|
||||||
log,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
rawMana,
|
|
||||||
totalManaGathered,
|
|
||||||
currentStudyTarget: { ...target, progress: newProgress },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not enough mana
|
|
||||||
log.unshift('⚠️ Not enough mana to continue studying. Progress saved.');
|
|
||||||
|
|
||||||
if (target.type === 'skill') {
|
|
||||||
return {
|
|
||||||
currentStudyTarget: null,
|
|
||||||
currentAction: 'meditate',
|
|
||||||
skillProgress: { ...skillProgress, [target.id]: target.progress },
|
|
||||||
log,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
currentStudyTarget: null,
|
|
||||||
currentAction: 'meditate',
|
|
||||||
spells: {
|
|
||||||
...spells,
|
|
||||||
[target.id]: {
|
|
||||||
...(spells[target.id] || { learned: false, level: 0 }),
|
|
||||||
studyProgress: target.progress,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
log,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/**
|
|
||||||
* Combat Store Tests - stores.test.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import { useCombatStore } from '@/lib/game/stores';
|
|
||||||
|
|
||||||
// ─── Test Fixtures ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
useCombatStore.setState({
|
|
||||||
currentFloor: 1,
|
|
||||||
floorHP: 151,
|
|
||||||
floorMaxHP: 151,
|
|
||||||
maxFloorReached: 1,
|
|
||||||
activeSpell: 'manaBolt',
|
|
||||||
currentAction: 'meditate',
|
|
||||||
castProgress: 0,
|
|
||||||
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Combat Store Tests ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('CombatStore', () => {
|
|
||||||
describe('floor operations', () => {
|
|
||||||
it('should advance floor', () => {
|
|
||||||
useCombatStore.getState().advanceFloor();
|
|
||||||
expect(useCombatStore.getState().currentFloor).toBe(2);
|
|
||||||
expect(useCombatStore.getState().maxFloorReached).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should cap at floor 100', () => {
|
|
||||||
useCombatStore.setState({ currentFloor: 100 });
|
|
||||||
useCombatStore.getState().advanceFloor();
|
|
||||||
expect(useCombatStore.getState().currentFloor).toBe(100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('action management', () => {
|
|
||||||
it('should set action', () => {
|
|
||||||
useCombatStore.getState().setAction('climb');
|
|
||||||
expect(useCombatStore.getState().currentAction).toBe('climb');
|
|
||||||
|
|
||||||
useCombatStore.getState().setAction('study');
|
|
||||||
expect(useCombatStore.getState().currentAction).toBe('study');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('spell management', () => {
|
|
||||||
it('should set active spell', () => {
|
|
||||||
useCombatStore.getState().learnSpell('fireball');
|
|
||||||
useCombatStore.getState().setSpell('fireball');
|
|
||||||
expect(useCombatStore.getState().activeSpell).toBe('fireball');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should learn spell', () => {
|
|
||||||
useCombatStore.getState().learnSpell('fireball');
|
|
||||||
expect(useCombatStore.getState().spells['fireball']?.learned).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ CombatStore tests defined (from stores.test.ts).');
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/**
|
|
||||||
* Store Integration Tests - stores.test.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import {
|
|
||||||
useManaStore,
|
|
||||||
useSkillStore,
|
|
||||||
usePrestigeStore,
|
|
||||||
useCombatStore,
|
|
||||||
computeMaxMana,
|
|
||||||
} from '@/lib/game/stores';
|
|
||||||
|
|
||||||
// ─── Test Fixtures ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
useManaStore.setState({
|
|
||||||
rawMana: 10,
|
|
||||||
meditateTicks: 0,
|
|
||||||
totalManaGathered: 0,
|
|
||||||
elements: (() => {
|
|
||||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
|
||||||
['fire', 'water', 'air', 'earth'].forEach(k => {
|
|
||||||
elements[k] = { current: 0, max: 10, unlocked: true };
|
|
||||||
});
|
|
||||||
return elements;
|
|
||||||
})(),
|
|
||||||
});
|
|
||||||
|
|
||||||
useSkillStore.setState({
|
|
||||||
skills: {},
|
|
||||||
skillProgress: {},
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
paidStudySkills: {},
|
|
||||||
currentStudyTarget: null,
|
|
||||||
parallelStudyTarget: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
usePrestigeStore.setState({
|
|
||||||
loopCount: 0,
|
|
||||||
insight: 0,
|
|
||||||
totalInsight: 0,
|
|
||||||
loopInsight: 0,
|
|
||||||
prestigeUpgrades: {},
|
|
||||||
memorySlots: 3,
|
|
||||||
pactSlots: 1,
|
|
||||||
memories: [],
|
|
||||||
defeatedGuardians: [],
|
|
||||||
signedPacts: [],
|
|
||||||
pactRitualFloor: null,
|
|
||||||
pactRitualProgress: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useCombatStore.setState({
|
|
||||||
currentFloor: 1,
|
|
||||||
floorHP: 151,
|
|
||||||
floorMaxHP: 151,
|
|
||||||
maxFloorReached: 1,
|
|
||||||
activeSpell: 'manaBolt',
|
|
||||||
currentAction: 'meditate',
|
|
||||||
castProgress: 0,
|
|
||||||
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Integration Tests ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Store Integration', () => {
|
|
||||||
describe('skill study flow', () => {
|
|
||||||
it('should complete full study flow', () => {
|
|
||||||
// Setup: give enough mana
|
|
||||||
useManaStore.getState().addRawMana(90, 1000);
|
|
||||||
|
|
||||||
// Start studying
|
|
||||||
const startResult = useSkillStore.getState().startStudyingSkill('manaWell', 100);
|
|
||||||
expect(startResult.started).toBe(true);
|
|
||||||
expect(startResult.cost).toBe(100);
|
|
||||||
|
|
||||||
// Deduct mana (simulating UI behavior)
|
|
||||||
if (startResult.cost > 0) {
|
|
||||||
useManaStore.getState().spendRawMana(startResult.cost);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set action to study
|
|
||||||
useCombatStore.getState().setAction('study');
|
|
||||||
expect(useCombatStore.getState().currentAction).toBe('study');
|
|
||||||
|
|
||||||
// Update progress until complete
|
|
||||||
const result = useSkillStore.getState().updateStudyProgress(4);
|
|
||||||
expect(result.completed).toBe(true);
|
|
||||||
|
|
||||||
// Level up skill
|
|
||||||
useSkillStore.getState().setSkillLevel('manaWell', 1);
|
|
||||||
expect(useSkillStore.getState().skills['manaWell']).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mana and prestige interaction', () => {
|
|
||||||
it('should apply prestige mana bonus', () => {
|
|
||||||
// Get prestige upgrade
|
|
||||||
usePrestigeStore.getState().startNewLoop(1000);
|
|
||||||
usePrestigeStore.getState().doPrestige('manaWell');
|
|
||||||
|
|
||||||
// Check that prestige upgrade is recorded
|
|
||||||
expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1);
|
|
||||||
|
|
||||||
// Mana well prestige gives +500 max mana per level
|
|
||||||
const state = {
|
|
||||||
skills: {},
|
|
||||||
prestigeUpgrades: usePrestigeStore.getState().prestigeUpgrades,
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const maxMana = computeMaxMana(state);
|
|
||||||
expect(maxMana).toBe(100 + 500); // Base 100 + 500 from prestige
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Store integration tests defined (from stores.test.ts).');
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mana Store Tests - stores.test.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import {
|
|
||||||
useManaStore,
|
|
||||||
useSkillStore,
|
|
||||||
usePrestigeStore,
|
|
||||||
useCombatStore,
|
|
||||||
useUIStore,
|
|
||||||
} from '@/lib/game/stores';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
// ─── Test Fixtures ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
useManaStore.setState({
|
|
||||||
rawMana: 10,
|
|
||||||
meditateTicks: 0,
|
|
||||||
totalManaGathered: 0,
|
|
||||||
elements: (() => {
|
|
||||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
|
||||||
Object.keys(ELEMENTS).forEach((k) => {
|
|
||||||
elements[k] = { current: 0, max: 10, unlocked: ['fire', 'water', 'air', 'earth'].includes(k) };
|
|
||||||
});
|
|
||||||
return elements;
|
|
||||||
})(),
|
|
||||||
});
|
|
||||||
|
|
||||||
useSkillStore.setState({
|
|
||||||
skills: {},
|
|
||||||
skillProgress: {},
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
paidStudySkills: {},
|
|
||||||
currentStudyTarget: null,
|
|
||||||
parallelStudyTarget: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
usePrestigeStore.setState({
|
|
||||||
loopCount: 0,
|
|
||||||
insight: 0,
|
|
||||||
totalInsight: 0,
|
|
||||||
loopInsight: 0,
|
|
||||||
prestigeUpgrades: {},
|
|
||||||
memorySlots: 3,
|
|
||||||
pactSlots: 1,
|
|
||||||
memories: [],
|
|
||||||
defeatedGuardians: [],
|
|
||||||
signedPacts: [],
|
|
||||||
pactRitualFloor: null,
|
|
||||||
pactRitualProgress: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useCombatStore.setState({
|
|
||||||
currentFloor: 1,
|
|
||||||
floorHP: 151,
|
|
||||||
floorMaxHP: 151,
|
|
||||||
maxFloorReached: 1,
|
|
||||||
activeSpell: 'manaBolt',
|
|
||||||
currentAction: 'meditate',
|
|
||||||
castProgress: 0,
|
|
||||||
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
|
||||||
});
|
|
||||||
|
|
||||||
useUIStore.setState({
|
|
||||||
logs: [],
|
|
||||||
paused: false,
|
|
||||||
gameOver: false,
|
|
||||||
victory: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Mana Store Tests ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('ManaStore', () => {
|
|
||||||
describe('initial state', () => {
|
|
||||||
it('should have correct initial values', () => {
|
|
||||||
const state = useManaStore.getState();
|
|
||||||
expect(state.rawMana).toBe(10);
|
|
||||||
expect(state.meditateTicks).toBe(0);
|
|
||||||
expect(state.totalManaGathered).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have base elements unlocked', () => {
|
|
||||||
const state = useManaStore.getState();
|
|
||||||
expect(state.elements.fire.unlocked).toBe(true);
|
|
||||||
expect(state.elements.water.unlocked).toBe(true);
|
|
||||||
expect(state.elements.air.unlocked).toBe(true);
|
|
||||||
expect(state.elements.earth.unlocked).toBe(true);
|
|
||||||
expect(state.elements.light.unlocked).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('raw mana operations', () => {
|
|
||||||
it('should add raw mana', () => {
|
|
||||||
useManaStore.getState().addRawMana(50, 100);
|
|
||||||
expect(useManaStore.getState().rawMana).toBe(60);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should cap at max mana', () => {
|
|
||||||
useManaStore.getState().addRawMana(200, 100);
|
|
||||||
expect(useManaStore.getState().rawMana).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should spend raw mana', () => {
|
|
||||||
const result = useManaStore.getState().spendRawMana(5);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(useManaStore.getState().rawMana).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail to spend more than available', () => {
|
|
||||||
const result = useManaStore.getState().spendRawMana(50);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
expect(useManaStore.getState().rawMana).toBe(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('element operations', () => {
|
|
||||||
it('should convert raw mana to element', () => {
|
|
||||||
useManaStore.getState().addRawMana(90, 1000); // Have 100 mana
|
|
||||||
const result = useManaStore.getState().convertMana('fire', 1);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(useManaStore.getState().elements.fire.current).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should unlock new element', () => {
|
|
||||||
useManaStore.getState().addRawMana(490, 1000); // Have 500 mana
|
|
||||||
const result = useManaStore.getState().unlockElement('light', 500);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(useManaStore.getState().elements.light.unlocked).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ ManaStore tests defined (from stores.test.ts).');
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
/**
|
|
||||||
* Prestige Store Tests - stores.test.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import {
|
|
||||||
usePrestigeStore,
|
|
||||||
useManaStore,
|
|
||||||
} from '@/lib/game/stores';
|
|
||||||
|
|
||||||
// ─── Test Fixtures ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
usePrestigeStore.setState({
|
|
||||||
loopCount: 0,
|
|
||||||
insight: 0,
|
|
||||||
totalInsight: 0,
|
|
||||||
loopInsight: 0,
|
|
||||||
prestigeUpgrades: {},
|
|
||||||
memorySlots: 3,
|
|
||||||
pactSlots: 1,
|
|
||||||
memories: [],
|
|
||||||
defeatedGuardians: [],
|
|
||||||
signedPacts: [],
|
|
||||||
pactRitualFloor: null,
|
|
||||||
pactRitualProgress: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useManaStore.setState({
|
|
||||||
rawMana: 10,
|
|
||||||
elements: (() => {
|
|
||||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
|
||||||
['fire', 'water', 'air', 'earth'].forEach(k => {
|
|
||||||
elements[k] = { current: 0, max: 10, unlocked: true };
|
|
||||||
});
|
|
||||||
return elements;
|
|
||||||
})(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Prestige Store Tests ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('PrestigeStore', () => {
|
|
||||||
describe('prestige upgrades', () => {
|
|
||||||
it('should purchase prestige upgrade', () => {
|
|
||||||
usePrestigeStore.getState().startNewLoop(1000); // Add 1000 insight
|
|
||||||
|
|
||||||
const result = usePrestigeStore.getState().doPrestige('manaWell');
|
|
||||||
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1);
|
|
||||||
expect(usePrestigeStore.getState().insight).toBe(500); // 1000 - 500 cost
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not purchase without enough insight', () => {
|
|
||||||
usePrestigeStore.getState().startNewLoop(100);
|
|
||||||
|
|
||||||
const result = usePrestigeStore.getState().doPrestige('manaWell');
|
|
||||||
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('loop management', () => {
|
|
||||||
it('should increment loop count', () => {
|
|
||||||
usePrestigeStore.getState().incrementLoopCount();
|
|
||||||
expect(usePrestigeStore.getState().loopCount).toBe(1);
|
|
||||||
|
|
||||||
usePrestigeStore.getState().incrementLoopCount();
|
|
||||||
expect(usePrestigeStore.getState().loopCount).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset for new loop', () => {
|
|
||||||
usePrestigeStore.getState().startNewLoop(1000);
|
|
||||||
usePrestigeStore.getState().doPrestige('manaWell');
|
|
||||||
|
|
||||||
usePrestigeStore.getState().resetPrestigeForNewLoop(
|
|
||||||
500, // total insight
|
|
||||||
{ manaWell: 1 }, // prestige upgrades
|
|
||||||
[], // memories
|
|
||||||
3 // memory slots
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(usePrestigeStore.getState().insight).toBe(500);
|
|
||||||
expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1);
|
|
||||||
expect(usePrestigeStore.getState().defeatedGuardians).toEqual([]);
|
|
||||||
expect(usePrestigeStore.getState().signedPacts).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('guardian pacts', () => {
|
|
||||||
it('should add signed pact', () => {
|
|
||||||
usePrestigeStore.getState().addSignedPact(10);
|
|
||||||
expect(usePrestigeStore.getState().signedPacts).toContain(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add defeated guardian', () => {
|
|
||||||
usePrestigeStore.getState().addDefeatedGuardian(10);
|
|
||||||
expect(usePrestigeStore.getState().defeatedGuardians).toContain(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not add same guardian twice', () => {
|
|
||||||
usePrestigeStore.getState().addDefeatedGuardian(10);
|
|
||||||
usePrestigeStore.getState().addDefeatedGuardian(10);
|
|
||||||
expect(usePrestigeStore.getState().defeatedGuardians.length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ PrestigeStore tests defined (from stores.test.ts).');
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
/**
|
|
||||||
* Skill Store Tests - stores.test.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import {
|
|
||||||
useManaStore,
|
|
||||||
useSkillStore,
|
|
||||||
usePrestigeStore,
|
|
||||||
getStudySpeedMultiplier,
|
|
||||||
} from '@/lib/game/stores';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
// ─── Test Fixtures ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
useManaStore.setState({
|
|
||||||
rawMana: 10,
|
|
||||||
meditateTicks: 0,
|
|
||||||
totalManaGathered: 0,
|
|
||||||
elements: (() => {
|
|
||||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
|
||||||
Object.keys(ELEMENTS).forEach((k) => {
|
|
||||||
elements[k] = { current: 0, max: 10, unlocked: ['fire', 'water', 'air', 'earth'].includes(k) };
|
|
||||||
});
|
|
||||||
return elements;
|
|
||||||
})(),
|
|
||||||
});
|
|
||||||
|
|
||||||
useSkillStore.setState({
|
|
||||||
skills: {},
|
|
||||||
skillProgress: {},
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
paidStudySkills: {},
|
|
||||||
currentStudyTarget: null,
|
|
||||||
parallelStudyTarget: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
usePrestigeStore.setState({
|
|
||||||
loopCount: 0,
|
|
||||||
insight: 0,
|
|
||||||
totalInsight: 0,
|
|
||||||
loopInsight: 0,
|
|
||||||
prestigeUpgrades: {},
|
|
||||||
memorySlots: 3,
|
|
||||||
pactSlots: 1,
|
|
||||||
memories: [],
|
|
||||||
defeatedGuardians: [],
|
|
||||||
signedPacts: [],
|
|
||||||
pactRitualFloor: null,
|
|
||||||
pactRitualProgress: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Skill Store Tests ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('SkillStore', () => {
|
|
||||||
describe('study skill', () => {
|
|
||||||
it('should start studying a skill', () => {
|
|
||||||
useManaStore.getState().addRawMana(90, 1000); // Have 100 mana
|
|
||||||
|
|
||||||
const result = useSkillStore.getState().startStudyingSkill('manaWell', 100);
|
|
||||||
|
|
||||||
expect(result.started).toBe(true);
|
|
||||||
expect(result.cost).toBe(100);
|
|
||||||
expect(useSkillStore.getState().currentStudyTarget).not.toBeNull();
|
|
||||||
expect(useSkillStore.getState().currentStudyTarget?.type).toBe('skill');
|
|
||||||
expect(useSkillStore.getState().currentStudyTarget?.id).toBe('manaWell');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not start studying without enough mana', () => {
|
|
||||||
const result = useSkillStore.getState().startStudyingSkill('manaWell', 50);
|
|
||||||
|
|
||||||
expect(result.started).toBe(false);
|
|
||||||
expect(result.cost).toBe(100);
|
|
||||||
expect(useSkillStore.getState().currentStudyTarget).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should track paid study skills', () => {
|
|
||||||
useManaStore.getState().addRawMana(90, 1000);
|
|
||||||
|
|
||||||
useSkillStore.getState().startStudyingSkill('manaWell', 100);
|
|
||||||
|
|
||||||
expect(useSkillStore.getState().paidStudySkills['manaWell']).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resume studying for free after payment', () => {
|
|
||||||
useManaStore.getState().addRawMana(90, 1000);
|
|
||||||
|
|
||||||
// First study attempt
|
|
||||||
const result1 = useSkillStore.getState().startStudyingSkill('manaWell', 100);
|
|
||||||
expect(result1.cost).toBe(100);
|
|
||||||
|
|
||||||
// Cancel study (simulated)
|
|
||||||
useSkillStore.getState().cancelStudy(0);
|
|
||||||
|
|
||||||
// Resume should be free
|
|
||||||
const result2 = useSkillStore.getState().startStudyingSkill('manaWell', 0);
|
|
||||||
expect(result2.started).toBe(true);
|
|
||||||
expect(result2.cost).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update study progress', () => {
|
|
||||||
it('should update study progress', () => {
|
|
||||||
useManaStore.getState().addRawMana(90, 1000);
|
|
||||||
useSkillStore.getState().startStudyingSkill('manaWell', 100);
|
|
||||||
|
|
||||||
const result = useSkillStore.getState().updateStudyProgress(1);
|
|
||||||
|
|
||||||
expect(result.completed).toBe(false);
|
|
||||||
expect(useSkillStore.getState().currentStudyTarget?.progress).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should complete study when progress reaches required', () => {
|
|
||||||
useManaStore.getState().addRawMana(90, 1000);
|
|
||||||
useSkillStore.getState().startStudyingSkill('manaWell', 100);
|
|
||||||
|
|
||||||
// manaWell requires 4 hours
|
|
||||||
const result = useSkillStore.getState().updateStudyProgress(4);
|
|
||||||
|
|
||||||
expect(result.completed).toBe(true);
|
|
||||||
expect(result.target?.id).toBe('manaWell');
|
|
||||||
expect(useSkillStore.getState().currentStudyTarget).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should apply study speed multiplier', () => {
|
|
||||||
useManaStore.getState().addRawMana(90, 1000);
|
|
||||||
useSkillStore.getState().setSkillLevel('quickLearner', 5); // 50% faster
|
|
||||||
|
|
||||||
useSkillStore.getState().startStudyingSkill('manaWell', 100);
|
|
||||||
|
|
||||||
// The caller should calculate progress with speed multiplier
|
|
||||||
const speedMult = getStudySpeedMultiplier(useSkillStore.getState().skills);
|
|
||||||
const result = useSkillStore.getState().updateStudyProgress(3 * speedMult); // 3 * 1.5 = 4.5
|
|
||||||
|
|
||||||
expect(result.completed).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('skill level operations', () => {
|
|
||||||
it('should set skill level', () => {
|
|
||||||
useSkillStore.getState().setSkillLevel('manaWell', 5);
|
|
||||||
expect(useSkillStore.getState().skills['manaWell']).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should increment skill level', () => {
|
|
||||||
useSkillStore.getState().setSkillLevel('manaWell', 5);
|
|
||||||
useSkillStore.getState().incrementSkillLevel('manaWell');
|
|
||||||
expect(useSkillStore.getState().skills['manaWell']).toBe(6);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('prerequisites', () => {
|
|
||||||
it('should not start studying without prerequisites', () => {
|
|
||||||
useManaStore.getState().addRawMana(990, 1000);
|
|
||||||
|
|
||||||
// manaOverflow requires manaWell 3
|
|
||||||
const result = useSkillStore.getState().startStudyingSkill('manaOverflow', 1000);
|
|
||||||
|
|
||||||
expect(result.started).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start studying with prerequisites met', () => {
|
|
||||||
useManaStore.getState().addRawMana(990, 1000);
|
|
||||||
useSkillStore.getState().setSkillLevel('manaWell', 3);
|
|
||||||
|
|
||||||
const result = useSkillStore.getState().startStudyingSkill('manaOverflow', 1000);
|
|
||||||
|
|
||||||
expect(result.started).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ SkillStore tests defined (from stores.test.ts).');
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/**
|
|
||||||
* UI Store Tests - stores.test.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import { useUIStore } from '@/lib/game/stores';
|
|
||||||
|
|
||||||
// ─── Test Fixtures ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
useUIStore.setState({
|
|
||||||
logs: [],
|
|
||||||
paused: false,
|
|
||||||
gameOver: false,
|
|
||||||
victory: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── UI Store Tests ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('UIStore', () => {
|
|
||||||
describe('log management', () => {
|
|
||||||
it('should add log message', () => {
|
|
||||||
useUIStore.getState().addLog('Test message');
|
|
||||||
expect(useUIStore.getState().logs).toContain('Test message');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear logs', () => {
|
|
||||||
useUIStore.getState().addLog('Test message');
|
|
||||||
useUIStore.getState().clearLogs();
|
|
||||||
expect(useUIStore.getState().logs.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('pause management', () => {
|
|
||||||
it('should toggle pause', () => {
|
|
||||||
expect(useUIStore.getState().paused).toBe(false);
|
|
||||||
|
|
||||||
useUIStore.getState().togglePause();
|
|
||||||
expect(useUIStore.getState().paused).toBe(true);
|
|
||||||
|
|
||||||
useUIStore.getState().togglePause();
|
|
||||||
expect(useUIStore.getState().paused).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set pause state', () => {
|
|
||||||
useUIStore.getState().setPaused(true);
|
|
||||||
expect(useUIStore.getState().paused).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('game over state', () => {
|
|
||||||
it('should set game over', () => {
|
|
||||||
useUIStore.getState().setGameOver(true);
|
|
||||||
expect(useUIStore.getState().gameOver).toBe(true);
|
|
||||||
expect(useUIStore.getState().victory).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set victory', () => {
|
|
||||||
useUIStore.getState().setGameOver(true, true);
|
|
||||||
expect(useUIStore.getState().gameOver).toBe(true);
|
|
||||||
expect(useUIStore.getState().victory).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ UIStore tests defined (from stores.test.ts).');
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Store Method Tests - Main Index
|
|
||||||
*
|
|
||||||
* This file re-exports all individual store method test files.
|
|
||||||
* Each test file is focused on a specific store's methods.
|
|
||||||
*
|
|
||||||
* Original file: store-methods.test.ts (588 lines)
|
|
||||||
* Refactored into 5 smaller test files (~2,900 total lines across files).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import '../store-method-tests/skill-store.test';
|
|
||||||
import '../store-method-tests/mana-store.test';
|
|
||||||
import '../store-method-tests/combat-store.test';
|
|
||||||
import '../store-method-tests/prestige-store.test';
|
|
||||||
import '../store-method-tests/ui-store.test';
|
|
||||||
|
|
||||||
console.log('✅ All store method tests complete (refactored from 588 lines to 5 focused test files).');
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Stores Tests (stores/__tests__/stores.test.ts) - Main Index
|
|
||||||
*
|
|
||||||
* This file re-exports all individual test files from stores/__tests__/stores.test.ts
|
|
||||||
* Each test file is focused on a specific area of functionality.
|
|
||||||
*
|
|
||||||
* Original file: stores.test.ts (458 lines)
|
|
||||||
* Refactored into 12 smaller test files.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import '../stores-tests/formatting.test';
|
|
||||||
import '../stores-tests/floor.test';
|
|
||||||
import '../stores-tests/mana-calculation.test';
|
|
||||||
import '../stores-tests/damage-calculation.test';
|
|
||||||
import '../stores-tests/insight-calculation.test';
|
|
||||||
import '../stores-tests/meditation.test';
|
|
||||||
import '../stores-tests/incursion.test';
|
|
||||||
import '../stores-tests/spell-cost.test';
|
|
||||||
import '../stores-tests/study-speed.test';
|
|
||||||
import '../stores-tests/guardians.test';
|
|
||||||
import '../stores-tests/skill-definitions.test';
|
|
||||||
import '../stores-tests/prestige-upgrades.test';
|
|
||||||
import '../stores-tests/spell-definitions.test';
|
|
||||||
|
|
||||||
console.log('✅ All stores tests from stores/__tests__/stores.test.ts complete (refactored from 458 lines to 12 focused test files).');
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
computeStats,
|
|
||||||
BASE_STATS,
|
|
||||||
SKILLS_V2,
|
|
||||||
getBaseSkillId,
|
|
||||||
hasPrerequisites,
|
|
||||||
} from '../../constants/skills-v2';
|
|
||||||
import type { ComputedStats } from '../../constants/skills-v2-types';
|
|
||||||
|
|
||||||
// Helper to create a minimal prestige state
|
|
||||||
const emptyPrestige = {};
|
|
||||||
|
|
||||||
describe('computeStats()', () => {
|
|
||||||
describe('base stats with no skills', () => {
|
|
||||||
it('should return base stats when no skills are provided', () => {
|
|
||||||
const result = computeStats({}, emptyPrestige);
|
|
||||||
expect(result.maxMana).toBe(100);
|
|
||||||
expect(result.manaRegen).toBe(2);
|
|
||||||
expect(result.clickMana).toBe(1);
|
|
||||||
expect(result.baseDamage).toBe(5);
|
|
||||||
expect(result.elementCap).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have all base values correct', () => {
|
|
||||||
const result = computeStats({}, emptyPrestige);
|
|
||||||
expect(result).toMatchObject({
|
|
||||||
maxMana: 100,
|
|
||||||
manaRegen: 2,
|
|
||||||
clickMana: 1,
|
|
||||||
elementCap: 10,
|
|
||||||
studySpeed: 1,
|
|
||||||
studyCostMult: 1,
|
|
||||||
meditationEfficiency: 1,
|
|
||||||
enchantCapacity: 100,
|
|
||||||
enchantSpeed: 1,
|
|
||||||
enchantPower: 1,
|
|
||||||
disenchantRecovery: 1,
|
|
||||||
baseDamage: 5,
|
|
||||||
damageMultiplier: 1,
|
|
||||||
attackSpeed: 1,
|
|
||||||
critChance: 0,
|
|
||||||
critMultiplier: 1.5,
|
|
||||||
armorPierce: 0,
|
|
||||||
insightGain: 1,
|
|
||||||
golemDamage: 1,
|
|
||||||
golemDuration: 1,
|
|
||||||
pactMultiplier: 1,
|
|
||||||
spellDamage: 1,
|
|
||||||
guardianDamage: 1,
|
|
||||||
craftSpeed: 1,
|
|
||||||
repairSpeed: 1,
|
|
||||||
elementalDamage: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Well skill', () => {
|
|
||||||
it('should add 100 max mana per level', () => {
|
|
||||||
const result = computeStats({ manaWell: 5 }, emptyPrestige);
|
|
||||||
expect(result.maxMana).toBe(100 + 5 * 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be 100 at level 0', () => {
|
|
||||||
const result = computeStats({ manaWell: 0 }, emptyPrestige);
|
|
||||||
expect(result.maxMana).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be 1100 at max level 10', () => {
|
|
||||||
const result = computeStats({ manaWell: 10 }, emptyPrestige);
|
|
||||||
expect(result.maxMana).toBe(1100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Flow skill', () => {
|
|
||||||
it('should add 1 mana regen per level', () => {
|
|
||||||
const result = computeStats({ manaFlow: 5 }, emptyPrestige);
|
|
||||||
expect(result.manaRegen).toBe(2 + 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be 2 at level 0', () => {
|
|
||||||
const result = computeStats({ manaFlow: 0 }, emptyPrestige);
|
|
||||||
expect(result.manaRegen).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be 12 at max level 10', () => {
|
|
||||||
const result = computeStats({ manaFlow: 10 }, emptyPrestige);
|
|
||||||
expect(result.manaRegen).toBe(12);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Tap skill', () => {
|
|
||||||
it('should add 1 click mana at level 1', () => {
|
|
||||||
const result = computeStats({ manaTap: 1 }, emptyPrestige);
|
|
||||||
expect(result.clickMana).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be 1 at level 0', () => {
|
|
||||||
const result = computeStats({ manaTap: 0 }, emptyPrestige);
|
|
||||||
expect(result.clickMana).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Surge skill', () => {
|
|
||||||
it('should add 3 click mana per level', () => {
|
|
||||||
const result = computeStats({ manaSurge: 1 }, emptyPrestige);
|
|
||||||
expect(result.clickMana).toBe(1 + 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack with manaTap', () => {
|
|
||||||
const result = computeStats({ manaTap: 1, manaSurge: 1 }, emptyPrestige);
|
|
||||||
expect(result.clickMana).toBe(1 + 1 + 3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Spring skill', () => {
|
|
||||||
it('should add 2 mana regen at level 1', () => {
|
|
||||||
const result = computeStats({ manaSpring: 1 }, emptyPrestige);
|
|
||||||
expect(result.manaRegen).toBe(2 + 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack with manaFlow', () => {
|
|
||||||
const result = computeStats({ manaFlow: 5, manaSpring: 1 }, emptyPrestige);
|
|
||||||
expect(result.manaRegen).toBe(2 + 5 + 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Mana Overflow skill', () => {
|
|
||||||
it('should multiply click mana by compounding 1.25 per level', () => {
|
|
||||||
// multiply effects compound per level: 1 * 1.25^2 = 1.5625
|
|
||||||
const result = computeStats({ manaOverflow: 2 }, emptyPrestige);
|
|
||||||
expect(result.clickMana).toBeCloseTo(1.5625, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be 1 at level 0', () => {
|
|
||||||
const result = computeStats({ manaOverflow: 0 }, emptyPrestige);
|
|
||||||
expect(result.clickMana).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Quick Learner skill', () => {
|
|
||||||
it('should multiply study speed by compounding 1.10 per level', () => {
|
|
||||||
// 1.1^5 = 1.61051
|
|
||||||
const result = computeStats({ quickLearner: 5 }, emptyPrestige);
|
|
||||||
expect(result.studySpeed).toBeCloseTo(1.61051, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be ~2.5937 at max level 10', () => {
|
|
||||||
const result = computeStats({ quickLearner: 10 }, emptyPrestige);
|
|
||||||
expect(result.studySpeed).toBeCloseTo(2.593742, 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Focused Mind skill', () => {
|
|
||||||
it('should multiply study cost by compounding 0.95 per level', () => {
|
|
||||||
// 0.95^2 = 0.9025
|
|
||||||
const result = computeStats({ focusedMind: 2 }, emptyPrestige);
|
|
||||||
expect(result.studyCostMult).toBeCloseTo(0.9025, 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Meditation Focus skill', () => {
|
|
||||||
it('should multiply meditation efficiency at level 1', () => {
|
|
||||||
// BASE (1) * (1 + 1.5) = 2.5
|
|
||||||
const result = computeStats({ meditation: 1 }, emptyPrestige);
|
|
||||||
expect(result.meditationEfficiency).toBe(2.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Deep Trance skill', () => {
|
|
||||||
it('should multiply meditation efficiency further', () => {
|
|
||||||
// meditation: 1 * (1+1.5) = 2.5, deepTrance: 2.5 * (1+1.8) = 7.0
|
|
||||||
const result = computeStats({ meditation: 1, deepTrance: 1 }, emptyPrestige);
|
|
||||||
expect(result.meditationEfficiency).toBe(7.0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Void Meditation skill', () => {
|
|
||||||
it('should multiply meditation efficiency to max', () => {
|
|
||||||
// 1 * 2.5 * 2.8 * 3.5 = 24.5
|
|
||||||
const result = computeStats({ meditation: 1, deepTrance: 1, voidMeditation: 1 }, emptyPrestige);
|
|
||||||
expect(result.meditationEfficiency).toBe(24.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Combat skills', () => {
|
|
||||||
it('Arcane Fury should multiply damage', () => {
|
|
||||||
const result = computeStats({ arcaneFury: 5 }, emptyPrestige);
|
|
||||||
expect(result.damageMultiplier).toBeCloseTo(1.61051, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Combat Training should add base damage', () => {
|
|
||||||
const result = computeStats({ combatTraining: 3 }, emptyPrestige);
|
|
||||||
expect(result.baseDamage).toBe(5 + 3 * 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Precision should add crit chance', () => {
|
|
||||||
const result = computeStats({ precision: 4 }, emptyPrestige);
|
|
||||||
expect(result.critChance).toBe(0.2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Precision should cap at 1.0', () => {
|
|
||||||
const result = computeStats({ precision: 25 }, emptyPrestige);
|
|
||||||
expect(result.critChance).toBe(1.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Elemental Mastery should multiply elemental damage', () => {
|
|
||||||
// 1 * 1.15^4 = ~1.749
|
|
||||||
const result = computeStats({ elementalMastery: 4 }, emptyPrestige);
|
|
||||||
expect(result.elementalDamage).toBeCloseTo(1.749, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Attack Speed should multiply attack speed (compounding)', () => {
|
|
||||||
// 1 * 0.9^3 = 0.729
|
|
||||||
const result = computeStats({ attackSpeed: 3 }, emptyPrestige);
|
|
||||||
expect(result.attackSpeed).toBeCloseTo(0.729, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Armor Piercing should add armor pierce', () => {
|
|
||||||
const result = computeStats({ armorPiercing: 4 }, emptyPrestige);
|
|
||||||
expect(result.armorPierce).toBe(0.2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Enchanting skills', () => {
|
|
||||||
it('Enchanting should affect enchantCapacity and enchantSpeed', () => {
|
|
||||||
const result = computeStats({ enchanting: 5 }, emptyPrestige);
|
|
||||||
expect(result.enchantCapacity).toBe(100 + 5 * 10); // add 10 per level
|
|
||||||
expect(result.enchantSpeed).toBeCloseTo(0.9, 2); // multiply by 0.98^5
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Essence Refining should multiply enchantPower', () => {
|
|
||||||
const result = computeStats({ enchanting: 4, essenceRefining: 1 }, emptyPrestige);
|
|
||||||
expect(result.enchantPower).toBeCloseTo(1.1, 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Golemancy skills', () => {
|
|
||||||
it('Golem Mastery should multiply golemDamage', () => {
|
|
||||||
const result = computeStats({ golemMastery: 5 }, emptyPrestige);
|
|
||||||
expect(result.golemDamage).toBeCloseTo(1.61051, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Golem Longevity should add golemDuration', () => {
|
|
||||||
const result = computeStats({ golemLongevity: 3 }, emptyPrestige);
|
|
||||||
expect(result.golemDuration).toBe(1 + 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Golem Efficiency should multiply attackSpeed', () => {
|
|
||||||
const result = computeStats({ golemEfficiency: 2 }, emptyPrestige);
|
|
||||||
expect(result.attackSpeed).toBeCloseTo(0.9025, 2); // 0.95^2
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Invocation / Pact skills', () => {
|
|
||||||
it('Invocation should multiply spellDamage', () => {
|
|
||||||
const result = computeStats({ invocation: 5 }, emptyPrestige);
|
|
||||||
expect(result.spellDamage).toBeCloseTo(1.27628, 2); // 1.05^5
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Pact Mastery should multiply pactMultiplier', () => {
|
|
||||||
const result = computeStats({ pactMastery: 5 }, emptyPrestige);
|
|
||||||
expect(result.pactMultiplier).toBeCloseTo(1.61051, 2); // 1.1^5
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Guardian Lore should multiply guardianDamage', () => {
|
|
||||||
const result = computeStats({ guardianLore: 3 }, emptyPrestige);
|
|
||||||
expect(result.guardianDamage).toBeCloseTo(1.728, 2); // 1.2^3
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Element capacity skills', () => {
|
|
||||||
it('Fire Mana Cap should increase fireCap', () => {
|
|
||||||
const result = computeStats({ fireManaCap: 5 }, emptyPrestige);
|
|
||||||
expect(result.fireCap).toBe(5 * 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Multiple element caps should contribute to elementCap', () => {
|
|
||||||
const result = computeStats({ fireManaCap: 3, waterManaCap: 2 }, emptyPrestige);
|
|
||||||
// fireCap=30, waterCap=20 -> elementCap = 10 + 30 + 20 = 60
|
|
||||||
expect(result.elementCap).toBe(60);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('All base element caps should contribute', () => {
|
|
||||||
const result = computeStats({
|
|
||||||
fireManaCap: 10, waterManaCap: 10, airManaCap: 10, earthManaCap: 10,
|
|
||||||
}, emptyPrestige);
|
|
||||||
// Each adds 10*10=100, total 400 + base 10 = 410
|
|
||||||
expect(result.elementCap).toBe(410);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Hybrid skills', () => {
|
|
||||||
it('Pact-Weaving should multiply enchantPower', () => {
|
|
||||||
const result = computeStats({ pactWeaving: 3 }, emptyPrestige);
|
|
||||||
expect(result.enchantPower).toBeCloseTo(1.331, 2); // 1.1^3
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Guardian Constructs should affect golemDamage and golemDuration', () => {
|
|
||||||
const result = computeStats({ guardianConstructs: 2 }, emptyPrestige);
|
|
||||||
expect(result.golemDamage).toBeCloseTo(1.3225, 2); // 1.15^2
|
|
||||||
expect(result.golemDuration).toBe(1.5); // add 0.25*2=0.5, but cap floor is 1, so 1 + 0.5 = 1.5
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Enchanted Golemancy should affect enchantPower and golemDamage', () => {
|
|
||||||
const result = computeStats({ enchantedGolemancy: 3 }, emptyPrestige);
|
|
||||||
expect(result.enchantPower).toBeCloseTo(1.157625, 2); // 1.05^3
|
|
||||||
expect(result.golemDamage).toBeCloseTo(1.331, 2); // 1.1^3
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Prestige upgrades', () => {
|
|
||||||
it('manaWell prestige should increase maxMana', () => {
|
|
||||||
const result = computeStats({}, { manaWell: 3 });
|
|
||||||
expect(result.maxMana).toBe(100 + 3 * 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('manaFlow prestige should increase manaRegen', () => {
|
|
||||||
const result = computeStats({}, { manaFlow: 2 });
|
|
||||||
expect(result.manaRegen).toBe(2 + 2 * 0.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('elementalAttune prestige should increase elementCap', () => {
|
|
||||||
const result = computeStats({}, { elementalAttune: 4 });
|
|
||||||
expect(result.elementCap).toBe(10 + 4 * 25);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('pactBinding prestige should increase pactMultiplier', () => {
|
|
||||||
const result = computeStats({}, { pactBinding: 2 });
|
|
||||||
expect(result.pactMultiplier).toBe(1 + 2 * 0.1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('insightAmp prestige should multiply insightGain', () => {
|
|
||||||
const result = computeStats({}, { insightAmp: 2 });
|
|
||||||
expect(result.insightGain).toBe(1 + 1 * 0.25 * 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('prestige should stack with skills', () => {
|
|
||||||
const result = computeStats({ manaWell: 5 }, { manaWell: 3 });
|
|
||||||
expect(result.maxMana).toBe(100 + 5 * 100 + 3 * 500);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Skill stacking', () => {
|
|
||||||
it('should correctly stack multiple skills', () => {
|
|
||||||
const result = computeStats({
|
|
||||||
manaWell: 3,
|
|
||||||
manaFlow: 2,
|
|
||||||
manaTap: 1,
|
|
||||||
precision: 4,
|
|
||||||
}, emptyPrestige);
|
|
||||||
|
|
||||||
expect(result.maxMana).toBe(100 + 300);
|
|
||||||
expect(result.manaRegen).toBe(2 + 2);
|
|
||||||
expect(result.clickMana).toBe(1 + 1);
|
|
||||||
expect(result.critChance).toBe(0.2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle all skills with effects at once without interference', () => {
|
|
||||||
// Only test skills that have effects defined (skip research skills with empty effects)
|
|
||||||
const allSkillsWithEffects: Record<string, number> = {};
|
|
||||||
for (const [id, def] of Object.entries(SKILLS_V2)) {
|
|
||||||
if (def.effects.length > 0) {
|
|
||||||
allSkillsWithEffects[id] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const result = computeStats(allSkillsWithEffects, emptyPrestige);
|
|
||||||
// Should not throw and should produce reasonable values
|
|
||||||
expect(result.maxMana).toBeGreaterThan(0);
|
|
||||||
expect(result.manaRegen).toBeGreaterThan(0);
|
|
||||||
expect(result.baseDamage).toBeGreaterThan(0);
|
|
||||||
expect(result.elementCap).toBeGreaterThanOrEqual(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('edge cases', () => {
|
|
||||||
it('should ignore negative levels (no effect applied)', () => {
|
|
||||||
const result = computeStats({}, emptyPrestige);
|
|
||||||
expect(result.maxMana).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore unknown skill IDs', () => {
|
|
||||||
const result = computeStats({ unknownSkill: 5 } as any, emptyPrestige);
|
|
||||||
expect(result.maxMana).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clamp critChance to 1.0', () => {
|
|
||||||
const result = computeStats({ precision: 100 } as any, emptyPrestige);
|
|
||||||
expect(result.critChance).toBe(1.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clamp armorPierce to 1.0', () => {
|
|
||||||
const result = computeStats({ armorPiercing: 100 } as any, emptyPrestige);
|
|
||||||
expect(result.armorPierce).toBe(1.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clamp attackSpeed minimum to 0.1', () => {
|
|
||||||
const result = computeStats({ attackSpeed: 100 } as any, emptyPrestige);
|
|
||||||
expect(result.attackSpeed).toBe(0.1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clamp maxMana minimum to 1', () => {
|
|
||||||
const result = computeStats({}, emptyPrestige);
|
|
||||||
expect(result.maxMana).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clamp baseDamage minimum to 1', () => {
|
|
||||||
const result = computeStats({}, emptyPrestige);
|
|
||||||
expect(result.baseDamage).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getBaseSkillId()', () => {
|
|
||||||
it('should strip _tN suffix for tiered skills', () => {
|
|
||||||
expect(getBaseSkillId('manaWell_t2')).toBe('manaWell');
|
|
||||||
expect(getBaseSkillId('manaWell_t5')).toBe('manaWell');
|
|
||||||
expect(getBaseSkillId('quickLearner_t3')).toBe('quickLearner');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return same ID for non-tiered skills', () => {
|
|
||||||
expect(getBaseSkillId('manaWell')).toBe('manaWell');
|
|
||||||
expect(getBaseSkillId('fireManaCap')).toBe('fireManaCap');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('hasPrerequisites()', () => {
|
|
||||||
it('should return true when no prerequisites', () => {
|
|
||||||
expect(hasPrerequisites({}, undefined)).toBe(true);
|
|
||||||
expect(hasPrerequisites({}, {})).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true when prerequisites are met', () => {
|
|
||||||
expect(hasPrerequisites({ manaWell: 5 }, { manaWell: 3 })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when prerequisites are not met', () => {
|
|
||||||
expect(hasPrerequisites({ manaWell: 2 }, { manaWell: 3 })).toBe(false);
|
|
||||||
expect(hasPrerequisites({}, { manaWell: 1 })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should check multiple prerequisites', () => {
|
|
||||||
expect(hasPrerequisites({ a: 2, b: 3 }, { a: 1, b: 2 })).toBe(true);
|
|
||||||
expect(hasPrerequisites({ a: 2, b: 1 }, { a: 1, b: 2 })).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SKILLS_V2', () => {
|
|
||||||
it('should have manaWell defined', () => {
|
|
||||||
expect(SKILLS_V2.manaWell).toBeDefined();
|
|
||||||
expect(SKILLS_V2.manaWell.id).toBe('manaWell');
|
|
||||||
expect(SKILLS_V2.manaWell.maxLevel).toBe(10);
|
|
||||||
expect(SKILLS_V2.manaWell.effects).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have all core skills defined', () => {
|
|
||||||
const coreSkills = ['manaWell', 'manaFlow', 'quickLearner', 'focusedMind', 'meditation'];
|
|
||||||
for (const id of coreSkills) {
|
|
||||||
expect(SKILLS_V2[id]).toBeDefined();
|
|
||||||
expect(SKILLS_V2[id].effects.length).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have correct manaWell effect', () => {
|
|
||||||
const effect = SKILLS_V2.manaWell.effects[0];
|
|
||||||
expect(effect.stat).toBe('maxMana');
|
|
||||||
expect(effect.mode).toBe('add');
|
|
||||||
expect(effect.valuePerLevel).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have correct manaFlow effect', () => {
|
|
||||||
const effect = SKILLS_V2.manaFlow.effects[0];
|
|
||||||
expect(effect.stat).toBe('manaRegen');
|
|
||||||
expect(effect.mode).toBe('add');
|
|
||||||
expect(effect.valuePerLevel).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle prerequisite fields', () => {
|
|
||||||
expect(SKILLS_V2.manaOverflow.prerequisites).toEqual({ manaWell: 3 });
|
|
||||||
expect(SKILLS_V2.deepTrance.prerequisites).toEqual({ meditation: 1 });
|
|
||||||
expect(SKILLS_V2.manaTap.prerequisites).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle attunementRequired fields', () => {
|
|
||||||
expect(SKILLS_V2.enchanting.attunementRequired).toBe('enchanter');
|
|
||||||
expect(SKILLS_V2.invocation.attunementRequired).toBe('invoker');
|
|
||||||
expect(SKILLS_V2.golemMastery.attunementRequired).toBe('fabricator');
|
|
||||||
expect(SKILLS_V2.manaWell.attunementRequired).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ computeStats() and skill v2 tests defined.');
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import { useCraftingStore } from '@/lib/game/stores';
|
|
||||||
|
|
||||||
describe('useCraftingStore - Equipment Actions', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Reset to initial state
|
|
||||||
useCraftingStore.setState({
|
|
||||||
equipmentInstances: {},
|
|
||||||
equippedInstances: {},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('equipItem sets equippedInstances[slot] to instanceId', () => {
|
|
||||||
const instanceId = 'test-instance-1';
|
|
||||||
const slot = 'mainHand';
|
|
||||||
|
|
||||||
// First, add the instance to equipmentInstances
|
|
||||||
useCraftingStore.setState((state) => ({
|
|
||||||
equipmentInstances: {
|
|
||||||
...state.equipmentInstances,
|
|
||||||
[instanceId]: {
|
|
||||||
id: instanceId,
|
|
||||||
equipmentId: 'test-equip',
|
|
||||||
name: 'Test Sword',
|
|
||||||
rarity: 'common',
|
|
||||||
level: 1,
|
|
||||||
upgrades: [],
|
|
||||||
createdAt: Date.now(),
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Equip the item - note: equipItem might take (slot, instanceId)
|
|
||||||
const state = useCraftingStore.getState();
|
|
||||||
if (state.equipItem) {
|
|
||||||
state.equipItem(slot, instanceId);
|
|
||||||
expect(useCraftingStore.getState().equippedInstances[slot]).toBe(instanceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('unequipItem sets equippedInstances[slot] to null', () => {
|
|
||||||
const instanceId = 'test-instance-1';
|
|
||||||
const slot = 'mainHand';
|
|
||||||
|
|
||||||
// First equip the item
|
|
||||||
useCraftingStore.setState((state) => ({
|
|
||||||
equipmentInstances: {
|
|
||||||
...state.equipmentInstances,
|
|
||||||
[instanceId]: {
|
|
||||||
id: instanceId,
|
|
||||||
equipmentId: 'test-equip',
|
|
||||||
name: 'Test Sword',
|
|
||||||
rarity: 'common',
|
|
||||||
level: 1,
|
|
||||||
upgrades: [],
|
|
||||||
createdAt: Date.now(),
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
equippedInstances: {
|
|
||||||
...state.equippedInstances,
|
|
||||||
[slot]: instanceId,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Unequip the item
|
|
||||||
const state = useCraftingStore.getState();
|
|
||||||
if (state.unequipItem) {
|
|
||||||
state.unequipItem(slot);
|
|
||||||
expect(useCraftingStore.getState().equippedInstances[slot]).toBeNull();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteEquipmentInstance removes from both equippedInstances and equipmentInstances', () => {
|
|
||||||
const instanceId = 'test-instance-1';
|
|
||||||
const slot = 'mainHand';
|
|
||||||
|
|
||||||
// Add and equip the item
|
|
||||||
useCraftingStore.setState((state) => ({
|
|
||||||
equipmentInstances: {
|
|
||||||
...state.equipmentInstances,
|
|
||||||
[instanceId]: {
|
|
||||||
id: instanceId,
|
|
||||||
equipmentId: 'test-equip',
|
|
||||||
name: 'Test Sword',
|
|
||||||
rarity: 'common',
|
|
||||||
level: 1,
|
|
||||||
upgrades: [],
|
|
||||||
createdAt: Date.now(),
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
equippedInstances: {
|
|
||||||
...state.equippedInstances,
|
|
||||||
[slot]: instanceId,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Delete the item
|
|
||||||
const state = useCraftingStore.getState();
|
|
||||||
if (state.deleteEquipmentInstance) {
|
|
||||||
state.deleteEquipmentInstance(instanceId);
|
|
||||||
const newState = useCraftingStore.getState();
|
|
||||||
expect(newState.equipmentInstances[instanceId]).toBeUndefined();
|
|
||||||
expect(newState.equippedInstances[slot]).toBeNull();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
/**
|
|
||||||
* Combat Calculation Tests for Stores Index
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { calcDamage, getFloorMaxHP, getFloorElement } from '@/lib/game/stores/index';
|
|
||||||
import { GUARDIANS } from '@/lib/game/constants';
|
|
||||||
import type { GameState } from '../../../types';
|
|
||||||
|
|
||||||
function createMockState(overrides: Partial<GameState> = {}): GameState {
|
|
||||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
|
||||||
['fire', 'water', 'air', 'earth', 'light', 'dark', 'death', 'transference', 'metal', 'sand', 'crystal', 'stellar', 'void', 'lightning'].forEach((k) => {
|
|
||||||
elements[k] = { current: 0, max: 10, unlocked: ['fire', 'water', 'air', 'earth'].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,
|
|
||||||
maxFloorReached: 1,
|
|
||||||
defeatedGuardians: [],
|
|
||||||
signedPacts: [],
|
|
||||||
pactSlots: 1,
|
|
||||||
pactRitualFloor: null,
|
|
||||||
pactRitualProgress: 0,
|
|
||||||
activeSpell: 'manaBolt',
|
|
||||||
currentAction: 'meditate',
|
|
||||||
castProgress: 0,
|
|
||||||
spells: {
|
|
||||||
manaBolt: { learned: true, level: 1, studyProgress: 0 },
|
|
||||||
fireball: { learned: true, level: 1, studyProgress: 0 },
|
|
||||||
waterJet: { learned: true, level: 1, studyProgress: 0 },
|
|
||||||
},
|
|
||||||
skills: {},
|
|
||||||
skillProgress: {},
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
paidStudySkills: {},
|
|
||||||
currentStudyTarget: null,
|
|
||||||
parallelStudyTarget: null,
|
|
||||||
insight: 0,
|
|
||||||
totalInsight: 0,
|
|
||||||
prestigeUpgrades: {},
|
|
||||||
memorySlots: 3,
|
|
||||||
memories: [],
|
|
||||||
incursionStrength: 0,
|
|
||||||
containmentWards: 0,
|
|
||||||
log: [],
|
|
||||||
loopInsight: 0,
|
|
||||||
equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null },
|
|
||||||
inventory: [],
|
|
||||||
blueprints: {},
|
|
||||||
schedule: [],
|
|
||||||
autoSchedule: false,
|
|
||||||
studyQueue: [],
|
|
||||||
craftQueue: [],
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
equippedInstances: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory1: null, accessory2: null },
|
|
||||||
equipmentInstances: {},
|
|
||||||
...overrides,
|
|
||||||
} as GameState;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Combat Calculations', () => {
|
|
||||||
describe('calcDamage', () => {
|
|
||||||
it('should return spell base damage with no bonuses', () => {
|
|
||||||
const state = createMockState();
|
|
||||||
const dmg = calcDamage(state, 'manaBolt');
|
|
||||||
expect(dmg).toBeGreaterThanOrEqual(5); // Base damage (can be higher with crit)
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have elemental bonuses', () => {
|
|
||||||
const state = createMockState({
|
|
||||||
spells: {
|
|
||||||
manaBolt: { learned: true, level: 1 },
|
|
||||||
fireball: { learned: true, level: 1 },
|
|
||||||
waterJet: { learned: true, level: 1 },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Test elemental bonus by comparing same spell vs different elements
|
|
||||||
// Fireball vs fire floor (same element, +25%) vs vs air floor (neutral)
|
|
||||||
let fireVsFire = 0, fireVsAir = 0;
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
fireVsFire += calcDamage(state, 'fireball', 'fire');
|
|
||||||
fireVsAir += calcDamage(state, 'fireball', 'air');
|
|
||||||
}
|
|
||||||
const sameAvg = fireVsFire / 100;
|
|
||||||
const neutralAvg = fireVsAir / 100;
|
|
||||||
// Same element should do more damage
|
|
||||||
expect(sameAvg).toBeGreaterThan(neutralAvg * 1.1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getFloorMaxHP', () => {
|
|
||||||
it('should return guardian HP for guardian floors', () => {
|
|
||||||
expect(getFloorMaxHP(10)).toBe(GUARDIANS[10].hp);
|
|
||||||
expect(getFloorMaxHP(100)).toBe(GUARDIANS[100].hp);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should scale HP for non-guardian floors', () => {
|
|
||||||
expect(getFloorMaxHP(1)).toBeGreaterThan(0);
|
|
||||||
expect(getFloorMaxHP(5)).toBeGreaterThan(getFloorMaxHP(1));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getFloorElement', () => {
|
|
||||||
it('should cycle through elements in order', () => {
|
|
||||||
// FLOOR_ELEM_CYCLE has 7 elements: fire, water, air, earth, light, dark, death
|
|
||||||
expect(getFloorElement(1)).toBe('fire');
|
|
||||||
expect(getFloorElement(2)).toBe('water');
|
|
||||||
expect(getFloorElement(3)).toBe('air');
|
|
||||||
expect(getFloorElement(4)).toBe('earth');
|
|
||||||
expect(getFloorElement(5)).toBe('light');
|
|
||||||
expect(getFloorElement(6)).toBe('dark');
|
|
||||||
expect(getFloorElement(7)).toBe('death');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should wrap around after 7 floors', () => {
|
|
||||||
// Floor 8 should be fire (wraps around)
|
|
||||||
expect(getFloorElement(8)).toBe('fire');
|
|
||||||
expect(getFloorElement(9)).toBe('water');
|
|
||||||
expect(getFloorElement(15)).toBe('fire'); // (15-1) % 7 = 0
|
|
||||||
expect(getFloorElement(16)).toBe('water'); // (16-1) % 7 = 1
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Combat calculation tests defined.');
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
/**
|
|
||||||
* Skill and Prestige Definition Tests for Stores Index
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { SKILLS_DEF, PRESTIGE_DEF, GUARDIANS } from '@/lib/game/constants';
|
|
||||||
import type { GameState } from '../../../types';
|
|
||||||
|
|
||||||
describe('Skill Definitions', () => {
|
|
||||||
it('all skills should have valid categories', () => {
|
|
||||||
const validCategories = ['mana', 'study', 'research', 'ascension', 'enchant',
|
|
||||||
'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'craft', 'hybrid'];
|
|
||||||
Object.values(SKILLS_DEF).forEach(skill => {
|
|
||||||
expect(validCategories).toContain(skill.cat);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('all skills should have reasonable study times', () => {
|
|
||||||
Object.values(SKILLS_DEF).forEach(skill => {
|
|
||||||
expect(skill.studyTime).toBeGreaterThan(0);
|
|
||||||
expect(skill.studyTime).toBeLessThanOrEqual(72);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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', () => {
|
|
||||||
// Need to import compute functions - this test is simpler in the actual refactored files
|
|
||||||
// For now, just test the definition
|
|
||||||
expect(PRESTIGE_DEF.manaWell).toBeDefined();
|
|
||||||
expect(PRESTIGE_DEF.manaWell.cost).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Elemental Attunement prestige should add 25 element cap', () => {
|
|
||||||
expect(PRESTIGE_DEF.elementalAttune).toBeDefined();
|
|
||||||
expect(PRESTIGE_DEF.elementalAttune.cost).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Guardian Definitions', () => {
|
|
||||||
it('should have guardians on expected floors (no floor 70)', () => {
|
|
||||||
// Floor 70 was removed from the game
|
|
||||||
[10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => {
|
|
||||||
expect(GUARDIANS[floor]).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have increasing HP', () => {
|
|
||||||
let prevHP = 0;
|
|
||||||
[10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => {
|
|
||||||
expect(GUARDIANS[floor].hp).toBeGreaterThan(prevHP);
|
|
||||||
prevHP = GUARDIANS[floor].hp;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have boons defined', () => {
|
|
||||||
Object.values(GUARDIANS).forEach(guardian => {
|
|
||||||
expect(guardian.boons).toBeDefined();
|
|
||||||
expect(guardian.boons.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have pact costs defined', () => {
|
|
||||||
Object.values(GUARDIANS).forEach(guardian => {
|
|
||||||
expect(guardian.pactCost).toBeGreaterThan(0);
|
|
||||||
expect(guardian.pactTime).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Skill, prestige, and guardian definition tests defined.');
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mana Calculation Tests for Stores Index
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { computeMaxMana, computeRegen, computeClickMana, computeElementMax } from '@/lib/game/stores/index';
|
|
||||||
import type { GameState } from '../../../types';
|
|
||||||
import { ELEMENTS } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
function createMockState(overrides: Partial<GameState> = {}): GameState {
|
|
||||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
|
||||||
Object.keys(ELEMENTS).forEach((k) => {
|
|
||||||
elements[k] = { current: 0, max: 10, unlocked: ['fire', 'water', 'air', 'earth'].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,
|
|
||||||
maxFloorReached: 1,
|
|
||||||
defeatedGuardians: [],
|
|
||||||
signedPacts: [],
|
|
||||||
pactSlots: 1,
|
|
||||||
pactRitualFloor: null,
|
|
||||||
pactRitualProgress: 0,
|
|
||||||
activeSpell: 'manaBolt',
|
|
||||||
currentAction: 'meditate',
|
|
||||||
castProgress: 0,
|
|
||||||
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
|
||||||
skills: {},
|
|
||||||
skillProgress: {},
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
paidStudySkills: {},
|
|
||||||
currentStudyTarget: null,
|
|
||||||
parallelStudyTarget: null,
|
|
||||||
insight: 0,
|
|
||||||
totalInsight: 0,
|
|
||||||
prestigeUpgrades: {},
|
|
||||||
memorySlots: 3,
|
|
||||||
memories: [],
|
|
||||||
incursionStrength: 0,
|
|
||||||
containmentWards: 0,
|
|
||||||
log: [],
|
|
||||||
loopInsight: 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,
|
|
||||||
},
|
|
||||||
equippedInstances: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory1: null, accessory2: null },
|
|
||||||
equipmentInstances: {},
|
|
||||||
...overrides,
|
|
||||||
} as GameState;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Mana Calculations', () => {
|
|
||||||
describe('computeMaxMana', () => {
|
|
||||||
it('should return base mana with no upgrades', () => {
|
|
||||||
const state = createMockState();
|
|
||||||
expect(computeMaxMana(state)).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add mana from manaWell skill', () => {
|
|
||||||
const state = createMockState({ skills: { manaWell: 5 } });
|
|
||||||
expect(computeMaxMana(state)).toBe(100 + 5 * 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add mana from prestige upgrades', () => {
|
|
||||||
const state = createMockState({ prestigeUpgrades: { manaWell: 3 } });
|
|
||||||
expect(computeMaxMana(state)).toBe(100 + 3 * 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack manaWell skill and prestige', () => {
|
|
||||||
const state = createMockState({
|
|
||||||
skills: { manaWell: 5 },
|
|
||||||
prestigeUpgrades: { manaWell: 2 },
|
|
||||||
});
|
|
||||||
expect(computeMaxMana(state)).toBe(100 + 5 * 100 + 2 * 500);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('computeRegen', () => {
|
|
||||||
it('should return base regen with no upgrades', () => {
|
|
||||||
// Base regen is 2 (attunement regen is added separately in the store)
|
|
||||||
const state = createMockState();
|
|
||||||
expect(computeRegen(state)).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add regen from manaFlow skill', () => {
|
|
||||||
// Base 2 + manaFlow 5
|
|
||||||
const state = createMockState({ skills: { manaFlow: 5 } });
|
|
||||||
expect(computeRegen(state)).toBe(2 + 5 * 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add regen from manaSpring skill', () => {
|
|
||||||
// Base 2 + manaSpring 2
|
|
||||||
const state = createMockState({ skills: { manaSpring: 1 } });
|
|
||||||
expect(computeRegen(state)).toBe(2 + 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should multiply by temporal echo prestige', () => {
|
|
||||||
const state = createMockState({ prestigeUpgrades: { temporalEcho: 2 } });
|
|
||||||
// Base 2 * 1.2 = 2.4
|
|
||||||
expect(computeRegen(state)).toBe(2 * 1.2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('computeClickMana', () => {
|
|
||||||
it('should return base click mana with no upgrades', () => {
|
|
||||||
const state = createMockState();
|
|
||||||
expect(computeClickMana(state)).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add mana from manaTap skill', () => {
|
|
||||||
const state = createMockState({ skills: { manaTap: 1 } });
|
|
||||||
expect(computeClickMana(state)).toBe(1 + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add mana from manaSurge skill', () => {
|
|
||||||
const state = createMockState({ skills: { manaSurge: 1 } });
|
|
||||||
expect(computeClickMana(state)).toBe(1 + 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stack manaTap and manaSurge', () => {
|
|
||||||
const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } });
|
|
||||||
expect(computeClickMana(state)).toBe(1 + 1 + 3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('computeElementMax', () => {
|
|
||||||
it('should return base element cap with no upgrades', () => {
|
|
||||||
const state = createMockState();
|
|
||||||
expect(computeElementMax(state)).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add cap from elemAttune skill', () => {
|
|
||||||
const state = createMockState({ skills: { elemAttune: 5 } });
|
|
||||||
expect(computeElementMax(state)).toBe(10 + 5 * 50);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add cap from prestige upgrades', () => {
|
|
||||||
const state = createMockState({ prestigeUpgrades: { elementalAttune: 3 } });
|
|
||||||
expect(computeElementMax(state)).toBe(10 + 3 * 25);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Mana calculation tests defined.');
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
/**
|
|
||||||
* Meditation, Insight, and Incursion Tests for Stores Index
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { getMeditationBonus, calcInsight, getIncursionStrength } from '@/lib/game/stores/index';
|
|
||||||
import { MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants';
|
|
||||||
import type { GameState } from '../../../types';
|
|
||||||
|
|
||||||
function createMockState(overrides: Partial<GameState> = {}): GameState {
|
|
||||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
|
||||||
['fire', 'water', 'air', 'earth', 'light', 'dark', 'death', 'transference'].forEach((k) => {
|
|
||||||
elements[k] = { current: 0, max: 10, unlocked: ['fire', 'water', 'air', 'earth'].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,
|
|
||||||
maxFloorReached: 1,
|
|
||||||
defeatedGuardians: [],
|
|
||||||
signedPacts: [],
|
|
||||||
pactSlots: 1,
|
|
||||||
pactRitualFloor: null,
|
|
||||||
pactRitualProgress: 0,
|
|
||||||
activeSpell: 'manaBolt',
|
|
||||||
currentAction: 'meditate',
|
|
||||||
castProgress: 0,
|
|
||||||
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
|
||||||
skills: {},
|
|
||||||
skillProgress: {},
|
|
||||||
skillUpgrades: {},
|
|
||||||
skillTiers: {},
|
|
||||||
paidStudySkills: {},
|
|
||||||
currentStudyTarget: null,
|
|
||||||
parallelStudyTarget: null,
|
|
||||||
insight: 0,
|
|
||||||
totalInsight: 0,
|
|
||||||
prestigeUpgrades: {},
|
|
||||||
memorySlots: 3,
|
|
||||||
memories: [],
|
|
||||||
incursionStrength: 0,
|
|
||||||
containmentWards: 0,
|
|
||||||
log: [],
|
|
||||||
loopInsight: 0,
|
|
||||||
equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null },
|
|
||||||
inventory: [],
|
|
||||||
blueprints: {},
|
|
||||||
schedule: [],
|
|
||||||
autoSchedule: false,
|
|
||||||
studyQueue: [],
|
|
||||||
craftQueue: [],
|
|
||||||
attunements: {
|
|
||||||
enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 },
|
|
||||||
},
|
|
||||||
golemancy: { enabledGolems: [], summonedGolems: [], lastSummonFloor: 0 },
|
|
||||||
equippedInstances: {},
|
|
||||||
...overrides,
|
|
||||||
} as GameState;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Meditation Tests ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Meditation Bonus', () => {
|
|
||||||
describe('getMeditationBonus', () => {
|
|
||||||
it('should start at 1x with no meditation', () => {
|
|
||||||
expect(getMeditationBonus(0, {})).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ramp up over time', () => {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Insight Tests ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Insight Calculations', () => {
|
|
||||||
describe('calcInsight', () => {
|
|
||||||
it('should calculate insight from floor progress', () => {
|
|
||||||
const state = createMockState({ maxFloorReached: 10 });
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
expect(insight).toBe(10 * 15);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should calculate insight from mana gathered', () => {
|
|
||||||
const state = createMockState({ totalManaGathered: 5000 });
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
// 1*15 + 5000/500 + 0 = 25
|
|
||||||
expect(insight).toBe(25);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should calculate insight from signed pacts', () => {
|
|
||||||
const state = createMockState({ signedPacts: [10, 20] });
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
// 1*15 + 0 + 2*150 = 315
|
|
||||||
expect(insight).toBe(315);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should multiply by insightAmp prestige', () => {
|
|
||||||
const state = createMockState({
|
|
||||||
maxFloorReached: 10,
|
|
||||||
prestigeUpgrades: { insightAmp: 2 },
|
|
||||||
});
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
expect(insight).toBe(Math.floor(10 * 15 * 1.5));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should multiply by insightHarvest skill', () => {
|
|
||||||
const state = createMockState({
|
|
||||||
maxFloorReached: 10,
|
|
||||||
skills: { insightHarvest: 3 },
|
|
||||||
});
|
|
||||||
const insight = calcInsight(state);
|
|
||||||
expect(insight).toBe(Math.floor(10 * 15 * 1.3));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Incursion Tests ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
describe('Incursion Strength', () => {
|
|
||||||
describe('getIncursionStrength', () => {
|
|
||||||
it('should be 0 before incursion start day', () => {
|
|
||||||
expect(getIncursionStrength(19, 0)).toBe(0);
|
|
||||||
expect(getIncursionStrength(19, 23)).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start at incursion start day', () => {
|
|
||||||
expect(getIncursionStrength(INCURSION_START_DAY, 1)).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should increase over time', () => {
|
|
||||||
const early = getIncursionStrength(INCURSION_START_DAY, 12);
|
|
||||||
const late = getIncursionStrength(25, 12);
|
|
||||||
expect(late).toBeGreaterThan(early);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should cap at 95%', () => {
|
|
||||||
const strength = getIncursionStrength(MAX_DAY, 23);
|
|
||||||
expect(strength).toBeLessThanOrEqual(0.95);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Meditation, insight, and incursion tests defined.');
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* Spell Cost Tests for Stores Index
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { canAffordSpellCost } from '@/lib/game/stores/index';
|
|
||||||
import { rawCost, elemCost } from '@/lib/game/constants';
|
|
||||||
|
|
||||||
describe('Spell Cost System', () => {
|
|
||||||
describe('rawCost', () => {
|
|
||||||
it('should create a raw mana cost', () => {
|
|
||||||
const cost = rawCost(10);
|
|
||||||
expect(cost.type).toBe('raw');
|
|
||||||
expect(cost.amount).toBe(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('elemCost', () => {
|
|
||||||
it('should create an elemental mana cost', () => {
|
|
||||||
const cost = elemCost('fire', 5);
|
|
||||||
expect(cost.type).toBe('element');
|
|
||||||
expect(cost.element).toBe('fire');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('canAffordSpellCost', () => {
|
|
||||||
it('should allow raw mana costs when enough raw mana', () => {
|
|
||||||
const cost = { type: 'raw' as const, amount: 10 };
|
|
||||||
const elements = { fire: { current: 0, max: 10, unlocked: true } };
|
|
||||||
expect(canAffordSpellCost(cost, 100, elements)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should deny raw mana costs when not enough raw mana', () => {
|
|
||||||
const cost = { type: 'raw' as const, amount: 100 };
|
|
||||||
const elements = { fire: { current: 0, max: 10, unlocked: true } };
|
|
||||||
expect(canAffordSpellCost(cost, 50, elements)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow elemental costs when enough element mana', () => {
|
|
||||||
const cost = { type: 'element' as const, element: 'fire', amount: 5 };
|
|
||||||
const elements = { fire: { current: 10, max: 10, unlocked: true } };
|
|
||||||
expect(canAffordSpellCost(cost, 0, elements)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should deny elemental costs when element not unlocked', () => {
|
|
||||||
const cost = { type: 'element' as const, element: 'fire', amount: 5 };
|
|
||||||
const elements = { fire: { current: 10, max: 10, unlocked: false } };
|
|
||||||
expect(canAffordSpellCost(cost, 100, elements)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Spell cost tests defined.');
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Study Speed Function Tests for Stores Index
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/stores/index';
|
|
||||||
|
|
||||||
describe('Study Speed Functions', () => {
|
|
||||||
describe('getStudySpeedMultiplier', () => {
|
|
||||||
it('should return 1 with no quickLearner skill', () => {
|
|
||||||
expect(getStudySpeedMultiplier({})).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should increase by 10% per level', () => {
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 1 })).toBe(1.1);
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 5 })).toBe(1.5);
|
|
||||||
expect(getStudySpeedMultiplier({ quickLearner: 10 })).toBe(2.0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getStudyCostMultiplier', () => {
|
|
||||||
it('should return 1 with no focusedMind skill', () => {
|
|
||||||
expect(getStudyCostMultiplier({})).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should decrease by 5% per level', () => {
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 1 })).toBe(0.95);
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 5 })).toBe(0.75);
|
|
||||||
expect(getStudyCostMultiplier({ focusedMind: 10 })).toBe(0.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Study speed function tests defined.');
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/**
|
|
||||||
* Utility Function Tests for Stores
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { fmt, fmtDec } from '@/lib/game/stores/index';
|
|
||||||
|
|
||||||
describe('Utility Functions', () => {
|
|
||||||
describe('fmt', () => {
|
|
||||||
it('should format small numbers', () => {
|
|
||||||
expect(fmt(0)).toBe('0');
|
|
||||||
expect(fmt(1)).toBe('1');
|
|
||||||
expect(fmt(999)).toBe('999');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format thousands', () => {
|
|
||||||
expect(fmt(1000)).toBe('1.0K');
|
|
||||||
expect(fmt(1500)).toBe('1.5K');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format millions', () => {
|
|
||||||
expect(fmt(1000000)).toBe('1.00M');
|
|
||||||
expect(fmt(1500000)).toBe('1.50M');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should format billions', () => {
|
|
||||||
expect(fmt(1000000000)).toBe('1.00B');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fmtDec', () => {
|
|
||||||
it('should format decimals', () => {
|
|
||||||
expect(fmtDec(1.234, 2)).toBe('1.23');
|
|
||||||
expect(fmtDec(1.5, 1)).toBe('1.5');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Utility function tests defined.');
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import { useManaStore } from '../manaStore';
|
|
||||||
import { useGameStore } from '../gameStore';
|
|
||||||
import { useAttunementStore } from '../attunementStore';
|
|
||||||
import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements';
|
|
||||||
|
|
||||||
describe('Mana Conversion Fix - Attunements deduct from regen, not pool', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Reset all stores
|
|
||||||
useManaStore.setState({
|
|
||||||
rawMana: 100,
|
|
||||||
elements: Object.fromEntries(
|
|
||||||
Object.keys(useManaStore.getState().elements).map(k => [
|
|
||||||
k,
|
|
||||||
{ current: 0, max: 10, unlocked: k === 'transference' }
|
|
||||||
])
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
useAttunementStore.setState({
|
|
||||||
attunements: {
|
|
||||||
enchanter: { active: true, level: 1 }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should deduct conversion cost from regen, not mana pool', () => {
|
|
||||||
const initialState = useManaStore.getState();
|
|
||||||
const initialRawMana = initialState.rawMana;
|
|
||||||
|
|
||||||
// Run a few ticks
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
useGameStore.getState().tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalState = useManaStore.getState();
|
|
||||||
// Mana pool should not be drained by conversion (only regen is reduced)
|
|
||||||
expect(finalState.rawMana).toBeGreaterThan(initialRawMana - 50); // Should not drop significantly
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reduce effective regen by conversion rate', () => {
|
|
||||||
// The conversion rate is subtracted from effective regen in gameStore.ts
|
|
||||||
// This is tested implicitly in the tick tests
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not get stuck below mana cap', () => {
|
|
||||||
useManaStore.setState({ rawMana: 99, elements: { ...useManaStore.getState().elements } });
|
|
||||||
|
|
||||||
// Run many ticks to approach mana cap
|
|
||||||
for (let i = 0; i < 1000; i++) {
|
|
||||||
useGameStore.getState().tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = useManaStore.getState();
|
|
||||||
// Should be able to reach mana cap (not stuck at cap -1)
|
|
||||||
expect(state.rawMana).toBeGreaterThan(98); // Should be near cap, not stuck at 99
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user