Compare commits

..

11 Commits

82 changed files with 3568 additions and 8069 deletions
+238
View File
@@ -0,0 +1,238 @@
# Mana-Loop Codebase Audit Report
**Date:** 2025-01-24
**Auditor:** Automated sub-agent analysis
## Executive Summary
The Mana-Loop codebase is a Next.js game application with a mixed architecture - partially refactored from a monolithic store (`store.ts`) to a modular store architecture (`stores/`). The codebase has significant technical debt in the form of dead code, unimplemented features, and files that have grown too large.
---
## Phase 1: Files Over 300 Lines
### Critical (Needs Immediate Refactoring)
| File | Lines | Purpose | Issue |
|------|-------|---------|-------|
| `src/lib/game/store.ts` | 2464 | Legacy monolithic game store | Monolith doing virtually all game logic |
| `src/lib/game/skill-evolution.ts` | 2312 | Skill talent trees | Single file with all skill trees; could split by category |
| `src/lib/game/constants.ts` | 1436 | Game constants & definitions | Mixes constants with content data (spells, skills, etc.) |
| `src/lib/game/types.ts` | 516 | TypeScript type definitions | Type definitions should be split by domain |
| `src/components/game/tabs/CraftingTab.tsx` | 965 | Crafting UI | Single component handling all crafting phases |
| `src/lib/game/crafting-slice.ts` | 847 | Old crafting store | Legacy file; newer version exists at `stores/craftingSlice.ts` |
| `src/lib/game/data/enchantment-effects.ts` | 846 | Enchantment effect definitions | Large but focused; acceptable as data file |
| `src/components/game/tabs/DebugTab.tsx` | 700 | Debug tools UI | Catch-all debug panel; could split by feature |
| `src/lib/game/store/craftingSlice.ts` | 644 | New crafting store | Cleaner but still combines state with business logic |
### Moderate (Should Refactor)
| File | Lines | Purpose | Issue |
|------|-------|---------|-------|
| `src/lib/game/attunements.ts` | 567 | Attunement system (OLD) | **DEAD CODE** - new version at `data/attunements.ts` |
| `src/components/game/tabs/GrimoireTab.tsx` | 567 | Pact system UI | Combines memory, pacts, boons |
| `src/components/game/StatsTab.tsx` | 551 | Stats display (OLD) | Too many unrelated stat categories |
| `src/components/game/tabs/StatsTab.tsx` | 545 | Stats display (NEW) | Same issues as above |
| `src/lib/game/stores/gameStore.ts` | 509 | Game coordinator store | Still coordinates too many systems |
| `src/lib/game/computed-stats.ts` | 492 | Computed statistics | Mixes utilities with stat calculations |
| `src/lib/game/data/golems.ts` | 471 | Golem definitions | Focused, acceptable size |
| `src/lib/game/data/equipment.ts` | 468 | Equipment definitions | Data file, acceptable size |
| `src/app/page.tsx` | 465 | Main game page | Should be thin shell; currently imports everything |
| `src/components/game/LootInventory.tsx` | 460 | Loot inventory UI | Handles multiple inventory types |
| `src/components/game/SkillsTab.tsx` | 418 | Skills UI (OLD) | Combines display with upgrade dialog |
| `src/components/game/GameContext.tsx` | 405 | Game context provider | Monolithic context combining all stores |
| `src/lib/game/upgrade-effects.ts` | 402 | Upgrade effect computation | Focused, acceptable |
| `src/components/game/tabs/EquipmentTab.tsx` | 393 | Equipment UI | Acceptable size |
| `src/lib/game/utils.ts` | 372 | Game utilities | Grown to include significant game logic |
| `src/components/game/tabs/SkillsTab.tsx` | 369 | Skills UI (NEW) | Same as old version |
| `src/lib/game/store/skillSlice.ts` | 346 | Skill store slice (OLD) | Legacy; newer version exists |
| `src/components/game/tabs/SpireTab.tsx` | 345 | Spire progression UI | Acceptable size |
| `src/components/game/tabs/GolemancyTab.tsx` | 338 | Golem management UI | Acceptable size |
| `src/lib/game/stores/skillStore.ts` | 332 | Skill store (NEW) | Acceptable size |
| `src/lib/game/store/computed.ts` | 322 | Computed values (OLD) | Legacy computed values |
| `src/components/game/SpireTab.tsx` | 320 | Spire UI (OLD) | Duplicate of tabs/ version |
### Test Files Over 300 Lines (Acceptable)
- `src/lib/game/store.test.ts` - 1042 lines
- `src/lib/game/__tests__/skills.test.ts` - 588 lines
- `src/lib/game/stores/__tests__/store-methods.test.ts` - 583 lines
- `src/lib/game/stores/index.test.ts` - 571 lines
- `src/lib/game/skills.test.ts` - 542 lines
- `src/lib/game/stores.test.ts` - 494 lines
- `src/lib/game/stores/__tests__/stores.test.ts` - 458 lines
- `src/lib/game/__tests__/skill-system.test.ts` - 347 lines
---
## Phase 1: TODO/FIXME and Unimplemented Features
### TODO/FIXME Comments
**No TODO or FIXME comments found in the codebase.**
### Unimplemented Special Effects
The following special effects are **registered but never applied** in game logic:
#### 1. `spellEcho10` (Enchantment Effect)
- **Location:** `src/lib/game/data/enchantment-effects.ts:530`
- **Definition:** 10% chance to cast a spell twice
- **Issue:** `hasSpecial(effects, 'spellEcho10')` is never checked in combat logic
- **Severity:** Medium - Equipment enchantment doesn't work
#### 2. `worldThread` and `worldWeb` (Skill Upgrades)
- **Location:** `src/lib/game/skill-evolution.ts:1662,1670`
- **Definitions:**
- `worldThread`: "Enchantments also apply 5% as world effects"
- `worldWeb`: "Enchantments also apply 10% as world effects"
- **Issue:** These specialIds are never checked with `hasSpecial()`
- **Severity:** High - Major feature gap ("world effects" mechanic unimplemented)
#### 3. Weapon Enchantment Specials
All defined but never checked:
| Special ID | Location | Purpose |
|------------|----------|---------|
| `fireBlade` | `constants.ts:864`, `enchantment-effects.ts:781` | Burn enemies |
| `frostBlade` | `constants.ts:877`, `enchantment-effects.ts:791` | Prevent enemy dodge |
| `lightningBlade` | `constants.ts:890`, `enchantment-effects.ts:801` | Pierce 30% armor |
| `voidBlade` | `constants.ts:903`, `enchantment-effects.ts:811` | +20% damage |
- **Severity:** High - Weapon enchantment system broken
#### 4. `comboMaster` (Special Effect)
- **Location:** `src/lib/game/upgrade-effects.ts:97,396`
- **Definition:** Every 5th attack deals 3x damage
- **Issue:** Hit counter tracking status unclear; combat handler may not check this
- **Severity:** Low - Implementation status unclear
### Effects Applied but Never Read
#### 1. `weaponManaMax` and `weaponManaRegen`
- **Location:** `src/lib/game/data/enchantment-effects.ts:566-616`
- **Issue:** Bonuses stored in `equipmentEffects.bonuses` but never read in `computeAllEffects()`
- **Severity:** Medium - Weapon mana system incomplete
#### 2. `insightGainMultiplier` from Equipment
- **Location:** `src/lib/game/effects.ts:127-129`
- **Issue:** Stored via type assertion but never read in `calcInsight()`
- **Severity:** Medium - Insight bonus from equipment doesn't work
#### 3. `guardianDamageMultiplier` from Equipment
- **Location:** `src/lib/game/effects.ts:131-132`
- **Issue:** Stored but unclear if ever read
- **Severity:** Low - Needs investigation
---
## Phase 1: Dead Code Analysis
### Confirmed Dead Code (Safe to Remove)
| File | Reason | Evidence |
|------|--------|----------|
| `src/lib/game/attunements.ts` | Old attunement system | No imports; new version at `data/attunements.ts` |
| `src/lib/game/navigation-slice.ts` | Old navigation system | No imports anywhere |
| `src/lib/db.ts` | Prisma client | No imports; possibly planned backend feature |
| `src/components/game/ComboMeter.tsx` | Unused component | No imports; references non-existent `ComboState` type |
| `src/components/game/layout/GameFooter.tsx` | Unused component | No imports |
| `src/components/game/shared/GameOverScreen.tsx` | Unused component | No imports |
| `src/components/game/shared/MemorySlotPicker.tsx` | Unused component | Only used by GameOverScreen |
### Duplicate Code to Clean
| Location | Issue | Action |
|----------|-------|--------|
| `src/components/game/types.ts` | Duplicate formatting functions | Remove; use `src/lib/game/formatting.ts` instead |
| `src/lib/game/store.ts` | Functions duplicated in `utils.ts` and `computed-stats.ts` | Consolidate during refactoring |
### Flagged for Review (String References/Dynamic Imports)
#### UI Components (Possibly Unused)
Many shadcn/ui components in `src/components/ui/` have no direct imports:
- `aspect-ratio.tsx`, `avatar.tsx`, `breadcrumb.tsx`, `calendar.tsx`, `carousel.tsx`, `chart.tsx`, `checkbox.tsx`, `collapsible.tsx`, `command.tsx`, `context-menu.tsx`, `drawer.tsx`, `dropdown-menu.tsx`, `form.tsx`, `hover-card.tsx`, `input-otp.tsx`, `label.tsx`, `menubar.tsx`, `navigation-menu.tsx`, `pagination.tsx`, `popover.tsx`, `radio-group.tsx`, `resizable.tsx`, `scroll-area.tsx` (USED), `select.tsx` (USED), `separator.tsx` (USED), `sheet.tsx`, `skeleton.tsx`, `slider.tsx`, `sonner.tsx`, `switch.tsx` (USED), `table.tsx`, `textarea.tsx`, `toggle.tsx`, `toggle-group.tsx`
**Note:** These might be used dynamically or via string references. Review before removing.
#### Test File Duplication
- `src/lib/game/skills.test.ts` (tests old store)
- `src/lib/game/store.test.ts` (tests old store)
- `src/lib/game/__tests__/skills.test.ts` (newer)
- `src/lib/game/stores/__tests__/store-methods.test.ts` (newer)
- `src/lib/game/stores/__tests__/stores.test.ts` (newer)
**Recommendation:** Old test files may be redundant if new tests provide adequate coverage.
---
## Architecture Notes
### Store Migration In Progress
The codebase shows an active migration from:
- **Old:** Monolithic `src/lib/game/store.ts` (2464 lines)
- **New:** Modular stores in `src/lib/game/stores/` (gameStore.ts, combatStore.ts, etc.)
Current status: Mixed usage - some components still import from old store.
### Data vs Logic Separation
Good separation in `src/lib/game/data/` for:
- `attunements.ts`
- `crafting-recipes.ts`
- `enchantment-effects.ts`
- `equipment.ts`
- `golems.ts`
- `loot-drops.ts`
- `achievements.ts`
---
## Recommended Action Plan
### Phase 2: Safe Deletions (High Priority)
1. Delete `src/lib/game/attunements.ts`
2. Delete `src/lib/game/navigation-slice.ts`
3. Delete `src/lib/db.ts`
4. Delete `src/components/game/ComboMeter.tsx`
5. Delete `src/components/game/layout/GameFooter.tsx`
6. Delete `src/components/game/shared/GameOverScreen.tsx`
7. Delete `src/components/game/shared/MemorySlotPicker.tsx`
8. Clean duplicate formatting functions from `src/components/game/types.ts`
### Phase 3: Refactor Large Files (Medium Priority)
1. **store.ts (2464 lines):** Complete migration to modular stores
2. **skill-evolution.ts (2312 lines):** Split by skill category
3. **constants.ts (1436 lines):** Split into domain-specific data files
4. **types.ts (516 lines):** Split by domain
5. **CraftingTab.tsx (965 lines):** Split by crafting phase
6. **page.tsx (465 lines):** Make thin shell
7. **GameContext.tsx (405 lines):** Simplify or remove need for monolithic context
### Phase 4: Implement Missing Effects (High Priority)
1. Implement weapon enchantment specials (`fireBlade`, `frostBlade`, `lightningBlade`, `voidBlade`)
2. Implement `worldThread`/`worldWeb` ("world effects" mechanic)
3. Wire up `spellEcho10` in combat logic
4. Apply `weaponManaMax`/`weaponManaRegen` or remove
5. Use `insightGainMultiplier` in `calcInsight()`
6. Verify `comboMaster` implementation
### Phase 5: Cleanup (Low Priority)
1. Review and remove unused UI components
2. Consolidate test files
3. Review relationship between `effects.ts` and `upgrade-effects.ts`
---
## Verification Checklist
After each phase:
- [ ] Game builds without errors
- [ ] Game runs correctly in browser
- [ ] All tabs functional
- [ ] No console errors
- [ ] Tests pass (if any)
- [ ] Commit and push changes
---
**Next Steps:** Begin Phase 2 - Safe Deletions
+149
View File
@@ -0,0 +1,149 @@
# Phase 1 Audit Report - Mana-Loop Game
## Overview
Audit completed on: 2024-04-24
Scope: `/home/user/repos/Mana-Loop/src/` directory
Initial build status: ✅ Passing (Next.js 16.2.4 build succeeds)
---
## 1. Files Over 300 Lines (Splitting Candidates)
| File Path | Line Count | Purpose | Split Candidate |
|-----------|------------|---------|-----------------|
| `src/lib/game/store.ts` | 2464 | Monolithic legacy game store | **YES (HIGH PRIORITY)** |
| `src/lib/game/skill-evolution.ts` | 2312 | All skill talent trees | **YES (HIGH PRIORITY)** |
| `src/lib/game/constants.ts` | 1436 | Mixed game constants | **YES (HIGH PRIORITY)** |
| `src/lib/game/data/enchantment-effects.ts` | 846 | Enchantment effect definitions | **YES (MEDIUM PRIORITY)** |
| `src/components/game/tabs/CraftingTab.tsx` | 965 | Crafting UI (4 stages) | **YES (MEDIUM PRIORITY)** |
| `src/components/game/tabs/DebugTab.tsx` | 700 | Debug/development UI | **YES (LOW PRIORITY)** |
| `src/lib/game/types.ts` | 516 | Central type definitions | **YES (MEDIUM PRIORITY)** |
| `src/lib/game/computed-stats.ts` | 492 | Mixed utility/stat functions | **YES (MEDIUM PRIORITY)** |
| `src/app/page.tsx` | 465 | Main game page component | **YES (LOW PRIORITY)** |
| `src/components/game/GameContext.tsx` | 405 | Unified store context | **YES (LOW PRIORITY)** |
| `src/lib/game/utils.ts` | 372 | Mixed utility functions | **YES (MEDIUM PRIORITY)** |
**Key Observation**: Project is mid-refactor from legacy `store.ts` to slice-based architecture (`lib/game/stores/`). Priority should be completing this migration.
---
## 2. Unused Exports (207 Total)
### Game Components (Never Imported)
- `src/components/game/ComboMeter.tsx` - `ComboMeter`
- `src/components/game/GrimoireTab.tsx` - `GrimoireTab`
- `src/components/game/layout/GameFooter.tsx` - `GameFooter`
- `src/components/game/layout/GameHeader.tsx` - `GameHeader`
- `src/components/game/layout/GameSidebar.tsx` - `GameSidebar`
- `src/components/game/shared/GameOverScreen.tsx` - `GameOverScreen`
### Tab Component Props (Unused Type Exports)
- All `Tabs/*TabProps` types in `src/components/game/tabs/` (12 total)
### Library Files (Unused Exports)
- `src/lib/game/attunements.ts` - 8 unused exports
- `src/lib/game/constants.ts` - 15+ unused exports
- `src/lib/game/computed-stats.ts` - 5 unused exports
- `src/lib/game/effects.ts` - 5 unused exports
- `src/lib/game/store.ts` - 7 unused exports
- `src/lib/game/types.ts` - 20+ unused type exports
- `src/lib/game/upgrade-effects.ts` - 6 unused exports
- `src/lib/game/utils.ts` - 2 unused exports
### UI Components (shadcn/ui - Never Imported)
28 unused shadcn/ui components in `src/components/ui/` (accordion, alert, calendar, chart, etc.)
---
## 3. Dead Imports (56 Total)
### Top-Level Components
- `src/app/page.tsx`: 5 dead imports (`fmtDec`, `getDamageBreakdown`, `SKILL_EVOLUTION_PATHS`, etc.)
- `src/components/game/SkillsTab.tsx`: 4 dead imports
- `src/components/game/SpellsTab.tsx`: 4 dead imports
- `src/components/game/StatsTab.tsx`: 5 dead imports
### Library Files
- `src/lib/game/store.ts`: 1 dead import
- `src/lib/game/store/combatSlice.ts`: 3 dead imports
- `src/lib/game/store/computed.ts`: 4 dead imports
- `src/lib/game/store/skillSlice.ts`: 3 dead imports
---
## 4. Unreferenced Files (57 Total)
### Game Components (Never Imported)
- 7 game components including `ComboMeter.tsx`, `GameFooter.tsx`, etc.
### UI Components
- 28 unused shadcn/ui components
### Library Files
- Old store architecture: `src/lib/game/store/*.ts` (10 files)
- Old stores: `src/lib/game/stores/*.ts` (8 files)
- Test files: `src/lib/game/*test.ts` (4 files)
- `src/lib/db.ts` (Prisma client, may be runtime-used)
---
## 5. TODO/FIXME Comments
**None found** in source code (only "Temp" substring matches from temporal/tempest references)
---
## 6. Unimplemented Stubs & Unused Effects
### Critical Issues
1. **`EXECUTIONER` effect used but not defined** (HIGH SEVERITY)
- Referenced in `store.ts:1085`, `combatSlice.ts:102`, `gameStore.ts:265`
- Missing from `SPECIAL_EFFECTS` in `upgrade-effects.ts`
- **Will cause runtime errors**
### Unused Effects
2. **51/59 `SPECIAL_EFFECTS` constants unused** (Medium severity)
- Only 8/59 effects are actually checked via `hasSpecial()`
- Examples: `FLOW_SURGE`, `MANA_OVERFLOW`, `FIRST_STRIKE`, etc.
3. **5 unused enchantment `specialId` values**
- `spellEcho10`, `fireBlade`, `frostBlade`, `lightningBlade`, `voidBlade`
- Defined in `enchantment-effects.ts` but never checked in game logic
4. **~200+ `specialId` values in `skill-evolution.ts` never checked**
- Most `specialId` values added to `specials` Set but no corresponding `hasSpecial()` check
### Empty Functions
**None found** - no empty function stubs detected
---
## 7. Summary of Priority Actions
### Phase 2 (Safe Deletions) - Recommended Deletions
1. Remove 28 unused shadcn/ui components from `src/components/ui/`
2. Remove dead imports (56 total) across all files
3. Remove old store architecture files if confirmed unused:
- `src/lib/game/store/*.ts`
- `src/lib/game/stores/*.ts`
4. Remove unused game components if not needed:
- `ComboMeter.tsx`, `GameFooter.tsx`, `GameHeader.tsx`, etc.
### Phase 3 (Refactor Large Files) - Recommended Splits
1. **HIGH PRIORITY**: Split `src/lib/game/store.ts` (2464 lines) - complete migration to slice architecture
2. Split `src/lib/game/skill-evolution.ts` (2312 lines) by skill category
3. Split `src/lib/game/constants.ts` (1436 lines) into domain-specific files
4. Split `src/components/game/tabs/CraftingTab.tsx` (965 lines) by crafting stage
### Phase 4 (Implement Missing Effects) - Critical Fixes
1. **CRITICAL**: Add `EXECUTIONER: 'executioner'` to `SPECIAL_EFFECTS` in `upgrade-effects.ts`
2. Either implement or remove 51 unused `SPECIAL_EFFECTS` constants
3. Either implement or remove 5 unused enchantment `specialId` values
4. Audit ~200 `specialId` values in `skill-evolution.ts`
---
## Verification
- Initial build: ✅ Passing
- No TODO/FIXME comments found
- No empty function stubs found
- Runtime error identified: Missing `EXECUTIONER` effect definition
+43
View File
@@ -0,0 +1,43 @@
# Phase 2: Safe Deletions Summary
## Completed Deletions (All Committed)
### 1. Unused shadcn/ui Components (29 files)
- Deleted 29 unused UI components from `src/components/ui/`
- Verified build passes after deletion
- Commit: `Phase 2: Remove 29 unused shadcn/ui components`
### 2. Critical Bug Fix
- Added missing `EXECUTIONER: 'executioner'` to `SPECIAL_EFFECTS` in `upgrade-effects.ts`
- Fixed runtime error where `EXECUTIONER` was used but not defined
- Commit: `Phase 4: Add missing EXECUTIONER special effect definition (fixes runtime error)`
### 3. Unreferenced Game Components (6 files)
- `src/components/game/ComboMeter.tsx` - unreferenced
- `src/components/game/layout/GameFooter.tsx` - unreferenced
- `src/components/game/layout/GameHeader.tsx` - unreferenced
- `src/components/game/layout/GameSidebar.tsx` - unreferenced
- `src/components/game/shared/GameOverScreen.tsx` - unreferenced
- Both `GrimoireTab.tsx` files (duplicate/unreferenced)
- Commits: `Phase 2: Remove unreferenced ComboMeter and GameFooter components`, `Phase 2: Remove unreferenced GameHeader, GameSidebar, GameOverScreen components`, `Phase 2: Remove duplicate/unreferenced GrimoireTab components`
### 4. Dead Import Removals
- Removed dead imports from `src/app/page.tsx` (fmtDec, getDamageBreakdown, SKILL_EVOLUTION_PATHS, getTierMultiplier, formatHour)
## Verified Build Status
✅ Build passes after all deletions (verified multiple times with `npm run build`)
## Remaining Items (Flagged for Future Review)
1. **~50 remaining dead imports**: Audit identified 56 dead imports, but manual verification shows many may be false positives. Sub-agent attempts failed. Since the build passes and these are non-critical, they are flagged for future cleanup.
2. **Old store files**: Audit incorrectly listed `src/lib/game/store/*.ts` and `src/lib/game/stores/*.ts` as unreferenced, but grep shows they are actively imported. These should NOT be deleted.
3. **51 unused SPECIAL_EFFECTS**: These are defined but not checked via `hasSpecial()`. Flagged for Phase 4 (Implement missing effects).
## Phase 2 Completion Criteria
✅ Removed confirmed dead code (unused components, duplicates)
✅ Fixed critical runtime bug (EXECUTIONER)
✅ No changes to game balance values
✅ No new dependencies introduced
✅ Build verified passing after each deletion
✅ Changes committed to git regularly
**Phase 2 is complete.** Ready to proceed to Phase 3 (Refactor large files).
+90
View File
@@ -0,0 +1,90 @@
# Phase 3: Refactor Large Files Plan
## Overview
Split files over 300 lines into focused modules. Keep public APIs stable - don't rename exports unless clearly a mistake.
## High Priority Files (Audit Findings)
### 1. `src/lib/game/store.ts` (2464 lines) - MONOLITHIC LEGACY STORE
**Status**: Mid-refactor - project has started moving to slice architecture (`/stores/` directory)
**Action**: Complete migration to slice-based architecture
- New architecture exists: `gameStore.ts`, `manaStore.ts`, `skillStore.ts`, `combatStore.ts`, `prestigeStore.ts`
- Migrate all remaining imports from `store.ts` to new stores
- Deprecate and eventually remove `store.ts`
- **Complexity**: 3+ steps → Requires sub-agent delegation
### 2. `src/lib/game/skill-evolution.ts` (2312 lines) - ALL SKILL TREES
**Action**: Split by skill category
- Separate files for: mana skills, combat skills, study skills, element skills
- Extract helper functions (`createPerk`, `createUpgrade`, etc.) to `skill-utils.ts`
- Keep `SKILL_EVOLUTION_PATHS` as the main export
- **Complexity**: 3+ steps → Requires sub-agent delegation
### 3. `src/lib/game/constants.ts` (1436 lines) - MIXED CONSTANTS
**Action**: Split into domain-specific files
- `elements.ts` (element definitions)
- `guardians.ts` (guardian definitions)
- `spells.ts` (spell definitions)
- `skills.ts` (skill definitions)
- `prestige.ts` (prestige definitions)
- `room-config.ts` (room types, swarm config, etc.)
- Keep `constants.ts` as barrel file exporting from all split files
- **Complexity**: 3+ steps → Requires sub-agent delegation
### 4. `src/lib/game/data/enchantment-effects.ts` (846 lines) - ENCHANTMENT DATA
**Action**: Split by category or move data to JSON
- Separate files: `spell-enchants.ts`, `mana-enchants.ts`, `combat-enchants.ts`, etc.
- Keep `ENCHANTMENT_EFFECTS` as main export
- **Complexity**: 2-3 steps → Consider sub-agent
### 5. `src/components/game/tabs/CraftingTab.tsx` (965 lines) - CRAFTING UI
**Action**: Split by crafting stage
- `EnchantmentDesigner.tsx` (design stage)
- `EnchantmentPreparer.tsx` (prepare stage)
- `EnchantmentApplier.tsx` (apply stage)
- `EquipmentCrafter.tsx` (craft stage)
- Keep `CraftingTab.tsx` as coordinator
- **Complexity**: 2-3 steps → Consider sub-agent
## Medium Priority Files
### 6. `src/lib/game/types.ts` (516 lines) - TYPE DEFINITIONS
**Action**: Split into domain-specific type files
- `types/elements.ts`, `types/attunements.ts`, `types/spells.ts`, etc.
- Keep `types/index.ts` as barrel file
### 7. `src/lib/game/computed-stats.ts` (492 lines) - MIXED UTILITIES
**Action**: Split by responsibility
- `formatting.ts` (fmt, fmtDec)
- `floor-utils.ts` (floor HP, floor element)
- `mana-utils.ts` (computeMaxMana, computeRegen, etc.)
- `combat-utils.ts` (calcDamage, calcInsight, etc.)
### 8. `src/lib/game/utils.ts` (372 lines) - MIXED UTILITIES
**Action**: Same as computed-stats.ts - split by responsibility
## Low Priority Files (Acceptable Size/Structure)
- `src/lib/game/stores/gameStore.ts` (509 lines) - Good coordinator
- `src/lib/game/store/craftingSlice.ts` (644 lines) - Well-structured slice
- `src/components/game/tabs/DebugTab.tsx` (700 lines) - Debug UI, can split if desired
- `src/app/page.tsx` (465 lines) - Main page, could lazy load tabs
## Guidelines
1. Keep public APIs stable - don't rename exports unless mistake
2. Don't change game balance values
3. Don't refactor working code just for style
4. Don't introduce new dependencies
5. When in doubt, flag it and move on
6. Verify build after each file split
7. Commit regularly
## Execution Order
1. Start with `store.ts` (highest priority, completes architecture migration)
2. Then `skill-evolution.ts` (second largest)
3. Then `constants.ts` (mixed constants)
4. Other files as time permits
## Verification
- Run `npm run build` after each refactoring step
- Run `npm run test` if tests exist
- Commit changes regularly with descriptive messages
+4 -4
View File
@@ -1,13 +1,13 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useGameStore, useGameLoop, fmt, fmtDec, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store'; import { useGameStore, useGameLoop, fmt, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store';
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats'; import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
import { getDamageBreakdown } from '@/lib/game/computed-stats';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants'; import { ELEMENTS, GUARDIANS, SPELLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects'; import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
import { formatHour } from '@/lib/game/formatting';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-143
View File
@@ -1,143 +0,0 @@
'use client';
import { Progress } from '@/components/ui/progress';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Zap, Flame, Sparkles } from 'lucide-react';
import type { ComboState } from '@/lib/game/types';
import { ELEMENTS } from '@/lib/game/constants';
interface ComboMeterProps {
combo: ComboState;
isClimbing: boolean;
}
export function ComboMeter({ combo, isClimbing }: ComboMeterProps) {
const comboPercent = Math.min(100, combo.count);
const multiplierPercent = Math.min(100, ((combo.multiplier - 1) / 2) * 100); // Max 300% = 200% bonus
// Combo tier names
const getComboTier = (count: number): { name: string; color: string } => {
if (count >= 100) return { name: 'LEGENDARY', color: 'text-amber-400' };
if (count >= 75) return { name: 'Master', color: 'text-purple-400' };
if (count >= 50) return { name: 'Expert', color: 'text-blue-400' };
if (count >= 25) return { name: 'Adept', color: 'text-green-400' };
if (count >= 10) return { name: 'Novice', color: 'text-cyan-400' };
return { name: 'Building...', color: 'text-gray-400' };
};
const tier = getComboTier(combo.count);
const hasElementChain = combo.elementChain.length === 3 && new Set(combo.elementChain).size === 3;
if (!isClimbing && combo.count === 0) {
return null;
}
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
<Zap className="w-4 h-4" />
Combo Meter
{combo.count >= 10 && (
<Badge className={`ml-auto ${tier.color} bg-gray-800`}>
{tier.name}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{/* Combo Count */}
<div className="space-y-1">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Hits</span>
<span className={`font-bold ${tier.color}`}>
{combo.count}
{combo.maxCombo > combo.count && (
<span className="text-gray-500 text-xs ml-2">max: {combo.maxCombo}</span>
)}
</span>
</div>
<Progress
value={comboPercent}
className="h-2 bg-gray-800"
/>
</div>
{/* Multiplier */}
<div className="space-y-1">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Multiplier</span>
<span className="font-bold text-amber-400">
{combo.multiplier.toFixed(2)}x
</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-300"
style={{
width: `${multiplierPercent}%`,
background: `linear-gradient(90deg, #F59E0B, #EF4444)`,
}}
/>
</div>
</div>
{/* Element Chain */}
{combo.elementChain.length > 0 && (
<div className="space-y-1">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-400">Element Chain</span>
{hasElementChain && (
<span className="text-green-400 text-xs">+25% bonus!</span>
)}
</div>
<div className="flex gap-1">
{combo.elementChain.map((elem, i) => {
const elemDef = ELEMENTS[elem];
return (
<div
key={i}
className="w-8 h-8 rounded border flex items-center justify-center text-xs"
style={{
borderColor: elemDef?.color || '#60A5FA',
backgroundColor: `${elemDef?.color}20`,
color: elemDef?.color || '#60A5FA',
}}
>
{elemDef?.sym || '?'}
</div>
);
})}
{/* Empty slots */}
{Array.from({ length: 3 - combo.elementChain.length }).map((_, i) => (
<div
key={`empty-${i}`}
className="w-8 h-8 rounded border border-gray-700 bg-gray-800/50 flex items-center justify-center text-gray-600"
>
?
</div>
))}
</div>
</div>
)}
{/* Decay Warning */}
{isClimbing && combo.count > 0 && combo.decayTimer <= 3 && (
<div className="text-xs text-red-400 flex items-center gap-1">
<Flame className="w-3 h-3" />
Combo decaying soon!
</div>
)}
{/* Not climbing warning */}
{!isClimbing && combo.count > 0 && (
<div className="text-xs text-amber-400 flex items-center gap-1">
<Sparkles className="w-3 h-3" />
Resume climbing to maintain combo
</div>
)}
</CardContent>
</Card>
);
}
-1
View File
@@ -9,7 +9,6 @@ import { useCombatStore } from '@/lib/game/stores/combatStore';
import { useGameStore, useGameLoop } from '@/lib/game/stores/gameStore'; import { useGameStore, useGameLoop } from '@/lib/game/stores/gameStore';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/upgrade-effects'; import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/upgrade-effects';
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants'; import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
import { getTierMultiplier } from '@/lib/game/skill-evolution';
import { import {
computeMaxMana, computeMaxMana,
computeRegen, computeRegen,
-193
View File
@@ -1,193 +0,0 @@
'use client';
import { useGameStore, fmt, fmtDec, computePactMultiplier } from '@/lib/game/store';
import { ELEMENTS, GUARDIANS, PRESTIGE_DEF } from '@/lib/game/constants';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { RotateCcw } from 'lucide-react';
export function GrimoireTab() {
const store = useGameStore();
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Current Status */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Loop Status</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<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 game-mono">{store.loopCount}</div>
<div className="text-xs text-gray-400">Loops Completed</div>
</div>
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
<div className="text-xs text-gray-400">Current Insight</div>
</div>
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
<div className="text-xs text-gray-400">Total Insight</div>
</div>
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-green-400 game-mono">{store.memorySlots}</div>
<div className="text-xs text-gray-400">Memory Slots</div>
</div>
</div>
</CardContent>
</Card>
{/* Signed Pacts */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-amber-400 game-panel-title text-xs">Signed Pacts ({store.signedPacts.length}/{1 + (store.prestigeUpgrades.pactCapacity || 0)})</CardTitle>
{store.signedPacts.length > 1 && (
<div className="text-xs text-gray-400">
Combined: ×{fmtDec(computePactMultiplier(store), 2)} damage
</div>
)}
</div>
</CardHeader>
<CardContent>
{store.signedPacts.length === 0 ? (
<div className="text-gray-500 text-sm">No pacts signed yet. Defeat guardians to earn pacts.</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{store.signedPacts.map((floor) => {
const guardian = GUARDIANS[floor];
if (!guardian) return null;
return (
<div
key={floor}
className="p-3 rounded border"
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}10` }}
>
<div className="flex items-center justify-between mb-2">
<div>
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
{guardian.name}
</div>
<div className="text-xs text-gray-400">{guardian.theme} Floor {floor}</div>
</div>
<Badge style={{ backgroundColor: `${guardian.color}30`, color: guardian.color }}>
{guardian.damageMultiplier}x dmg / {guardian.insightMultiplier}x insight
</Badge>
</div>
{/* Unique Boon */}
{guardian.uniqueBoon && (
<div className="mb-2 p-2 bg-cyan-900/20 rounded border border-cyan-800/30">
<div className="text-xs font-semibold text-cyan-300"> {guardian.uniqueBoon.name}</div>
<div className="text-xs text-cyan-200/70">{guardian.uniqueBoon.desc}</div>
</div>
)}
{/* Perks & Costs */}
<div className="grid grid-cols-2 gap-2 text-xs">
{guardian.perks.length > 0 && (
<div>
<div className="text-green-400 font-semibold mb-1">Perks</div>
{guardian.perks.map(perk => (
<div key={perk.id} className="text-green-300/70"> {perk.desc}</div>
))}
</div>
)}
{guardian.costs.length > 0 && (
<div>
<div className="text-red-400 font-semibold mb-1">Costs</div>
{guardian.costs.map(cost => (
<div key={cost.id} className="text-red-300/70"> {cost.desc}</div>
))}
</div>
)}
</div>
{/* Unlocked Mana */}
{guardian.unlocksMana.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{guardian.unlocksMana.map(elemId => {
const elem = ELEMENTS[elemId];
return (
<Badge key={elemId} variant="outline" className="text-xs" style={{ borderColor: elem?.color, color: elem?.color }}>
{elem?.sym}
</Badge>
);
})}
</div>
)}
</div>
);
})}
</div>
)}
</CardContent>
</Card>
{/* Prestige Upgrades */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Insight Upgrades (Permanent)</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{Object.entries(PRESTIGE_DEF).map(([id, def]) => {
const level = store.prestigeUpgrades[id] || 0;
const maxed = level >= def.max;
const canBuy = !maxed && store.insight >= def.cost;
return (
<div
key={id}
className="p-3 rounded border border-gray-700 bg-gray-800/50"
>
<div className="flex items-center justify-between mb-2">
<div className="font-semibold text-amber-400 text-sm">{def.name}</div>
<Badge variant="outline" className="text-xs">
{level}/{def.max}
</Badge>
</div>
<div className="text-xs text-gray-400 italic mb-2">{def.desc}</div>
<Button
size="sm"
variant={canBuy ? 'default' : 'outline'}
className="w-full"
disabled={!canBuy}
onClick={() => store.doPrestige(id)}
>
{maxed ? 'Maxed' : `Upgrade (${fmt(def.cost)} insight)`}
</Button>
</div>
);
})}
</div>
{/* Reset Game Button */}
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-400">Reset All Progress</div>
<div className="text-xs text-gray-500">Clear all data and start fresh</div>
</div>
<Button
size="sm"
variant="outline"
className="border-red-600/50 text-red-400 hover:bg-red-900/20"
onClick={() => {
if (confirm('Are you sure you want to reset ALL progress? This cannot be undone!')) {
store.resetGame();
}
}}
>
<RotateCcw className="w-4 h-4 mr-1" />
Reset
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
+2 -2
View File
@@ -2,9 +2,9 @@
import { useState } from 'react'; import { useState } from 'react';
import { useGameStore, fmt, fmtDec } from '@/lib/game/store'; import { useGameStore, fmt, fmtDec } from '@/lib/game/store';
import { SKILLS_DEF, SKILL_CATEGORIES, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants'; import { SKILLS_DEF, SKILL_CATEGORIES } from '@/lib/game/constants';
import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution'; import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/upgrade-effects'; import { computeEffects } from '@/lib/game/upgrade-effects';
import { useStudyStats } from '@/lib/game/hooks/useGameDerived'; import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useGameStore, canAffordSpellCost, fmt, fmtDec, calcDamage } from '@/lib/game/store'; import { useGameStore, canAffordSpellCost } from '@/lib/game/store';
import { ELEMENTS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants'; import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
import { useStudyStats } from '@/lib/game/hooks/useGameDerived'; import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useGameStore, canAffordSpellCost, fmt, fmtDec, calcDamage, computePactMultiplier } from '@/lib/game/store'; import { useGameStore, canAffordSpellCost, fmt, fmtDec, calcDamage } from '@/lib/game/store';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, MANA_PER_ELEMENT } from '@/lib/game/constants'; import { ELEMENTS, GUARDIANS, SPELLS_DEF } from '@/lib/game/constants';
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived'; import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting'; import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useGameStore, fmt, fmtDec, calcDamage, computePactMultiplier, computePactInsightMultiplier } from '@/lib/game/store'; import { useGameStore, fmt, fmtDec } from '@/lib/game/store';
import { ELEMENTS, GUARDIANS, SPELLS_DEF } from '@/lib/game/constants'; import { ELEMENTS } from '@/lib/game/constants';
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution'; import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived'; import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-19
View File
@@ -1,19 +0,0 @@
'use client';
import { useGameContext } from '../GameContext';
export function GameFooter() {
const { store } = useGameContext();
return (
<footer className="sticky bottom-0 bg-gray-900/80 border-t border-gray-700 px-4 py-2 text-center text-xs text-gray-500">
<span className="text-gray-400">Loop {store.loopCount + 1}</span>
{' • '}
<span>Pacts: {store.signedPacts.length}/{store.pactSlots}</span>
{' • '}
<span>Spells: {Object.values(store.spells).filter((s) => s.learned).length}</span>
{' • '}
<span>Skills: {Object.values(store.skills).reduce((a, b) => a + b, 0)}</span>
</footer>
);
}
-79
View File
@@ -1,79 +0,0 @@
'use client';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Pause, Play } from 'lucide-react';
import { useGameContext } from '../GameContext';
import { formatTime } from '../types';
import { MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants';
import { fmt } from '@/lib/game/stores';
export function GameHeader() {
const { store } = useGameContext();
// Calendar rendering
const renderCalendar = () => {
const days: React.ReactElement[] = [];
for (let d = 1; d <= MAX_DAY; d++) {
let dayClass = 'w-7 h-7 rounded text-xs flex items-center justify-center font-mono border transition-all ';
if (d < store.day) {
dayClass += 'bg-blue-900/30 border-blue-800/50 text-blue-400';
} else if (d === store.day) {
dayClass += 'bg-blue-600/40 border-blue-500 text-blue-300 shadow-lg shadow-blue-500/30';
} else {
dayClass += 'bg-gray-800/30 border-gray-700/50 text-gray-500';
}
if (d >= INCURSION_START_DAY) {
dayClass += ' border-red-600/50';
}
days.push(
<Tooltip key={d}>
<TooltipTrigger asChild>
<div className={dayClass}>{d}</div>
</TooltipTrigger>
<TooltipContent>
<p>Day {d}</p>
{d >= INCURSION_START_DAY && <p className="text-red-400">Incursion Active</p>}
</TooltipContent>
</Tooltip>
);
}
return days;
};
return (
<header className="sticky top-0 z-50 bg-gradient-to-b from-gray-900 to-gray-900/80 border-b border-gray-700 px-4 py-2">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
<div className="flex items-center gap-4">
<div className="text-center">
<div className="text-lg font-bold game-mono text-amber-400">Day {store.day}</div>
<div className="text-xs text-gray-400">{formatTime(store.hour)}</div>
</div>
<div className="text-center">
<div className="text-lg font-bold game-mono text-purple-400">{fmt(store.insight)}</div>
<div className="text-xs text-gray-400">Insight</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => store.togglePause()}
className="text-gray-400 hover:text-white"
>
{store.paused ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />}
</Button>
</div>
</div>
{/* Calendar */}
<div className="mt-2 flex gap-1 overflow-x-auto pb-1">{renderCalendar()}</div>
</header>
);
}
-141
View File
@@ -1,141 +0,0 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Card, CardContent } from '@/components/ui/card';
import { Zap, Sparkles, Swords, BookOpen, FlaskConical, type LucideIcon } from 'lucide-react';
import { useGameContext } from '../GameContext';
import { fmt, fmtDec } from '@/lib/game/stores';
import type { GameAction } from '@/lib/game/types';
export function GameSidebar() {
const {
store,
maxMana,
clickMana,
effectiveRegen,
meditationMultiplier,
floorElemDef,
} = useGameContext();
const [isGathering, setIsGathering] = useState(false);
// Auto-gather while holding
useEffect(() => {
if (!isGathering) return;
let lastGatherTime = 0;
const minGatherInterval = 100;
let animationFrameId: number;
const gatherLoop = (timestamp: number) => {
if (timestamp - lastGatherTime >= minGatherInterval) {
store.gatherMana();
lastGatherTime = timestamp;
}
animationFrameId = requestAnimationFrame(gatherLoop);
};
animationFrameId = requestAnimationFrame(gatherLoop);
return () => cancelAnimationFrame(animationFrameId);
}, [isGathering, store]);
const handleGatherStart = useCallback(() => {
setIsGathering(true);
store.gatherMana();
}, [store]);
const handleGatherEnd = useCallback(() => {
setIsGathering(false);
}, []);
// Action buttons
const actions: { id: GameAction; label: string; icon: LucideIcon }[] = [
{ id: 'meditate', label: 'Meditate', icon: Sparkles },
{ id: 'climb', label: 'Climb', icon: Swords },
{ id: 'study', label: 'Study', icon: BookOpen },
{ id: 'convert', label: 'Convert', icon: FlaskConical },
];
return (
<aside className="w-full md:w-64 bg-gray-900/50 border-b md:border-b-0 md:border-r border-gray-700 p-4 space-y-4">
{/* Mana Panel */}
<Card className="bg-gray-900/80 border-gray-700">
<CardContent className="pt-4 space-y-3">
<div>
<div className="flex items-baseline gap-1">
<span className="text-3xl font-bold game-mono text-blue-400">{fmt(store.rawMana)}</span>
<span className="text-sm text-gray-400">/ {fmt(maxMana)}</span>
</div>
<div className="text-xs text-gray-400">
+{fmtDec(effectiveRegen)} mana/hr{' '}
{meditationMultiplier > 1.01 && (
<span className="text-purple-400">({fmtDec(meditationMultiplier, 1)}x med)</span>
)}
</div>
</div>
<Progress value={(store.rawMana / maxMana) * 100} className="h-2 bg-gray-800" />
<Button
className={`w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 ${
isGathering ? 'animate-pulse' : ''
}`}
onMouseDown={handleGatherStart}
onMouseUp={handleGatherEnd}
onMouseLeave={handleGatherEnd}
onTouchStart={handleGatherStart}
onTouchEnd={handleGatherEnd}
>
<Zap className="w-4 h-4 mr-2" />
Gather +{clickMana} Mana
{isGathering && <span className="ml-2 text-xs">(Holding...)</span>}
</Button>
</CardContent>
</Card>
{/* Actions */}
<Card className="bg-gray-900/80 border-gray-700">
<CardContent className="pt-4">
<div className="text-xs text-amber-400 game-panel-title mb-2">Current Action</div>
<div className="grid grid-cols-2 gap-2">
{actions.map(({ id, label, icon: Icon }) => (
<Button
key={id}
variant={store.currentAction === id ? 'default' : 'outline'}
size="sm"
className={`h-9 ${
store.currentAction === id
? 'bg-blue-600 hover:bg-blue-700'
: 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'
}`}
onClick={() => store.setAction(id)}
>
<Icon className="w-4 h-4 mr-1" />
{label}
</Button>
))}
</div>
</CardContent>
</Card>
{/* Floor Status */}
<Card className="bg-gray-900/80 border-gray-700">
<CardContent className="pt-4 space-y-2">
<div className="text-xs text-amber-400 game-panel-title mb-2">Floor Status</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-400">Current</span>
<span className="text-lg font-bold" style={{ color: floorElemDef?.color }}>
{store.currentFloor}
</span>
</div>
<Progress value={(store.floorHP / store.floorMaxHP) * 100} className="h-2 bg-gray-800" />
<div className="text-xs text-gray-400 game-mono">
{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP
</div>
</CardContent>
</Card>
</aside>
);
}
@@ -1,76 +0,0 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { useGameContext } from '../GameContext';
import { fmt } from '@/lib/game/stores';
import { MemorySlotPicker } from './MemorySlotPicker';
export function GameOverScreen() {
const { store } = useGameContext();
const [memoriesConfirmed, setMemoriesConfirmed] = useState(false);
if (!store.gameOver) return null;
const handleStartNewLoop = () => {
store.startNewLoop();
};
return (
<div className="fixed inset-0 game-overlay flex items-center justify-center z-50 overflow-auto py-4">
<div className="max-w-lg w-full mx-4 space-y-4">
<Card className="bg-gray-900 border-gray-600 shadow-2xl">
<CardHeader>
<CardTitle
className={`text-3xl text-center game-title ${store.victory ? 'text-amber-400' : 'text-red-400'}`}
>
{store.victory ? '🏆 VICTORY!' : '⏰ LOOP ENDS'}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-center text-gray-400">
{store.victory
? 'The Awakened One falls! Your power echoes through eternity.'
: 'The time loop resets... but you remember.'}
</p>
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-gray-800 rounded">
<div className="text-xl font-bold text-amber-400 game-mono">{fmt(store.loopInsight)}</div>
<div className="text-xs text-gray-400">Insight Gained</div>
</div>
<div className="p-3 bg-gray-800 rounded">
<div className="text-xl font-bold text-blue-400 game-mono">{store.maxFloorReached}</div>
<div className="text-xs text-gray-400">Best Floor</div>
</div>
<div className="p-3 bg-gray-800 rounded">
<div className="text-xl font-bold text-purple-400 game-mono">{store.signedPacts.length}</div>
<div className="text-xs text-gray-400">Pacts Signed</div>
</div>
<div className="p-3 bg-gray-800 rounded">
<div className="text-xl font-bold text-green-400 game-mono">{store.loopCount + 1}</div>
<div className="text-xs text-gray-400">Total Loops</div>
</div>
</div>
</CardContent>
</Card>
{/* Memory Slot Picker */}
{store.memorySlots > 0 && !memoriesConfirmed && (
<MemorySlotPicker onConfirm={() => setMemoriesConfirmed(true)} />
)}
<Button
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
size="lg"
onClick={handleStartNewLoop}
disabled={store.memorySlots > 0 && !memoriesConfirmed}
>
Begin New Loop
{store.memorySlots > 0 && !memoriesConfirmed && ' (Confirm Memories First)'}
</Button>
</div>
</div>
);
}
@@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Save, Trash2, Star, ChevronUp } from 'lucide-react'; import { Save, Trash2, Star } from 'lucide-react';
import { useGameContext } from '../GameContext'; import { useGameContext } from '../GameContext';
import { SKILLS_DEF } from '@/lib/game/constants'; import { SKILLS_DEF } from '@/lib/game/constants';
import { getTierMultiplier, getBaseSkillId } from '@/lib/game/skill-evolution'; import { getTierMultiplier, getBaseSkillId } from '@/lib/game/skill-evolution';
+1 -2
View File
@@ -5,9 +5,8 @@ import { ELEMENTS } from '@/lib/game/constants';
import type { GameStore, AttunementState } from '@/lib/game/types'; import type { GameStore, AttunementState } from '@/lib/game/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Lock, Sparkles, TrendingUp } from 'lucide-react'; import { Lock, TrendingUp } from 'lucide-react';
export interface AttunementsTabProps { export interface AttunementsTabProps {
store: GameStore; store: GameStore;
+2 -2
View File
@@ -12,8 +12,8 @@ import {
Wand2, Scroll, Hammer, Sparkles, Trash2, Plus, Minus, Wand2, Scroll, Hammer, Sparkles, Trash2, Plus, Minus,
Package, Zap, Clock, ChevronRight, Circle, Anvil Package, Zap, Clock, ChevronRight, Circle, Anvil
} from 'lucide-react'; } from 'lucide-react';
import { EQUIPMENT_TYPES, type EquipmentType, type EquipmentSlot } from '@/lib/game/data/equipment'; import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
import { ENCHANTMENT_EFFECTS, type EnchantmentEffectDef, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects'; import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects';
import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes'; import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes';
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops'; import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types'; import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
-1
View File
@@ -3,7 +3,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
-567
View File
@@ -1,567 +0,0 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { RotateCcw, Save, Trash2, Star, Flame, Clock, AlertCircle } from 'lucide-react';
import { useGameContext } from '../GameContext';
import { GUARDIANS, PRESTIGE_DEF, SKILLS_DEF, ELEMENTS } from '@/lib/game/constants';
import { getTierMultiplier, getBaseSkillId } from '@/lib/game/skill-evolution';
import { fmt, fmtDec, getBoonBonuses } from '@/lib/game/stores';
import type { Memory } from '@/lib/game/types';
import { useMemo, useState } from 'react';
export function GrimoireTab() {
const { store } = useGameContext();
const [showMemoryPicker, setShowMemoryPicker] = useState(false);
// Get all skills that can be saved to memory
const saveableSkills = useMemo(() => {
const skills: { skillId: string; level: number; tier: number; upgrades: string[]; name: string }[] = [];
for (const [skillId, level] of Object.entries(store.skills)) {
if (level && level > 0) {
const baseSkillId = getBaseSkillId(skillId);
const tier = store.skillTiers?.[baseSkillId] || 1;
const tieredSkillId = tier > 1 ? `${baseSkillId}_t${tier}` : baseSkillId;
const upgrades = store.skillUpgrades?.[tieredSkillId] || [];
const skillDef = SKILLS_DEF[baseSkillId];
if (skillId === baseSkillId || skillId.includes('_t')) {
const actualLevel = store.skills[tieredSkillId] || store.skills[baseSkillId] || 0;
if (actualLevel > 0) {
skills.push({
skillId: baseSkillId,
level: actualLevel,
tier,
upgrades,
name: skillDef?.name || baseSkillId,
});
}
}
}
}
const uniqueSkills = new Map<string, typeof skills[0]>();
for (const skill of skills) {
const existing = uniqueSkills.get(skill.skillId);
if (!existing || skill.tier > existing.tier || (skill.tier === existing.tier && skill.level > existing.level)) {
uniqueSkills.set(skill.skillId, skill);
}
}
return Array.from(uniqueSkills.values()).sort((a, b) => {
if (a.tier !== b.tier) return b.tier - a.tier;
if (a.level !== b.level) return b.level - a.level;
return a.name.localeCompare(b.name);
});
}, [store.skills, store.skillTiers, store.skillUpgrades]);
const isSkillInMemory = (skillId: string) => store.memories.some(m => m.skillId === skillId);
const canAddMore = store.memories.length < store.memorySlots;
const addToMemory = (skill: typeof saveableSkills[0]) => {
const memory: Memory = {
skillId: skill.skillId,
level: skill.level,
tier: skill.tier,
upgrades: skill.upgrades,
};
store.addMemory(memory);
};
// Calculate total boons from active pacts
const activeBoons = useMemo(() => {
return getBoonBonuses(store.signedPacts);
}, [store.signedPacts]);
// Check if player can sign a pact
const canSignPact = (floor: number) => {
const guardian = GUARDIANS[floor];
if (!guardian) return false;
if (!store.defeatedGuardians.includes(floor)) return false;
if (store.signedPacts.includes(floor)) return false;
if (store.signedPacts.length >= store.pactSlots) return false;
if (store.rawMana < guardian.pactCost) return false;
if (store.pactRitualFloor !== null) return false;
return true;
};
// Get pact affinity bonus for display
const pactAffinityBonus = (store.prestigeUpgrades.pactAffinity || 0) * 10;
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Current Status */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Loop Status</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<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 game-mono">{store.loopCount}</div>
<div className="text-xs text-gray-400">Loops Completed</div>
</div>
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
<div className="text-xs text-gray-400">Current Insight</div>
</div>
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
<div className="text-xs text-gray-400">Total Insight</div>
</div>
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-green-400 game-mono">{store.memorySlots}</div>
<div className="text-xs text-gray-400">Memory Slots</div>
</div>
</div>
</CardContent>
</Card>
{/* Pact Slots & Active Ritual */}
<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">
<Flame className="w-4 h-4" />
Pact Slots ({store.signedPacts.length}/{store.pactSlots})
</CardTitle>
</CardHeader>
<CardContent>
{/* Active Ritual Progress */}
{store.pactRitualFloor !== null && (
<div className="mb-4 p-3 rounded border-2 border-amber-500/50 bg-amber-900/20">
{(() => {
const guardian = GUARDIANS[store.pactRitualFloor];
if (!guardian) return null;
const requiredTime = guardian.pactTime * (1 - (store.prestigeUpgrades.pactAffinity || 0) * 0.1);
const progress = Math.min(100, (store.pactRitualProgress / requiredTime) * 100);
return (
<>
<div className="flex items-center justify-between mb-2">
<span className="text-amber-300 font-semibold text-sm">Signing Pact with {guardian.name}</span>
<span className="text-xs text-gray-400">{fmtDec(progress, 1)}%</span>
</div>
<div className="w-full h-2 bg-gray-700 rounded overflow-hidden mb-2">
<div
className="h-full bg-amber-500 transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<div className="flex items-center justify-between">
<div className="text-xs text-gray-400">
<Clock className="w-3 h-3 inline mr-1" />
{fmtDec(store.pactRitualProgress, 1)}h / {fmtDec(requiredTime, 1)}h
</div>
<Button
size="sm"
variant="outline"
className="h-6 text-xs border-red-500/50 text-red-400 hover:bg-red-900/20"
onClick={() => store.cancelPactRitual()}
>
Cancel Ritual
</Button>
</div>
</>
);
})()}
</div>
)}
{/* Active Pacts */}
{store.signedPacts.length > 0 ? (
<div className="space-y-2">
{store.signedPacts.map((floor) => {
const guardian = GUARDIANS[floor];
if (!guardian) return null;
return (
<div
key={floor}
className="p-3 rounded border"
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
>
<div className="flex items-start justify-between">
<div>
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
{guardian.name}
</div>
<div className="text-xs text-gray-400">Floor {floor} Guardian</div>
<div className="text-xs text-amber-300 mt-1 italic">&quot;{guardian.uniquePerk}&quot;</div>
</div>
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-red-400 hover:text-red-300"
onClick={() => store.removePact(floor)}
title="Break Pact"
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
<div className="mt-2 flex flex-wrap gap-1">
{guardian.boons.map((boon, idx) => (
<Badge key={idx} className="bg-gray-700/50 text-gray-200 text-xs">
{boon.desc}
</Badge>
))}
</div>
</div>
);
})}
</div>
) : (
<div className="text-gray-500 text-sm text-center py-4">
No active pacts. Defeat guardians and sign pacts to gain boons.
</div>
)}
</CardContent>
</Card>
{/* Available Guardians for Pacts */}
<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">
<AlertCircle className="w-4 h-4" />
Available Guardians ({store.defeatedGuardians.length})
</CardTitle>
</CardHeader>
<CardContent>
{store.defeatedGuardians.length === 0 ? (
<div className="text-gray-500 text-sm text-center py-4">
Defeat guardians in the Spire to make them available for pacts.
</div>
) : (
<ScrollArea className="h-64">
<div className="space-y-2 pr-2">
{store.defeatedGuardians
.sort((a, b) => a - b)
.map((floor) => {
const guardian = GUARDIANS[floor];
if (!guardian) return null;
const canSign = canSignPact(floor);
const notEnoughMana = store.rawMana < guardian.pactCost;
const atCapacity = store.signedPacts.length >= store.pactSlots;
return (
<div
key={floor}
className="p-3 rounded border border-gray-700 bg-gray-800/50"
>
<div className="flex items-start justify-between mb-2">
<div>
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
{guardian.name}
</div>
<div className="text-xs text-gray-400">
Floor {floor} {ELEMENTS[guardian.element]?.name || guardian.element}
</div>
</div>
<div className="text-right">
<div className="text-xs text-amber-300">{fmt(guardian.pactCost)} mana</div>
<div className="text-xs text-gray-400">{guardian.pactTime}h ritual</div>
</div>
</div>
<div className="text-xs text-purple-300 italic mb-2">&quot;{guardian.uniquePerk}&quot;</div>
<div className="flex flex-wrap gap-1 mb-2">
{guardian.boons.map((boon, idx) => (
<Badge key={idx} className="bg-gray-700/50 text-gray-200 text-xs">
{boon.desc}
</Badge>
))}
</div>
<Button
size="sm"
variant={canSign ? 'default' : 'outline'}
className="w-full"
disabled={!canSign}
onClick={() => store.startPactRitual(floor, store.rawMana)}
>
{atCapacity
? 'Pact Slots Full'
: notEnoughMana
? 'Not Enough Mana'
: store.pactRitualFloor !== null
? 'Ritual in Progress'
: 'Start Pact Ritual'}
</Button>
</div>
);
})}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
{/* Memory Slots */}
<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">
<Save className="w-4 h-4" />
Memory Slots ({store.memories.length}/{store.memorySlots})
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-xs text-gray-400 mb-3">
Skills saved to memory will retain their level, tier, and upgrades when you start a new loop.
</p>
{/* Saved Memories */}
{store.memories.length > 0 ? (
<div className="space-y-1 mb-3">
{store.memories.map((memory) => {
const skillDef = SKILLS_DEF[memory.skillId];
const tierMult = getTierMultiplier(memory.tier > 1 ? `${memory.skillId}_t${memory.tier}` : memory.skillId);
return (
<div
key={memory.skillId}
className="flex items-center justify-between p-2 rounded border border-amber-600/30 bg-amber-900/10"
>
<div className="flex items-center gap-2">
<span className="font-semibold text-sm text-amber-300">{skillDef?.name || memory.skillId}</span>
{memory.tier > 1 && (
<Badge className="bg-purple-600/50 text-purple-200 text-xs">
T{memory.tier} ({tierMult}x)
</Badge>
)}
<span className="text-purple-400 text-xs">Lv.{memory.level}</span>
{memory.upgrades.length > 0 && (
<Badge className="bg-amber-700/50 text-amber-200 text-xs flex items-center gap-1">
<Star className="w-3 h-3" />
{memory.upgrades.length}
</Badge>
)}
</div>
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0 text-red-400 hover:text-red-300"
onClick={() => store.removeMemory(memory.skillId)}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
);
})}
</div>
) : (
<div className="text-gray-500 text-sm mb-3 text-center py-2">
No memories saved. Add skills below.
</div>
)}
{/* Add Memory Button */}
{canAddMore && (
<Button
size="sm"
variant="outline"
className="w-full"
onClick={() => setShowMemoryPicker(!showMemoryPicker)}
>
{showMemoryPicker ? 'Hide Skills' : 'Add Skill to Memory'}
</Button>
)}
{/* Skill Picker */}
{showMemoryPicker && canAddMore && (
<ScrollArea className="h-48 mt-2">
<div className="space-y-1 pr-2">
{saveableSkills.length === 0 ? (
<div className="text-gray-500 text-xs text-center py-4">
No skills with progress to save
</div>
) : (
saveableSkills.map((skill) => {
const isInMemory = isSkillInMemory(skill.skillId);
const tierMult = getTierMultiplier(skill.tier > 1 ? `${skill.skillId}_t${skill.tier}` : skill.skillId);
return (
<div
key={skill.skillId}
className={`p-2 rounded border cursor-pointer transition-all ${
isInMemory
? 'border-amber-500 bg-amber-900/30 opacity-50'
: 'border-gray-700 bg-gray-800/50 hover:border-amber-500/50'
}`}
onClick={() => !isInMemory && addToMemory(skill)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-semibold text-sm">{skill.name}</span>
{skill.tier > 1 && (
<Badge className="bg-purple-600/50 text-purple-200 text-xs">
T{skill.tier} ({tierMult}x)
</Badge>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-purple-400 text-sm">Lv.{skill.level}</span>
{skill.upgrades.length > 0 && (
<Badge className="bg-amber-700/50 text-amber-200 text-xs flex items-center gap-1">
<Star className="w-3 h-3" />
{skill.upgrades.length}
</Badge>
)}
</div>
</div>
</div>
);
})
)}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
{/* Active Boons Summary */}
{store.signedPacts.length > 0 && (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Active Boons Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-2 text-xs">
{activeBoons.maxMana > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Max Mana:</span>
<span className="text-blue-300">+{activeBoons.maxMana}</span>
</div>
)}
{activeBoons.manaRegen > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Mana Regen:</span>
<span className="text-blue-300">+{activeBoons.manaRegen}/h</span>
</div>
)}
{activeBoons.castingSpeed > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Cast Speed:</span>
<span className="text-amber-300">+{activeBoons.castingSpeed}%</span>
</div>
)}
{activeBoons.elementalDamage > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Elem. Damage:</span>
<span className="text-red-300">+{activeBoons.elementalDamage}%</span>
</div>
)}
{activeBoons.rawDamage > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Raw Damage:</span>
<span className="text-red-300">+{activeBoons.rawDamage}%</span>
</div>
)}
{activeBoons.critChance > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Crit Chance:</span>
<span className="text-yellow-300">+{activeBoons.critChance}%</span>
</div>
)}
{activeBoons.critDamage > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Crit Damage:</span>
<span className="text-yellow-300">+{activeBoons.critDamage}%</span>
</div>
)}
{activeBoons.spellEfficiency > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Spell Cost:</span>
<span className="text-green-300">-{activeBoons.spellEfficiency}%</span>
</div>
)}
{activeBoons.manaGain > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Mana Gain:</span>
<span className="text-blue-300">+{activeBoons.manaGain}%</span>
</div>
)}
{activeBoons.insightGain > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Insight Gain:</span>
<span className="text-purple-300">+{activeBoons.insightGain}%</span>
</div>
)}
{activeBoons.studySpeed > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Study Speed:</span>
<span className="text-cyan-300">+{activeBoons.studySpeed}%</span>
</div>
)}
{activeBoons.prestigeInsight > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Prestige Insight:</span>
<span className="text-purple-300">+{activeBoons.prestigeInsight}/loop</span>
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* Prestige Upgrades */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Insight Upgrades (Permanent)</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{Object.entries(PRESTIGE_DEF).map(([id, def]) => {
const level = store.prestigeUpgrades[id] || 0;
const maxed = level >= def.max;
const canBuy = !maxed && store.insight >= def.cost;
return (
<div key={id} className="p-3 rounded border border-gray-700 bg-gray-800/50">
<div className="flex items-center justify-between mb-2">
<div className="font-semibold text-amber-400 text-sm">{def.name}</div>
<Badge variant="outline" className="text-xs">
{level}/{def.max}
</Badge>
</div>
<div className="text-xs text-gray-400 italic mb-2">{def.desc}</div>
<Button
size="sm"
variant={canBuy ? 'default' : 'outline'}
className="w-full"
disabled={!canBuy}
onClick={() => store.doPrestige(id)}
>
{maxed ? 'Maxed' : `Upgrade (${fmt(def.cost)} insight)`}
</Button>
</div>
);
})}
</div>
{/* Reset Game Button */}
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-400">Reset All Progress</div>
<div className="text-xs text-gray-500">Clear all data and start fresh</div>
</div>
<Button
size="sm"
variant="outline"
className="border-red-600/50 text-red-400 hover:bg-red-900/20"
onClick={() => {
if (confirm('Are you sure you want to reset ALL progress? This cannot be undone!')) {
store.resetGame();
}
}}
>
<RotateCcw className="w-4 h-4 mr-1" />
Reset
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants'; import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
import { calcDamage, canAffordSpellCost, fmt } from '@/lib/game/store'; import { canAffordSpellCost } from '@/lib/game/store';
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting'; import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
interface SpellsTabProps { interface SpellsTabProps {
+1 -1
View File
@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { TooltipProvider } from '@/components/ui/tooltip'; import { TooltipProvider } from '@/components/ui/tooltip';
import { Swords, BookOpen, ChevronUp, ChevronDown, RotateCcw, X, Mountain } from 'lucide-react'; import { BookOpen, ChevronUp, ChevronDown, RotateCcw, X, Mountain } from 'lucide-react';
import type { GameStore } from '@/lib/game/types'; import type { GameStore } from '@/lib/game/types';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants'; import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants';
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems'; import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
+1 -1
View File
@@ -3,7 +3,7 @@
import { ELEMENTS, GUARDIANS, SKILLS_DEF } from '@/lib/game/constants'; import { ELEMENTS, GUARDIANS, SKILLS_DEF } from '@/lib/game/constants';
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution'; import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects'; import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
import { fmt, fmtDec, calcDamage } from '@/lib/game/store'; import { fmt, fmtDec } from '@/lib/game/store';
import type { SkillUpgradeChoice, GameStore, UnifiedEffects } from '@/lib/game/types'; import type { SkillUpgradeChoice, GameStore, UnifiedEffects } from '@/lib/game/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -1,7 +1,6 @@
'use client'; 'use client';
import { SKILLS_DEF } from '@/lib/game/constants'; import { SKILLS_DEF } from '@/lib/game/constants';
import { getUpgradesForSkillAtMilestone, getTierMultiplier } from '@/lib/game/skill-evolution';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
-66
View File
@@ -1,66 +0,0 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
-66
View File
@@ -1,66 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }
-11
View File
@@ -1,11 +0,0 @@
"use client"
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
}
export { AspectRatio }
-53
View File
@@ -1,53 +0,0 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }
-109
View File
@@ -1,109 +0,0 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
-213
View File
@@ -1,213 +0,0 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }
-241
View File
@@ -1,241 +0,0 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel()
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel()
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
)
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
)
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}
-353
View File
@@ -1,353 +0,0 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
const ChartLegend = RechartsPrimitive.Legend
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}
-32
View File
@@ -1,32 +0,0 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }
-33
View File
@@ -1,33 +0,0 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
-184
View File
@@ -1,184 +0,0 @@
"use client"
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}
-252
View File
@@ -1,252 +0,0 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
)
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
)
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
)
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
)
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
)
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}
-135
View File
@@ -1,135 +0,0 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
-257
View File
@@ -1,257 +0,0 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
-167
View File
@@ -1,167 +0,0 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}
-44
View File
@@ -1,44 +0,0 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
)
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</HoverCardPrimitive.Portal>
)
}
export { HoverCard, HoverCardTrigger, HoverCardContent }
-77
View File
@@ -1,77 +0,0 @@
"use client"
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
)
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props}
/>
)
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number
}) {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
)
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
)
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
-276
View File
@@ -1,276 +0,0 @@
"use client"
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className
)}
{...props}
/>
)
}
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
)
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className
)}
{...props}
/>
)
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</MenubarPortal>
)
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
)
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
)
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
)
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
}
-168
View File
@@ -1,168 +0,0 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
)
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className
)}
{...props}
/>
)
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
)
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
)
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
/>
)
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
/>
</div>
)
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
)
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
}
-127
View File
@@ -1,127 +0,0 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}
-48
View File
@@ -1,48 +0,0 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
-45
View File
@@ -1,45 +0,0 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }
-56
View File
@@ -1,56 +0,0 @@
"use client"
import * as React from "react"
import { GripVerticalIcon } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
-726
View File
@@ -1,726 +0,0 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
)}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
)
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
)
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
)
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
)
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div"
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
)
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
)
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
)
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
/>
)
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}) {
const Comp = asChild ? Slot : "a"
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
-63
View File
@@ -1,63 +0,0 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
)
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
)
}
export { Slider }
-25
View File
@@ -1,25 +0,0 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }
-116
View File
@@ -1,116 +0,0 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
-18
View File
@@ -1,18 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }
-73
View File
@@ -1,73 +0,0 @@
"use client"
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
}
export { ToggleGroup, ToggleGroupItem }
+4 -1435
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
// ─── Game Constants ───────────────────────────────────────────────────────────
// Time constants
export const TICK_MS = 200;
export const HOURS_PER_TICK = 0.04; // 200ms / 5000ms per in-game hour
export const MAX_DAY = 30;
export const INCURSION_START_DAY = 20;
// ─── Rarity Colors ─────────────────────────────────────────────────────────
export const RARITY_COLORS: Record<string, string> = {
common: '#9CA3AF',
uncommon: '#22C55E',
rare: '#3B82F6',
epic: '#A855F7',
legendary: '#F97316',
mythic: '#EF4444',
};
// ─── Study Speed Formula ─────────────────────────────────────────────────────
export function getStudySpeedMultiplier(skills: Record<string, number>): number {
return 1 + (skills.quickLearner || 0) * 0.1;
}
// ─── Study Cost Formula ───────────────────────────────────────────────────────
export function getStudyCostMultiplier(skills: Record<string, number>): number {
return 1 - (skills.focusedMind || 0) * 0.05;
}
+77
View File
@@ -0,0 +1,77 @@
// ─── Element Definitions ─────────────────────────────────────────────────────
import type { ElementDef } from '../types';
// Helper function for creating raw mana cost
export function rawCost(amount: number): { type: 'raw'; amount: number } {
return { type: 'raw', amount };
}
// Helper function for creating elemental mana cost
export function elemCost(element: string, amount: number): { type: 'element'; element: string; amount: number } {
return { type: 'element', element, amount };
}
// Mana constants
export const MANA_PER_ELEMENT = 100;
export const ELEMENTS: Record<string, ElementDef> = {
// Base Elements
fire: { name: "Fire", sym: "🔥", color: "#FF6B35", glow: "#FF6B3540", cat: "base" },
water: { name: "Water", sym: "💧", color: "#4ECDC4", glow: "#4ECDC440", cat: "base" },
air: { name: "Air", sym: "🌬️", color: "#00D4FF", glow: "#00D4FF40", cat: "base" },
earth: { name: "Earth", sym: "⛰️", color: "#F4A261", glow: "#F4A26140", cat: "base" },
light: { name: "Light", sym: "☀️", color: "#FFD700", glow: "#FFD70040", cat: "base" },
dark: { name: "Dark", sym: "🌑", color: "#9B59B6", glow: "#9B59B640", cat: "base" },
death: { name: "Death", sym: "💀", color: "#778CA3", glow: "#778CA340", cat: "base" },
// Utility Elements
transference: { name: "Transference", sym: "🔗", color: "#1ABC9C", glow: "#1ABC9C40", cat: "utility" },
// Composite Elements
metal: { name: "Metal", sym: "⚙️", color: "#BDC3C7", glow: "#BDC3C740", cat: "composite", recipe: ["fire", "earth"] },
sand: { name: "Sand", sym: "⏳", color: "#D4AC0D", glow: "#D4AC0D40", cat: "composite", recipe: ["earth", "water"] },
lightning: { name: "Lightning", sym: "⚡", color: "#FFEB3B", glow: "#FFEB3B40", cat: "composite", recipe: ["fire", "air"] },
// Exotic Elements
crystal: { name: "Crystal", sym: "💎", color: "#85C1E9", glow: "#85C1E940", cat: "exotic", recipe: ["sand", "sand", "light"] },
stellar: { name: "Stellar", sym: "⭐", color: "#F0E68C", glow: "#F0E68C40", cat: "exotic", recipe: ["fire", "fire", "light"] },
void: { name: "Void", sym: "🕳️", color: "#4A235A", glow: "#4A235A40", cat: "exotic", recipe: ["dark", "dark", "death"] },
};
// NOTE: Life, Blood, Wood, Mental, and Force mana types have been removed.
// Lifesteal and healing are BANNED from player abilities - see AGENTS.md
// Crystal recipe updated to use light instead of mental.
export const FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "death"];
// ─── Element Opposites for Damage Calculation ────────────────────────────────
export const ELEMENT_OPPOSITES: Record<string, string> = {
fire: 'water', water: 'fire',
air: 'earth', earth: 'air',
light: 'dark', dark: 'light',
lightning: 'earth', // Lightning is weak to earth (grounding)
};
// ─── Element Icon Mapping (Lucide Icons) ──────────────────────────────────────
// Note: These are string identifiers for dynamic icon loading
// The actual Lucide icons are imported in the components
export const ELEMENT_ICON_NAMES: Record<string, string> = {
fire: 'Flame',
water: 'Droplet',
air: 'Wind',
earth: 'Mountain',
light: 'Sun',
dark: 'Moon',
death: 'Skull',
transference: 'Link',
metal: 'Target',
sand: 'Hourglass',
lightning: 'Zap',
crystal: 'Gem',
stellar: 'Star',
void: 'CircleDot',
raw: 'Circle',
};
// ─── Base Unlocked Elements ───────────────────────────────────────────────────
export const BASE_UNLOCKED_ELEMENTS = ['fire', 'water', 'air', 'earth'];
+108
View File
@@ -0,0 +1,108 @@
// ─── Guardians ────────────────────────────────────────────────────────────────
import type { GuardianDef } from '../types';
// All guardians have armor - damage reduction percentage
export const GUARDIANS: Record<number, GuardianDef> = {
10: {
name: "Ignis Prime", element: "fire", hp: 5000, pact: 1.5, color: "#FF6B35",
armor: 0.10, // 10% damage reduction
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Fire damage' },
{ type: 'maxMana', value: 50, desc: '+50 max mana' },
],
pactCost: 500,
pactTime: 2,
uniquePerk: "Fire spells cast 10% faster"
},
20: {
name: "Aqua Regia", element: "water", hp: 15000, pact: 1.75, color: "#4ECDC4",
armor: 0.15,
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Water damage' },
{ type: 'manaRegen', value: 0.5, desc: '+0.5 mana regen' },
],
pactCost: 1000,
pactTime: 4,
uniquePerk: "Water spells deal +15% damage"
},
30: {
name: "Ventus Rex", element: "air", hp: 30000, pact: 2.0, color: "#00D4FF",
armor: 0.18,
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Air damage' },
{ type: 'castingSpeed', value: 5, desc: '+5% cast speed' },
],
pactCost: 2000,
pactTime: 6,
uniquePerk: "Air spells have 15% crit chance"
},
40: {
name: "Terra Firma", element: "earth", hp: 50000, pact: 2.25, color: "#F4A261",
armor: 0.25, // Earth guardian - highest armor
boons: [
{ type: 'elementalDamage', value: 5, desc: '+5% Earth damage' },
{ type: 'maxMana', value: 100, desc: '+100 max mana' },
],
pactCost: 4000,
pactTime: 8,
uniquePerk: "Earth spells deal +25% damage to guardians"
},
50: {
name: "Lux Aeterna", element: "light", hp: 80000, pact: 2.5, color: "#FFD700",
armor: 0.20,
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Light damage' },
{ type: 'insightGain', value: 10, desc: '+10% insight gain' },
],
pactCost: 8000,
pactTime: 10,
uniquePerk: "Light spells reveal enemy weaknesses (+20% damage)"
},
60: {
name: "Umbra Mortis", element: "dark", hp: 120000, pact: 2.75, color: "#9B59B6",
armor: 0.22,
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Dark damage' },
{ type: 'critDamage', value: 15, desc: '+15% crit damage' },
],
pactCost: 15000,
pactTime: 12,
uniquePerk: "Dark spells deal +25% damage to armored enemies"
},
80: {
name: "Mors Ultima", element: "death", hp: 250000, pact: 3.25, color: "#778CA3",
armor: 0.25,
boons: [
{ type: 'elementalDamage', value: 10, desc: '+10% Death damage' },
{ type: 'rawDamage', value: 10, desc: '+10% raw damage' },
],
pactCost: 40000,
pactTime: 16,
uniquePerk: "Death spells execute enemies below 20% HP"
},
90: {
name: "Primordialis", element: "void", hp: 400000, pact: 4.0, color: "#4A235A",
armor: 0.30,
boons: [
{ type: 'elementalDamage', value: 15, desc: '+15% Void damage' },
{ type: 'maxMana', value: 200, desc: '+200 max mana' },
{ type: 'manaRegen', value: 1, desc: '+1 mana regen' },
],
pactCost: 75000,
pactTime: 20,
uniquePerk: "Void spells ignore 30% of enemy resistance"
},
100: {
name: "The Awakened One", element: "stellar", hp: 1000000, pact: 5.0, color: "#F0E68C",
armor: 0.35, // Final boss has highest armor
boons: [
{ type: 'elementalDamage', value: 20, desc: '+20% Stellar damage' },
{ type: 'maxMana', value: 500, desc: '+500 max mana' },
{ type: 'manaRegen', value: 2, desc: '+2 mana regen' },
{ type: 'insightGain', value: 25, desc: '+25% insight gain' },
],
pactCost: 150000,
pactTime: 24,
uniquePerk: "All spells deal +50% damage and cast 25% faster"
},
};
+28
View File
@@ -0,0 +1,28 @@
// ─── Constants Index ────────────────────────────────────────────────────────
// Re-exports all constants for backward compatibility
// Core game constants
export { TICK_MS, HOURS_PER_TICK, MAX_DAY, INCURSION_START_DAY } from './core';
export { RARITY_COLORS } from './core';
export { getStudySpeedMultiplier, getStudyCostMultiplier } from './core';
// Element-related constants
export { MANA_PER_ELEMENT, rawCost, elemCost, ELEMENTS, FLOOR_ELEM_CYCLE } from './elements';
export { ELEMENT_OPPOSITES, ELEMENT_ICON_NAMES, BASE_UNLOCKED_ELEMENTS } from './elements';
// Guardian constants
export { GUARDIANS } from './guardians';
// Spell constants
export { SPELLS_DEF } from './spells';
// Skill constants
export { SKILLS_DEF, SKILL_CATEGORIES, EFFECT_RESEARCH_MAPPING, BASE_UNLOCKED_EFFECTS, ENCHANTING_UNLOCK_EFFECTS } from './skills';
// Prestige constants
export { PRESTIGE_DEF } from './prestige';
// Room constants
export type { RoomType } from './rooms';
export { PUZZLE_ROOM_INTERVAL, SWARM_ROOM_CHANCE, SPEED_ROOM_CHANCE, PUZZLE_ROOM_CHANCE } from './rooms';
export { PUZZLE_ROOMS, SWARM_CONFIG, SPEED_ROOM_CONFIG, FLOOR_ARMOR_CONFIG } from './rooms';
+18
View File
@@ -0,0 +1,18 @@
// ─── Prestige Upgrades ────────────────────────────────────────────────────────
import type { PrestigeDef } from '../types';
export const PRESTIGE_DEF: Record<string, PrestigeDef> = {
manaWell: { name: "Mana Well", desc: "+500 starting max mana", max: 5, cost: 500 },
manaFlow: { name: "Mana Flow", desc: "+0.5 regen/sec permanently", max: 10, cost: 750 },
deepMemory: { name: "Deep Memory", desc: "+1 memory slot", max: 5, cost: 1000 },
insightAmp: { name: "Insight Amp", desc: "+25% insight gain", max: 4, cost: 1500 },
spireKey: { name: "Spire Key", desc: "Start at floor +2", max: 5, cost: 4000 },
temporalEcho: { name: "Temporal Echo", desc: "+10% mana generation", max: 5, cost: 3000 },
steadyHand: { name: "Steady Hand", desc: "-15% durability loss", max: 5, cost: 1200 },
ancientKnowledge: { name: "Ancient Knowledge", desc: "Start with blueprint discovered", max: 5, cost: 2000 },
elementalAttune: { name: "Elemental Attunement", desc: "+25 elemental mana cap", max: 10, cost: 600 },
spellMemory: { name: "Spell Memory", desc: "Start with random spell learned", max: 3, cost: 2500 },
guardianPact: { name: "Guardian Pact", desc: "+10% pact multiplier", max: 5, cost: 3500 },
quickStart: { name: "Quick Start", desc: "Start with 100 raw mana", max: 3, cost: 400 },
elemStart: { name: "Elem. Start", desc: "Start with 5 of each unlocked element", max: 3, cost: 800 },
};
+90
View File
@@ -0,0 +1,90 @@
// ─── Room Types ────────────────────────────────────────────────────────────────
// Room types for spire floors
export type RoomType = 'combat' | 'puzzle' | 'swarm' | 'speed' | 'guardian';
// Room generation rules:
// - Guardian floors (10, 20, 30, etc.) are ALWAYS guardian type
// - Every 5th floor (5, 15, 25, etc.) has a chance for special rooms
// - Other floors are combat with chance for swarm/speed
export const PUZZLE_ROOM_INTERVAL = 7; // Every 7 floors, chance for puzzle
export const SWARM_ROOM_CHANCE = 0.15; // 15% chance for swarm room
export const SPEED_ROOM_CHANCE = 0.10; // 10% chance for speed room
export const PUZZLE_ROOM_CHANCE = 0.20; // 20% chance for puzzle room on puzzle floors
// Puzzle room definitions - themed around attunements
export const PUZZLE_ROOMS: Record<string, {
name: string;
attunements: string[];
baseProgressPerTick: number;
attunementBonus: number;
description: string;
}> = {
enchanter_trial: {
name: "Enchanter's Trial",
attunements: ['enchanter'],
baseProgressPerTick: 0.02,
attunementBonus: 0.03,
description: "Decipher ancient enchantment runes."
},
fabricator_trial: {
name: "Fabricator's Trial",
attunements: ['fabricator'],
baseProgressPerTick: 0.02,
attunementBonus: 0.03,
description: "Construct a mana-powered mechanism."
},
invoker_trial: {
name: "Invoker's Trial",
attunements: ['invoker'],
baseProgressPerTick: 0.02,
attunementBonus: 0.03,
description: "Commune with guardian spirits."
},
hybrid_enchanter_fabricator: {
name: "Fusion Workshop",
attunements: ['enchanter', 'fabricator'],
baseProgressPerTick: 0.015,
attunementBonus: 0.025,
description: "Enchant and construct in harmony."
},
hybrid_enchanter_invoker: {
name: "Ritual Circle",
attunements: ['enchanter', 'invoker'],
baseProgressPerTick: 0.015,
attunementBonus: 0.025,
description: "Bind pact energies into enchantments."
},
hybrid_fabricator_invoker: {
name: "Golem Forge",
attunements: ['fabricator', 'invoker'],
baseProgressPerTick: 0.015,
attunementBonus: 0.025,
description: "Channel guardian power into constructs."
},
};
// Swarm room configuration
export const SWARM_CONFIG = {
minEnemies: 3,
maxEnemies: 6,
hpMultiplier: 0.4, // Each enemy has 40% of normal floor HP
armorBase: 0, // Swarm enemies start with no armor
armorPerFloor: 0.01, // Gain 1% armor per 10 floors
};
// Speed room configuration (dodging enemies)
export const SPEED_ROOM_CONFIG = {
baseDodgeChance: 0.25, // 25% base dodge chance
dodgePerFloor: 0.005, // +0.5% dodge per floor
maxDodge: 0.50, // Max 50% dodge
speedBonus: 0.5, // 50% less time to complete if dodged
};
// Armor scaling for normal floors
export const FLOOR_ARMOR_CONFIG = {
baseChance: 0, // No armor on floor 1-9
chancePerFloor: 0.01, // +1% chance per floor after 10
maxArmorChance: 0.5, // Max 50% of floors have armor
minArmor: 0.05, // Min 5% armor
maxArmor: 0.25, // Max 25% armor on non-guardians
};
+293
View File
@@ -0,0 +1,293 @@
// ─── 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 },
elemAttune: { name: "Elem. Attunement", desc: "+50 elem mana cap", cat: "mana", max: 10, base: 200, studyTime: 4 },
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: "study", 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 } },
disenchanting: { name: "Disenchanting", desc: "Recover 20% mana from removed enchantments", cat: "enchant", max: 3, base: 400, studyTime: 6, req: { enchanting: 2 }, attunementReq: { enchanter: 1 } },
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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, attunementReq: { enchanter: 2 } },
researchLifeDeathSpells: { name: "Death Research", desc: "Unlock Drain spell enchantment", cat: "effectResearch", max: 1, base: 400, studyTime: 8, req: { enchanting: 3 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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: "research", max: 1, base: 300, studyTime: 12 },
manaSurge: { name: "Mana Surge", desc: "+3 mana/click", cat: "research", max: 1, base: 800, studyTime: 36, req: { manaTap: 1 } },
manaSpring: { name: "Mana Spring", desc: "+2 mana regen", cat: "research", max: 1, base: 600, studyTime: 24 },
deepTrance: { name: "Deep Trance", desc: "Extend meditation bonus to 6hrs for 3x", cat: "research", max: 1, base: 900, studyTime: 48, req: { meditation: 1 } },
voidMeditation:{ name: "Void Meditation", desc: "Extend meditation bonus to 8hrs for 5x", cat: "research", max: 1, base: 1500, studyTime: 72, req: { deepTrance: 1 } },
// Ascension Skills (very long study, powerful effects) - Require any attunement level 5+
insightHarvest: { name: "Insight Harvest", desc: "+10% insight gain", cat: "ascension", max: 5, base: 1000, studyTime: 20, attunementReq: { enchanter: 1 } },
guardianBane: { name: "Guardian Bane", desc: "+20% dmg vs guardians", cat: "ascension", max: 3, base: 1500, studyTime: 30, attunementReq: { invoker: 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 },
{ id: 'research', name: 'Research', icon: '🔮', attunement: null },
{ id: 'ascension', name: 'Ascension', icon: '⭐', attunement: null },
// 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'],
};
// 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'];
+826
View File
@@ -0,0 +1,826 @@
// ─── Spells ────────────────────────────────────────────────────────────────────
import type { SpellDef, SpellCost } from '../types';
import { rawCost, elemCost } from './elements';
export const SPELLS_DEF: Record<string, SpellDef> = {
// Tier 0 - Basic Raw Mana Spells (fast, costs raw mana)
manaBolt: {
name: "Mana Bolt",
elem: "raw",
dmg: 5,
cost: rawCost(3),
tier: 0,
castSpeed: 3,
unlock: 0,
studyTime: 0,
desc: "A weak bolt of pure mana. Costs raw mana instead of elemental."
},
manaStrike: {
name: "Mana Strike",
elem: "raw",
dmg: 8,
cost: rawCost(5),
tier: 0,
castSpeed: 2.5,
unlock: 50,
studyTime: 1,
desc: "A concentrated strike of raw mana. Slightly stronger than Mana Bolt."
},
// Tier 1 - Basic Elemental Spells (2-4 hours study)
fireball: {
name: "Fireball",
elem: "fire",
dmg: 15,
cost: elemCost("fire", 2),
tier: 1,
castSpeed: 2,
unlock: 100,
studyTime: 2,
desc: "Hurl a ball of fire at your enemy."
},
emberShot: {
name: "Ember Shot",
elem: "fire",
dmg: 10,
cost: elemCost("fire", 1),
tier: 1,
castSpeed: 3,
unlock: 75,
studyTime: 1,
desc: "A quick shot of embers. Efficient fire damage."
},
waterJet: {
name: "Water Jet",
elem: "water",
dmg: 12,
cost: elemCost("water", 2),
tier: 1,
castSpeed: 2,
unlock: 100,
studyTime: 2,
desc: "A high-pressure jet of water."
},
iceShard: {
name: "Ice Shard",
elem: "water",
dmg: 14,
cost: elemCost("water", 2),
tier: 1,
castSpeed: 2,
unlock: 120,
studyTime: 2,
desc: "Launch a sharp shard of ice."
},
gust: {
name: "Gust",
elem: "air",
dmg: 10,
cost: elemCost("air", 2),
tier: 1,
castSpeed: 3,
unlock: 100,
studyTime: 2,
desc: "A powerful gust of wind."
},
windSlash: {
name: "Wind Slash",
elem: "air",
dmg: 12,
cost: elemCost("air", 2),
tier: 1,
castSpeed: 2.5,
unlock: 110,
studyTime: 2,
desc: "A cutting blade of wind."
},
stoneBullet: {
name: "Stone Bullet",
elem: "earth",
dmg: 16,
cost: elemCost("earth", 2),
tier: 1,
castSpeed: 2,
unlock: 150,
studyTime: 3,
desc: "Launch a bullet of solid stone."
},
rockSpike: {
name: "Rock Spike",
elem: "earth",
dmg: 18,
cost: elemCost("earth", 3),
tier: 1,
castSpeed: 1.5,
unlock: 180,
studyTime: 3,
desc: "Summon a spike of rock from below."
},
lightLance: {
name: "Light Lance",
elem: "light",
dmg: 18,
cost: elemCost("light", 2),
tier: 1,
castSpeed: 2,
unlock: 200,
studyTime: 4,
desc: "A piercing lance of pure light."
},
radiance: {
name: "Radiance",
elem: "light",
dmg: 14,
cost: elemCost("light", 2),
tier: 1,
castSpeed: 2.5,
unlock: 180,
studyTime: 3,
desc: "Burst of radiant energy."
},
shadowBolt: {
name: "Shadow Bolt",
elem: "dark",
dmg: 16,
cost: elemCost("dark", 2),
tier: 1,
castSpeed: 2,
unlock: 200,
studyTime: 4,
desc: "A bolt of shadowy energy."
},
darkPulse: {
name: "Dark Pulse",
elem: "dark",
dmg: 12,
cost: elemCost("dark", 1),
tier: 1,
castSpeed: 3,
unlock: 150,
studyTime: 2,
desc: "A quick pulse of darkness."
},
drain: {
name: "Drain",
elem: "death",
dmg: 10,
cost: elemCost("death", 2),
tier: 1,
castSpeed: 2,
unlock: 150,
studyTime: 3,
desc: "Drain life force from your enemy.",
},
rotTouch: {
name: "Rot Touch",
elem: "death",
dmg: 14,
cost: elemCost("death", 2),
tier: 1,
castSpeed: 2,
unlock: 170,
studyTime: 3,
desc: "Touch of decay and rot."
},
// Tier 2 - Advanced Spells (8-12 hours study)
inferno: {
name: "Inferno",
elem: "fire",
dmg: 60,
cost: elemCost("fire", 8),
tier: 2,
castSpeed: 1,
unlock: 1000,
studyTime: 8,
desc: "Engulf your enemy in flames."
},
flameWave: {
name: "Flame Wave",
elem: "fire",
dmg: 45,
cost: elemCost("fire", 6),
tier: 2,
castSpeed: 1.5,
unlock: 800,
studyTime: 6,
desc: "A wave of fire sweeps across the battlefield."
},
tidalWave: {
name: "Tidal Wave",
elem: "water",
dmg: 55,
cost: elemCost("water", 8),
tier: 2,
castSpeed: 1,
unlock: 1000,
studyTime: 8,
desc: "A massive wave crashes down."
},
iceStorm: {
name: "Ice Storm",
elem: "water",
dmg: 50,
cost: elemCost("water", 7),
tier: 2,
castSpeed: 1.2,
unlock: 900,
studyTime: 7,
desc: "A storm of ice shards."
},
earthquake: {
name: "Earthquake",
elem: "earth",
dmg: 70,
cost: elemCost("earth", 10),
tier: 2,
castSpeed: 0.8,
unlock: 1200,
studyTime: 10,
desc: "Shake the very foundation."
},
stoneBarrage: {
name: "Stone Barrage",
elem: "earth",
dmg: 55,
cost: elemCost("earth", 7),
tier: 2,
castSpeed: 1.2,
unlock: 1000,
studyTime: 8,
desc: "Multiple stone projectiles."
},
hurricane: {
name: "Hurricane",
elem: "air",
dmg: 50,
cost: elemCost("air", 8),
tier: 2,
castSpeed: 1,
unlock: 1000,
studyTime: 8,
desc: "A devastating hurricane."
},
windBlade: {
name: "Wind Blade",
elem: "air",
dmg: 40,
cost: elemCost("air", 5),
tier: 2,
castSpeed: 1.8,
unlock: 700,
studyTime: 6,
desc: "A blade of cutting wind."
},
solarFlare: {
name: "Solar Flare",
elem: "light",
dmg: 65,
cost: elemCost("light", 9),
tier: 2,
castSpeed: 0.9,
unlock: 1100,
studyTime: 9,
desc: "A blinding flare of solar energy."
},
divineSmite: {
name: "Divine Smite",
elem: "light",
dmg: 55,
cost: elemCost("light", 7),
tier: 2,
castSpeed: 1.2,
unlock: 900,
studyTime: 7,
desc: "A smite of divine power."
},
voidRift: {
name: "Void Rift",
elem: "dark",
dmg: 55,
cost: elemCost("dark", 8),
tier: 2,
castSpeed: 1,
unlock: 1000,
studyTime: 8,
desc: "Open a rift to the void."
},
shadowStorm: {
name: "Shadow Storm",
elem: "dark",
dmg: 48,
cost: elemCost("dark", 6),
tier: 2,
castSpeed: 1.3,
unlock: 800,
studyTime: 6,
desc: "A storm of shadows."
},
soulRend: {
name: "Soul Rend",
elem: "death",
dmg: 50,
cost: elemCost("death", 7),
tier: 2,
castSpeed: 1.1,
unlock: 1100,
studyTime: 9,
desc: "Tear at the enemy's soul."
},
// Tier 3 - Master Spells (20-30 hours study)
pyroclasm: {
name: "Pyroclasm",
elem: "fire",
dmg: 250,
cost: elemCost("fire", 25),
tier: 3,
castSpeed: 0.6,
unlock: 10000,
studyTime: 24,
desc: "An eruption of volcanic fury."
},
tsunami: {
name: "Tsunami",
elem: "water",
dmg: 220,
cost: elemCost("water", 22),
tier: 3,
castSpeed: 0.65,
unlock: 10000,
studyTime: 24,
desc: "A towering wall of water."
},
meteorStrike: {
name: "Meteor Strike",
elem: "earth",
dmg: 280,
cost: elemCost("earth", 28),
tier: 3,
castSpeed: 0.5,
unlock: 12000,
studyTime: 28,
desc: "Call down a meteor from the heavens."
},
cosmicStorm: {
name: "Cosmic Storm",
elem: "air",
dmg: 200,
cost: elemCost("air", 20),
tier: 3,
castSpeed: 0.7,
unlock: 10000,
studyTime: 24,
desc: "A storm of cosmic proportions."
},
heavenLight: {
name: "Heaven's Light",
elem: "light",
dmg: 240,
cost: elemCost("light", 24),
tier: 3,
castSpeed: 0.6,
unlock: 11000,
studyTime: 26,
desc: "The light of heaven itself."
},
oblivion: {
name: "Oblivion",
elem: "dark",
dmg: 230,
cost: elemCost("dark", 23),
tier: 3,
castSpeed: 0.6,
unlock: 10500,
studyTime: 25,
desc: "Consign to oblivion."
},
deathMark: {
name: "Death Mark",
elem: "death",
dmg: 200,
cost: elemCost("death", 20),
tier: 3,
castSpeed: 0.7,
unlock: 10000,
studyTime: 24,
desc: "Mark for death."
},
// Tier 4 - Legendary Spells (40-60 hours study, require exotic elements)
stellarNova: {
name: "Stellar Nova",
elem: "stellar",
dmg: 500,
cost: elemCost("stellar", 15),
tier: 4,
castSpeed: 0.4,
unlock: 50000,
studyTime: 48,
desc: "A nova of stellar energy."
},
voidCollapse: {
name: "Void Collapse",
elem: "void",
dmg: 450,
cost: elemCost("void", 12),
tier: 4,
castSpeed: 0.45,
unlock: 40000,
studyTime: 42,
desc: "Collapse the void upon your enemy."
},
crystalShatter: {
name: "Crystal Shatter",
elem: "crystal",
dmg: 400,
cost: elemCost("crystal", 10),
tier: 4,
castSpeed: 0.5,
unlock: 35000,
studyTime: 36,
desc: "Shatter crystalline energy."
},
// ═══════════════════════════════════════════════════════════════════════════
// LIGHTNING SPELLS - Fast, armor-piercing, harder to dodge
// ═══════════════════════════════════════════════════════════════════════════
// Tier 1 - Basic Lightning
spark: {
name: "Spark",
elem: "lightning",
dmg: 8,
cost: elemCost("lightning", 1),
tier: 1,
castSpeed: 4,
unlock: 120,
studyTime: 2,
desc: "A quick spark of lightning. Very fast and hard to dodge.",
effects: [{ type: 'armor_pierce', value: 0.2 }]
},
lightningBolt: {
name: "Lightning Bolt",
elem: "lightning",
dmg: 14,
cost: elemCost("lightning", 2),
tier: 1,
castSpeed: 3,
unlock: 150,
studyTime: 3,
desc: "A bolt of lightning that pierces armor.",
effects: [{ type: 'armor_pierce', value: 0.3 }]
},
// Tier 2 - Advanced Lightning
chainLightning: {
name: "Chain Lightning",
elem: "lightning",
dmg: 25,
cost: elemCost("lightning", 5),
tier: 2,
castSpeed: 2,
unlock: 900,
studyTime: 8,
desc: "Lightning that arcs between enemies. Hits 3 targets.",
isAoe: true,
aoeTargets: 3,
effects: [{ type: 'chain', value: 3 }]
},
stormCall: {
name: "Storm Call",
elem: "lightning",
dmg: 40,
cost: elemCost("lightning", 6),
tier: 2,
castSpeed: 1.5,
unlock: 1100,
studyTime: 10,
desc: "Call down a storm. Hits 2 targets with armor pierce.",
isAoe: true,
aoeTargets: 2,
effects: [{ type: 'armor_pierce', value: 0.4 }]
},
// Tier 3 - Master Lightning
thunderStrike: {
name: "Thunder Strike",
elem: "lightning",
dmg: 150,
cost: elemCost("lightning", 15),
tier: 3,
castSpeed: 0.8,
unlock: 10000,
studyTime: 24,
desc: "Devastating lightning that ignores 50% armor.",
effects: [{ type: 'armor_pierce', value: 0.5 }]
},
// ═══════════════════════════════════════════════════════════════════════════
// AOE SPELLS - Hit multiple enemies, less damage per target
// ═══════════════════════════════════════════════════════════════════════════
// Tier 1 AOE
fireballAoe: {
name: "Fireball (AOE)",
elem: "fire",
dmg: 8,
cost: elemCost("fire", 3),
tier: 1,
castSpeed: 2,
unlock: 150,
studyTime: 3,
desc: "An explosive fireball that hits 3 enemies.",
isAoe: true,
aoeTargets: 3,
effects: [{ type: 'aoe', value: 3 }]
},
frostNova: {
name: "Frost Nova",
elem: "water",
dmg: 6,
cost: elemCost("water", 3),
tier: 1,
castSpeed: 2,
unlock: 140,
studyTime: 3,
desc: "A burst of frost hitting 4 enemies. May freeze.",
isAoe: true,
aoeTargets: 4,
effects: [{ type: 'freeze', value: 0.15, chance: 0.2 }]
},
// Tier 2 AOE
meteorShower: {
name: "Meteor Shower",
elem: "fire",
dmg: 20,
cost: elemCost("fire", 8),
tier: 2,
castSpeed: 1,
unlock: 1200,
studyTime: 10,
desc: "Rain meteors on 5 enemies.",
isAoe: true,
aoeTargets: 5
},
blizzard: {
name: "Blizzard",
elem: "water",
dmg: 18,
cost: elemCost("water", 7),
tier: 2,
castSpeed: 1.2,
unlock: 1000,
studyTime: 9,
desc: "A freezing blizzard hitting 4 enemies.",
isAoe: true,
aoeTargets: 4,
effects: [{ type: 'freeze', value: 0.1, chance: 0.15 }]
},
earthquakeAoe: {
name: "Earth Tremor",
elem: "earth",
dmg: 25,
cost: elemCost("earth", 8),
tier: 2,
castSpeed: 0.8,
unlock: 1400,
studyTime: 10,
desc: "Shake the ground, hitting 3 enemies with high damage.",
isAoe: true,
aoeTargets: 3
},
// Tier 3 AOE
apocalypse: {
name: "Apocalypse",
elem: "fire",
dmg: 80,
cost: elemCost("fire", 20),
tier: 3,
castSpeed: 0.5,
unlock: 15000,
studyTime: 30,
desc: "End times. Hits ALL enemies with devastating fire.",
isAoe: true,
aoeTargets: 10
},
// ═══════════════════════════════════════════════════════════════════════════
// MAGIC SWORD ENCHANTMENTS - For weapon enchanting system
// ═══════════════════════════════════════════════════════════════════════════
fireBlade: {
name: "Fire Blade",
elem: "fire",
dmg: 3,
cost: rawCost(1),
tier: 1,
castSpeed: 4,
unlock: 100,
studyTime: 2,
desc: "Enchant a blade with fire. Burns enemies over time.",
isWeaponEnchant: true,
effects: [{ type: 'burn', value: 2, duration: 3 }]
},
frostBlade: {
name: "Frost Blade",
elem: "water",
dmg: 3,
cost: rawCost(1),
tier: 1,
castSpeed: 4,
unlock: 100,
studyTime: 2,
desc: "Enchant a blade with frost. Prevents enemy dodge.",
isWeaponEnchant: true,
effects: [{ type: 'freeze', value: 0, chance: 1 }] // 100% freeze = no dodge
},
lightningBlade: {
name: "Lightning Blade",
elem: "lightning",
dmg: 4,
cost: rawCost(1),
tier: 1,
castSpeed: 5,
unlock: 150,
studyTime: 3,
desc: "Enchant a blade with lightning. Pierces 30% armor.",
isWeaponEnchant: true,
effects: [{ type: 'armor_pierce', value: 0.3 }]
},
voidBlade: {
name: "Void Blade",
elem: "dark",
dmg: 5,
cost: rawCost(2),
tier: 2,
castSpeed: 3,
unlock: 800,
studyTime: 8,
desc: "Enchant a blade with void. +20% damage.",
isWeaponEnchant: true,
effects: [{ type: 'buff', value: 0.2 }]
},
// ═══════════════════════════════════════════════════════════════════════════
// COMPOUND MANA SPELLS - Blood, Metal, Wood, Sand
// ═══════════════════════════════════════════════════════════════════════════
// ─── METAL SPELLS (Fire + Earth) ─────────────────────────────────────────────
// Metal magic is slow but devastating with high armor pierce
metalShard: {
name: "Metal Shard",
elem: "metal",
dmg: 16,
cost: elemCost("metal", 2),
tier: 1,
castSpeed: 1.8,
unlock: 220,
studyTime: 3,
desc: "A sharpened metal shard. Slower but pierces armor.",
effects: [{ type: 'armor_pierce', value: 0.25 }]
},
ironFist: {
name: "Iron Fist",
elem: "metal",
dmg: 28,
cost: elemCost("metal", 4),
tier: 1,
castSpeed: 1.5,
unlock: 350,
studyTime: 5,
desc: "A crushing fist of iron. High armor pierce.",
effects: [{ type: 'armor_pierce', value: 0.35 }]
},
steelTempest: {
name: "Steel Tempest",
elem: "metal",
dmg: 55,
cost: elemCost("metal", 8),
tier: 2,
castSpeed: 1,
unlock: 1300,
studyTime: 12,
desc: "A whirlwind of steel blades. Ignores much armor.",
effects: [{ type: 'armor_pierce', value: 0.45 }]
},
furnaceBlast: {
name: "Furnace Blast",
elem: "metal",
dmg: 200,
cost: elemCost("metal", 20),
tier: 3,
castSpeed: 0.5,
unlock: 18000,
studyTime: 32,
desc: "Molten metal and fire combined. Devastating armor pierce.",
effects: [{ type: 'armor_pierce', value: 0.6 }]
},
// ─── SAND SPELLS (Earth + Water) ────────────────────────────────────────────
// Sand magic slows enemies and deals steady damage
sandBlast: {
name: "Sand Blast",
elem: "sand",
dmg: 11,
cost: elemCost("sand", 2),
tier: 1,
castSpeed: 3,
unlock: 190,
studyTime: 3,
desc: "A blast of stinging sand. Fast casting.",
},
sandstorm: {
name: "Sandstorm",
elem: "sand",
dmg: 22,
cost: elemCost("sand", 4),
tier: 1,
castSpeed: 2,
unlock: 300,
studyTime: 4,
desc: "A swirling sandstorm. Hits 2 enemies.",
isAoe: true,
aoeTargets: 2,
},
desertWind: {
name: "Desert Wind",
elem: "sand",
dmg: 38,
cost: elemCost("sand", 6),
tier: 2,
castSpeed: 1.5,
unlock: 950,
studyTime: 8,
desc: "A scouring desert wind. Hits 3 enemies.",
isAoe: true,
aoeTargets: 3,
},
duneCollapse: {
name: "Dune Collapse",
elem: "sand",
dmg: 100,
cost: elemCost("sand", 16),
tier: 3,
castSpeed: 0.6,
unlock: 14000,
studyTime: 28,
desc: "Dunes collapse on all enemies. Hits 5 targets.",
isAoe: true,
aoeTargets: 5,
},
// ═══════════════════════════════════════════════════════════════════════════
// UTILITY MANA SPELLS - Mental, Transference, Force
// ═══════════════════════════════════════════════════════════════════════════
// ─── TRANSFERENCE SPELLS ─────────────────────────────────────────────────────
// Transference magic moves mana and enhances efficiency
transferStrike: {
name: "Transfer Strike",
elem: "transference",
dmg: 9,
cost: elemCost("transference", 2),
tier: 1,
castSpeed: 3,
unlock: 150,
studyTime: 2,
desc: "Strike that transfers energy. Very efficient.",
},
manaRip: {
name: "Mana Rip",
elem: "transference",
dmg: 16,
cost: elemCost("transference", 3),
tier: 1,
castSpeed: 2.5,
unlock: 250,
studyTime: 4,
desc: "Rip mana from the enemy. High efficiency.",
},
essenceDrain: {
name: "Essence Drain",
elem: "transference",
dmg: 42,
cost: elemCost("transference", 7),
tier: 2,
castSpeed: 1.3,
unlock: 1050,
studyTime: 10,
desc: "Drain the enemy's essence.",
},
soulTransfer: {
name: "Soul Transfer",
elem: "transference",
dmg: 130,
cost: elemCost("transference", 16),
tier: 3,
castSpeed: 0.6,
unlock: 13000,
studyTime: 26,
desc: "Transfer the soul's energy.",
},
};
+19 -844
View File
@@ -1,846 +1,21 @@
// ─── Enchantment Effects Catalogue ──────────────────────────────────────────────── // ─── Enchantment Effects Catalogue ────────────────────────────────────────────────
// All available enchantment effects that can be applied to equipment // Re-exports from category-specific files for backward compatibility
// All enchantment effect definitions have been moved to src/lib/game/data/enchantments/
import type { EquipmentCategory } from './equipment' export {
ENCHANTMENT_EFFECTS,
// Helper to define allowed equipment categories for each effect type type EnchantmentEffectCategory,
const ALL_CASTER: EquipmentCategory[] = ['caster'] type EnchantmentEffectDef,
const CASTER_AND_SWORD: EquipmentCategory[] = ['caster', 'sword'] getEnchantmentEffect,
const WEAPON_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'sword'] // All main hand equipment getEffectsForEquipment,
const CASTER_AND_HANDS: EquipmentCategory[] = ['caster', 'hands'] canApplyEffect,
const BODY_AND_SHIELD: EquipmentCategory[] = ['body', 'shield'] calculateEffectCapacityCost,
const CASTER_CATALYST_ACCESSORY: EquipmentCategory[] = ['caster', 'catalyst', 'accessory'] // Also export category-specific collections
const MANA_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'accessory'] SPELL_EFFECTS,
const UTILITY_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'hands', 'feet', 'accessory'] MANA_EFFECTS,
const ALL_EQUIPMENT: EquipmentCategory[] = ['caster', 'shield', 'catalyst', 'sword', 'head', 'body', 'hands', 'feet', 'accessory'] COMBAT_EFFECTS,
ELEMENTAL_EFFECTS,
export type EnchantmentEffectCategory = 'spell' | 'mana' | 'combat' | 'elemental' | 'defense' | 'utility' | 'special' DEFENSE_EFFECTS,
UTILITY_EFFECTS,
export interface EnchantmentEffectDef { SPECIAL_EFFECTS,
id: string; } from './enchantments/index'
name: string;
description: string;
category: EnchantmentEffectCategory;
baseCapacityCost: number;
maxStacks: number;
allowedEquipmentCategories: EquipmentCategory[];
effect: {
type: 'spell' | 'bonus' | 'multiplier' | 'special';
spellId?: string;
stat?: string;
value?: number;
specialId?: string;
};
}
export const ENCHANTMENT_EFFECTS: Record<string, EnchantmentEffectDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// SPELL EFFECTS - Only for CASTER equipment (staves, wands, rods, orbs)
// ═══════════════════════════════════════════════════════════════════════════
// Tier 0 - Basic Spells
spell_manaBolt: {
id: 'spell_manaBolt',
name: 'Mana Bolt',
description: 'Grants the ability to cast Mana Bolt (5 base damage, raw mana cost)',
category: 'spell',
baseCapacityCost: 50,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'manaBolt' }
},
spell_manaStrike: {
id: 'spell_manaStrike',
name: 'Mana Strike',
description: 'Grants the ability to cast Mana Strike (8 base damage, raw mana cost)',
category: 'spell',
baseCapacityCost: 40,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'manaStrike' }
},
// Tier 1 - Basic Elemental Spells
spell_fireball: {
id: 'spell_fireball',
name: 'Fireball',
description: 'Grants the ability to cast Fireball (15 fire damage)',
category: 'spell',
baseCapacityCost: 80,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'fireball' }
},
spell_emberShot: {
id: 'spell_emberShot',
name: 'Ember Shot',
description: 'Grants the ability to cast Ember Shot (10 fire damage, fast cast)',
category: 'spell',
baseCapacityCost: 60,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'emberShot' }
},
spell_waterJet: {
id: 'spell_waterJet',
name: 'Water Jet',
description: 'Grants the ability to cast Water Jet (12 water damage)',
category: 'spell',
baseCapacityCost: 70,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'waterJet' }
},
spell_iceShard: {
id: 'spell_iceShard',
name: 'Ice Shard',
description: 'Grants the ability to cast Ice Shard (14 water damage)',
category: 'spell',
baseCapacityCost: 75,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'iceShard' }
},
spell_gust: {
id: 'spell_gust',
name: 'Gust',
description: 'Grants the ability to cast Gust (10 air damage, fast cast)',
category: 'spell',
baseCapacityCost: 60,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'gust' }
},
spell_stoneBullet: {
id: 'spell_stoneBullet',
name: 'Stone Bullet',
description: 'Grants the ability to cast Stone Bullet (16 earth damage)',
category: 'spell',
baseCapacityCost: 80,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'stoneBullet' }
},
spell_lightLance: {
id: 'spell_lightLance',
name: 'Light Lance',
description: 'Grants the ability to cast Light Lance (18 light damage)',
category: 'spell',
baseCapacityCost: 95,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'lightLance' }
},
spell_shadowBolt: {
id: 'spell_shadowBolt',
name: 'Shadow Bolt',
description: 'Grants the ability to cast Shadow Bolt (16 dark damage)',
category: 'spell',
baseCapacityCost: 95,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'shadowBolt' }
},
spell_drain: {
id: 'spell_drain',
name: 'Drain',
description: 'Grants the ability to cast Drain (10 death damage)',
category: 'spell',
baseCapacityCost: 85,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'drain' }
},
// Tier 2 - Advanced Spells
spell_inferno: {
id: 'spell_inferno',
name: 'Inferno',
description: 'Grants the ability to cast Inferno (60 fire damage)',
category: 'spell',
baseCapacityCost: 180,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'inferno' }
},
spell_tidalWave: {
id: 'spell_tidalWave',
name: 'Tidal Wave',
description: 'Grants the ability to cast Tidal Wave (55 water damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'tidalWave' }
},
spell_hurricane: {
id: 'spell_hurricane',
name: 'Hurricane',
description: 'Grants the ability to cast Hurricane (50 air damage)',
category: 'spell',
baseCapacityCost: 170,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'hurricane' }
},
spell_earthquake: {
id: 'spell_earthquake',
name: 'Earthquake',
description: 'Grants the ability to cast Earthquake (70 earth damage)',
category: 'spell',
baseCapacityCost: 200,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'earthquake' }
},
spell_solarFlare: {
id: 'spell_solarFlare',
name: 'Solar Flare',
description: 'Grants the ability to cast Solar Flare (65 light damage)',
category: 'spell',
baseCapacityCost: 190,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'solarFlare' }
},
spell_voidRift: {
id: 'spell_voidRift',
name: 'Void Rift',
description: 'Grants the ability to cast Void Rift (55 dark damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'voidRift' }
},
// Additional Tier 1 Spells
spell_windSlash: {
id: 'spell_windSlash',
name: 'Wind Slash',
description: 'Grants the ability to cast Wind Slash (12 air damage)',
category: 'spell',
baseCapacityCost: 72,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'windSlash' }
},
spell_rockSpike: {
id: 'spell_rockSpike',
name: 'Rock Spike',
description: 'Grants the ability to cast Rock Spike (18 earth damage)',
category: 'spell',
baseCapacityCost: 88,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'rockSpike' }
},
spell_radiance: {
id: 'spell_radiance',
name: 'Radiance',
description: 'Grants the ability to cast Radiance (14 light damage)',
category: 'spell',
baseCapacityCost: 80,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'radiance' }
},
spell_darkPulse: {
id: 'spell_darkPulse',
name: 'Dark Pulse',
description: 'Grants the ability to cast Dark Pulse (12 dark damage)',
category: 'spell',
baseCapacityCost: 68,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'darkPulse' }
},
// Additional Tier 2 Spells
spell_flameWave: {
id: 'spell_flameWave',
name: 'Flame Wave',
description: 'Grants the ability to cast Flame Wave (45 fire damage)',
category: 'spell',
baseCapacityCost: 165,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'flameWave' }
},
spell_iceStorm: {
id: 'spell_iceStorm',
name: 'Ice Storm',
description: 'Grants the ability to cast Ice Storm (50 water damage)',
category: 'spell',
baseCapacityCost: 170,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'iceStorm' }
},
spell_windBlade: {
id: 'spell_windBlade',
name: 'Wind Blade',
description: 'Grants the ability to cast Wind Blade (40 air damage)',
category: 'spell',
baseCapacityCost: 155,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'windBlade' }
},
spell_stoneBarrage: {
id: 'spell_stoneBarrage',
name: 'Stone Barrage',
description: 'Grants the ability to cast Stone Barrage (55 earth damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'stoneBarrage' }
},
spell_divineSmite: {
id: 'spell_divineSmite',
name: 'Divine Smite',
description: 'Grants the ability to cast Divine Smite (55 light damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'divineSmite' }
},
spell_shadowStorm: {
id: 'spell_shadowStorm',
name: 'Shadow Storm',
description: 'Grants the ability to cast Shadow Storm (48 dark damage)',
category: 'spell',
baseCapacityCost: 168,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'shadowStorm' }
},
// Tier 3 - Master Spells
spell_pyroclasm: {
id: 'spell_pyroclasm',
name: 'Pyroclasm',
description: 'Grants the ability to cast Pyroclasm (250 fire damage)',
category: 'spell',
baseCapacityCost: 400,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'pyroclasm' }
},
spell_tsunami: {
id: 'spell_tsunami',
name: 'Tsunami',
description: 'Grants the ability to cast Tsunami (220 water damage)',
category: 'spell',
baseCapacityCost: 380,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'tsunami' }
},
spell_meteorStrike: {
id: 'spell_meteorStrike',
name: 'Meteor Strike',
description: 'Grants the ability to cast Meteor Strike (280 earth damage)',
category: 'spell',
baseCapacityCost: 420,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'meteorStrike' }
},
// ═══════════════════════════════════════════════════════════════════════════
// MANA EFFECTS - Boost mana capacity and regeneration
// ═══════════════════════════════════════════════════════════════════════════
mana_cap_50: {
id: 'mana_cap_50',
name: 'Mana Reserve',
description: '+50 maximum mana',
category: 'mana',
baseCapacityCost: 20,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'maxMana', value: 50 }
},
mana_cap_100: {
id: 'mana_cap_100',
name: 'Mana Reservoir',
description: '+100 maximum mana',
category: 'mana',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'maxMana', value: 100 }
},
mana_regen_1: {
id: 'mana_regen_1',
name: 'Trickle',
description: '+1 mana per hour regeneration',
category: 'mana',
baseCapacityCost: 15,
maxStacks: 5,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'regen', value: 1 }
},
mana_regen_2: {
id: 'mana_regen_2',
name: 'Stream',
description: '+2 mana per hour regeneration',
category: 'mana',
baseCapacityCost: 28,
maxStacks: 4,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'regen', value: 2 }
},
mana_regen_5: {
id: 'mana_regen_5',
name: 'River',
description: '+5 mana per hour regeneration',
category: 'mana',
baseCapacityCost: 50,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'regen', value: 5 }
},
click_mana_1: {
id: 'click_mana_1',
name: 'Mana Tap',
description: '+1 mana per click',
category: 'mana',
baseCapacityCost: 20,
maxStacks: 5,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'clickMana', value: 1 }
},
click_mana_3: {
id: 'click_mana_3',
name: 'Mana Surge',
description: '+3 mana per click',
category: 'mana',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'clickMana', value: 3 }
},
// ═══════════════════════════════════════════════════════════════════════════
// COMBAT EFFECTS - Damage and attack enhancements
// ═══════════════════════════════════════════════════════════════════════════
damage_5: {
id: 'damage_5',
name: 'Minor Power',
description: '+5 base damage',
category: 'combat',
baseCapacityCost: 15,
maxStacks: 5,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'bonus', stat: 'baseDamage', value: 5 }
},
damage_10: {
id: 'damage_10',
name: 'Moderate Power',
description: '+10 base damage',
category: 'combat',
baseCapacityCost: 28,
maxStacks: 4,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'bonus', stat: 'baseDamage', value: 10 }
},
damage_pct_10: {
id: 'damage_pct_10',
name: 'Amplification',
description: '+10% damage',
category: 'combat',
baseCapacityCost: 30,
maxStacks: 3,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'multiplier', stat: 'baseDamage', value: 1.10 }
},
crit_5: {
id: 'crit_5',
name: 'Sharp Edge',
description: '+5% critical hit chance',
category: 'combat',
baseCapacityCost: 20,
maxStacks: 4,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'bonus', stat: 'critChance', value: 0.05 }
},
attack_speed_10: {
id: 'attack_speed_10',
name: 'Swift Casting',
description: '+10% attack speed',
category: 'combat',
baseCapacityCost: 22,
maxStacks: 4,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'multiplier', stat: 'attackSpeed', value: 1.10 }
},
// ═══════════════════════════════════════════════════════════════════════════
// UTILITY EFFECTS - Study speed, insight, meditation
// ═══════════════════════════════════════════════════════════════════════════
meditate_10: {
id: 'meditate_10',
name: 'Meditative Focus',
description: '+10% meditation efficiency',
category: 'utility',
baseCapacityCost: 18,
maxStacks: 5,
allowedEquipmentCategories: ['head', 'body', 'accessory'],
effect: { type: 'multiplier', stat: 'meditationEfficiency', value: 1.10 }
},
study_10: {
id: 'study_10',
name: 'Quick Study',
description: '+10% study speed',
category: 'utility',
baseCapacityCost: 22,
maxStacks: 4,
allowedEquipmentCategories: UTILITY_EQUIPMENT,
effect: { type: 'multiplier', stat: 'studySpeed', value: 1.10 }
},
insight_5: {
id: 'insight_5',
name: 'Insightful',
description: '+5% insight gain',
category: 'utility',
baseCapacityCost: 25,
maxStacks: 4,
allowedEquipmentCategories: ['head', 'accessory'],
effect: { type: 'multiplier', stat: 'insightGain', value: 1.05 }
},
// ═══════════════════════════════════════════════════════════════════════════
// SPECIAL EFFECTS - Unique and powerful effects
// ═══════════════════════════════════════════════════════════════════════════
spell_echo_10: {
id: 'spell_echo_10',
name: 'Echo Chamber',
description: '10% chance to cast a spell twice',
category: 'special',
baseCapacityCost: 60,
maxStacks: 2,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'special', specialId: 'spellEcho10' }
},
guardian_dmg_10: {
id: 'guardian_dmg_10',
name: 'Bane',
description: '+10% damage to guardians',
category: 'special',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: CASTER_CATALYST_ACCESSORY,
effect: { type: 'multiplier', stat: 'guardianDamage', value: 1.10 }
},
overpower_80: {
id: 'overpower_80',
name: 'Overpower',
description: '+50% damage when mana above 80%',
category: 'special',
baseCapacityCost: 55,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'special', specialId: 'overpower' }
},
// ═══════════════════════════════════════════════════════════════════════════
// WEAPON MANA EFFECTS - Enchanter level 3+ unlocks these
// These add mana capacity and regeneration to weapons for their enchantments
// ═══════════════════════════════════════════════════════════════════════════
weapon_mana_cap_20: {
id: 'weapon_mana_cap_20',
name: 'Mana Cell',
description: '+20 weapon mana capacity (for weapon enchantments)',
category: 'mana',
baseCapacityCost: 25,
maxStacks: 5,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaMax', value: 20 }
},
weapon_mana_cap_50: {
id: 'weapon_mana_cap_50',
name: 'Mana Vessel',
description: '+50 weapon mana capacity (for weapon enchantments)',
category: 'mana',
baseCapacityCost: 50,
maxStacks: 3,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaMax', value: 50 }
},
weapon_mana_cap_100: {
id: 'weapon_mana_cap_100',
name: 'Mana Core',
description: '+100 weapon mana capacity (for weapon enchantments)',
category: 'mana',
baseCapacityCost: 80,
maxStacks: 2,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaMax', value: 100 }
},
weapon_mana_regen_1: {
id: 'weapon_mana_regen_1',
name: 'Mana Wick',
description: '+1 weapon mana regeneration per hour',
category: 'mana',
baseCapacityCost: 20,
maxStacks: 5,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 1 }
},
weapon_mana_regen_2: {
id: 'weapon_mana_regen_2',
name: 'Mana Siphon',
description: '+2 weapon mana regeneration per hour',
category: 'mana',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 2 }
},
weapon_mana_regen_5: {
id: 'weapon_mana_regen_5',
name: 'Mana Well',
description: '+5 weapon mana regeneration per hour',
category: 'mana',
baseCapacityCost: 60,
maxStacks: 2,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 5 }
},
// ═══════════════════════════════════════════════════════════════════════════
// LIGHTNING SPELL EFFECTS - Fast, armor-piercing, harder to dodge
// ═══════════════════════════════════════════════════════════════════════════
spell_spark: {
id: 'spell_spark',
name: 'Spark',
description: 'Grants the ability to cast Spark (8 lightning damage, very fast, armor pierce)',
category: 'spell',
baseCapacityCost: 70,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'spark' }
},
spell_lightningBolt: {
id: 'spell_lightningBolt',
name: 'Lightning Bolt',
description: 'Grants the ability to cast Lightning Bolt (14 lightning damage, armor pierce)',
category: 'spell',
baseCapacityCost: 90,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'lightningBolt' }
},
spell_chainLightning: {
id: 'spell_chainLightning',
name: 'Chain Lightning',
description: 'Grants the ability to cast Chain Lightning (25 lightning damage, hits 3 targets)',
category: 'spell',
baseCapacityCost: 160,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'chainLightning' }
},
spell_stormCall: {
id: 'spell_stormCall',
name: 'Storm Call',
description: 'Grants the ability to cast Storm Call (40 lightning damage, hits 2 targets)',
category: 'spell',
baseCapacityCost: 190,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'stormCall' }
},
spell_thunderStrike: {
id: 'spell_thunderStrike',
name: 'Thunder Strike',
description: 'Grants the ability to cast Thunder Strike (150 lightning damage, 50% armor pierce)',
category: 'spell',
baseCapacityCost: 350,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'thunderStrike' }
},
// ═══════════════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════════════
// METAL SPELL EFFECTS - Fire + Earth compound, armor pierce focus
// ═══════════════════════════════════════════════════════════════════════════
spell_metalShard: {
id: 'spell_metalShard',
name: 'Metal Shard',
description: 'Grants the ability to cast Metal Shard (16 metal damage, 25% armor pierce)',
category: 'spell',
baseCapacityCost: 85,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'metalShard' }
},
spell_ironFist: {
id: 'spell_ironFist',
name: 'Iron Fist',
description: 'Grants the ability to cast Iron Fist (28 metal damage, 35% armor pierce)',
category: 'spell',
baseCapacityCost: 120,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'ironFist' }
},
spell_steelTempest: {
id: 'spell_steelTempest',
name: 'Steel Tempest',
description: 'Grants the ability to cast Steel Tempest (55 metal damage, 45% armor pierce)',
category: 'spell',
baseCapacityCost: 190,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'steelTempest' }
},
spell_furnaceBlast: {
id: 'spell_furnaceBlast',
name: 'Furnace Blast',
description: 'Grants the ability to cast Furnace Blast (200 metal damage, 60% armor pierce)',
category: 'spell',
baseCapacityCost: 400,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'furnaceBlast' }
},
// ═══════════════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════════════
// SAND SPELL EFFECTS - Earth + Water compound, AOE focus
// ═══════════════════════════════════════════════════════════════════════════
spell_sandBlast: {
id: 'spell_sandBlast',
name: 'Sand Blast',
description: 'Grants the ability to cast Sand Blast (11 sand damage, very fast)',
category: 'spell',
baseCapacityCost: 72,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'sandBlast' }
},
spell_sandstorm: {
id: 'spell_sandstorm',
name: 'Sandstorm',
description: 'Grants the ability to cast Sandstorm (22 sand damage, hits 2 enemies)',
category: 'spell',
baseCapacityCost: 100,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'sandstorm' }
},
spell_desertWind: {
id: 'spell_desertWind',
name: 'Desert Wind',
description: 'Grants the ability to cast Desert Wind (38 sand damage, hits 3 enemies)',
category: 'spell',
baseCapacityCost: 155,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'desertWind' }
},
spell_duneCollapse: {
id: 'spell_duneCollapse',
name: 'Dune Collapse',
description: 'Grants the ability to cast Dune Collapse (100 sand damage, hits 5 enemies)',
category: 'spell',
baseCapacityCost: 300,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'duneCollapse' }
},
// ═══════════════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════════════
// MAGIC SWORD ENCHANTMENTS - Elemental weapon effects
// ═══════════════════════════════════════════════════════════════════════════
sword_fire: {
id: 'sword_fire',
name: 'Fire Enchant',
description: 'Enchant blade with fire. Burns enemies over time.',
category: 'elemental',
baseCapacityCost: 40,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'fireBlade' }
},
sword_frost: {
id: 'sword_frost',
name: 'Frost Enchant',
description: 'Enchant blade with frost. Prevents enemy dodge.',
category: 'elemental',
baseCapacityCost: 40,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'frostBlade' }
},
sword_lightning: {
id: 'sword_lightning',
name: 'Lightning Enchant',
description: 'Enchant blade with lightning. Pierces 30% armor.',
category: 'elemental',
baseCapacityCost: 50,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'lightningBlade' }
},
sword_void: {
id: 'sword_void',
name: 'Void Enchant',
description: 'Enchant blade with void. +20% damage bonus.',
category: 'elemental',
baseCapacityCost: 60,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'voidBlade' }
},
};
// ─── Helper Functions ────────────────────────────────────────────────────────────
export function getEnchantmentEffect(id: string): EnchantmentEffectDef | undefined {
return ENCHANTMENT_EFFECTS[id];
}
export function getEffectsForEquipment(equipmentCategory: EquipmentCategory): EnchantmentEffectDef[] {
return Object.values(ENCHANTMENT_EFFECTS).filter(effect =>
effect.allowedEquipmentCategories.includes(equipmentCategory)
);
}
export function canApplyEffect(effectId: string, equipmentCategory: EquipmentCategory): boolean {
const effect = ENCHANTMENT_EFFECTS[effectId];
if (!effect) return false;
return effect.allowedEquipmentCategories.includes(equipmentCategory);
}
export function calculateEffectCapacityCost(effectId: string, stacks: number, efficiencyBonus: number = 0): number {
const effect = ENCHANTMENT_EFFECTS[effectId];
if (!effect) return 0;
let totalCost = 0;
for (let i = 0; i < stacks; i++) {
// Each additional stack costs 20% more
const stackMultiplier = 1 + (i * 0.2);
totalCost += effect.baseCapacityCost * stackMultiplier;
}
// Apply efficiency bonus (reduces cost)
return Math.floor(totalCost * (1 - efficiencyBonus));
}
+23
View File
@@ -0,0 +1,23 @@
// ─── Enchantment Types ─────────────────────────────────────────────────
// Shared types for enchantment effects
import type { EquipmentCategory } from './equipment'
export type EnchantmentEffectCategory = 'spell' | 'mana' | 'combat' | 'elemental' | 'defense' | 'utility' | 'special'
export interface EnchantmentEffectDef {
id: string;
name: string;
description: string;
category: EnchantmentEffectCategory;
baseCapacityCost: number;
maxStacks: number;
allowedEquipmentCategories: EquipmentCategory[];
effect: {
type: 'spell' | 'bonus' | 'multiplier' | 'special';
spellId?: string;
stat?: string;
value?: number;
specialId?: string;
};
}
@@ -0,0 +1,65 @@
// ─── Combat Enchantment Effects ─────────────────────────────────────────────
// All combat-related enchantment effects that can be applied to equipment
import type { EquipmentCategory } from '../equipment'
import type { EnchantmentEffectDef } from '../enchantment-types'
// Helper to define allowed equipment categories for each effect type
const CASTER_AND_HANDS: EquipmentCategory[] = ['caster', 'hands']
export const COMBAT_EFFECTS: Record<string, EnchantmentEffectDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// COMBAT EFFECTS - Damage and attack enhancements
// ═══════════════════════════════════════════════════════════════════════════
damage_5: {
id: 'damage_5',
name: 'Minor Power',
description: '+5 base damage',
category: 'combat',
baseCapacityCost: 15,
maxStacks: 5,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'bonus', stat: 'baseDamage', value: 5 }
},
damage_10: {
id: 'damage_10',
name: 'Moderate Power',
description: '+10 base damage',
category: 'combat',
baseCapacityCost: 28,
maxStacks: 4,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'bonus', stat: 'baseDamage', value: 10 }
},
damage_pct_10: {
id: 'damage_pct_10',
name: 'Amplification',
description: '+10% damage',
category: 'combat',
baseCapacityCost: 30,
maxStacks: 3,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'multiplier', stat: 'baseDamage', value: 1.10 }
},
crit_5: {
id: 'crit_5',
name: 'Sharp Edge',
description: '+5% critical hit chance',
category: 'combat',
baseCapacityCost: 20,
maxStacks: 4,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'bonus', stat: 'critChance', value: 0.05 }
},
attack_speed_10: {
id: 'attack_speed_10',
name: 'Swift Casting',
description: '+10% attack speed',
category: 'combat',
baseCapacityCost: 22,
maxStacks: 4,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'multiplier', stat: 'attackSpeed', value: 1.10 }
},
};
@@ -0,0 +1,8 @@
// ─── Defense Enchantment Effects ─────────────────────────────────────────
// All defense-related enchantment effects that can be applied to equipment
// Currently empty - no defense effects defined in the original enchantment-effects.ts
import type { EquipmentCategory } from '../equipment'
import type { EnchantmentEffectDef } from '../enchantment-types'
export const DEFENSE_EFFECTS: Record<string, EnchantmentEffectDef> = {};
@@ -0,0 +1,55 @@
// ─── Elemental Enchantment Effects ─────────────────────────────────────────
// All elemental-related enchantment effects that can be applied to equipment
import type { EquipmentCategory } from '../equipment'
import type { EnchantmentEffectDef } from '../enchantment-types'
// Helper to define allowed equipment categories for each effect type
const CASTER_AND_SWORD: EquipmentCategory[] = ['caster', 'sword']
export const ELEMENTAL_EFFECTS: Record<string, EnchantmentEffectDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// MAGIC SWORD ENCHANTMENTS - Elemental weapon effects
// ═══════════════════════════════════════════════════════════════════════════
sword_fire: {
id: 'sword_fire',
name: 'Fire Enchant',
description: 'Enchant blade with fire. Burns enemies over time.',
category: 'elemental',
baseCapacityCost: 40,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'fireBlade' }
},
sword_frost: {
id: 'sword_frost',
name: 'Frost Enchant',
description: 'Enchant blade with frost. Prevents enemy dodge.',
category: 'elemental',
baseCapacityCost: 40,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'frostBlade' }
},
sword_lightning: {
id: 'sword_lightning',
name: 'Lightning Enchant',
description: 'Enchant blade with lightning. Pierces 30% armor.',
category: 'elemental',
baseCapacityCost: 50,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'lightningBlade' }
},
sword_void: {
id: 'sword_void',
name: 'Void Enchant',
description: 'Enchant blade with void. +20% damage bonus.',
category: 'elemental',
baseCapacityCost: 60,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_SWORD,
effect: { type: 'special', specialId: 'voidBlade' }
},
};
+69
View File
@@ -0,0 +1,69 @@
// ─── Enchantment Effects Index ─────────────────────────────────────────
// Re-exports everything from category-specific files for backward compatibility
// Import types
import type { EnchantmentEffectCategory, EnchantmentEffectDef } from '../enchantment-types'
// Import all category-specific effect collections
import { SPELL_EFFECTS } from './spell-effects'
import { MANA_EFFECTS } from './mana-effects'
import { COMBAT_EFFECTS } from './combat-effects'
import { ELEMENTAL_EFFECTS } from './elemental-effects'
import { DEFENSE_EFFECTS } from './defense-effects'
import { UTILITY_EFFECTS } from './utility-effects'
import { SPECIAL_EFFECTS } from './special-effects'
// Merge all effects into a single record for backward compatibility
export const ENCHANTMENT_EFFECTS: Record<string, EnchantmentEffectDef> = {
...SPELL_EFFECTS,
...MANA_EFFECTS,
...COMBAT_EFFECTS,
...ELEMENTAL_EFFECTS,
...DEFENSE_EFFECTS,
...UTILITY_EFFECTS,
...SPECIAL_EFFECTS,
}
// ─── Helper Functions ────────────────────────────────────────────────────────
export function getEnchantmentEffect(id: string): EnchantmentEffectDef | undefined {
return ENCHANTMENT_EFFECTS[id];
}
export function getEffectsForEquipment(equipmentCategory: EquipmentCategory): EnchantmentEffectDef[] {
return Object.values(ENCHANTMENT_EFFECTS).filter(effect =>
effect.allowedEquipmentCategories.includes(equipmentCategory)
);
}
export function canApplyEffect(effectId: string, equipmentCategory: EquipmentCategory): boolean {
const effect = ENCHANTMENT_EFFECTS[effectId];
if (!effect) return false;
return effect.allowedEquipmentCategories.includes(equipmentCategory);
}
export function calculateEffectCapacityCost(effectId: string, stacks: number, efficiencyBonus: number = 0): number {
const effect = ENCHANTMENT_EFFECTS[effectId];
if (!effect) return 0;
let totalCost = 0;
for (let i = 0; i < stacks; i++) {
// Each additional stack costs 20% more
const stackMultiplier = 1 + (i * 0.2);
totalCost += effect.baseCapacityCost * stackMultiplier;
}
// Apply efficiency bonus (reduces cost)
return Math.floor(totalCost * (1 - efficiencyBonus));
}
// Re-export category-specific collections for direct access if needed
export {
SPELL_EFFECTS,
MANA_EFFECTS,
COMBAT_EFFECTS,
ELEMENTAL_EFFECTS,
DEFENSE_EFFECTS,
UTILITY_EFFECTS,
SPECIAL_EFFECTS,
}
@@ -0,0 +1,152 @@
// ─── Mana Enchantment Effects ────────────────────────────────────────────────
// All mana-related enchantment effects that can be applied to equipment
import type { EquipmentCategory } from '../equipment'
import type { EnchantmentEffectDef } from '../enchantment-types'
// Helper to define allowed equipment categories for each effect type
const MANA_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'accessory']
const WEAPON_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'sword'] // All main hand equipment
export const MANA_EFFECTS: Record<string, EnchantmentEffectDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// MANA EFFECTS - Boost mana capacity and regeneration
// ═══════════════════════════════════════════════════════════════════════════
mana_cap_50: {
id: 'mana_cap_50',
name: 'Mana Reserve',
description: '+50 maximum mana',
category: 'mana',
baseCapacityCost: 20,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'maxMana', value: 50 }
},
mana_cap_100: {
id: 'mana_cap_100',
name: 'Mana Reservoir',
description: '+100 maximum mana',
category: 'mana',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'maxMana', value: 100 }
},
mana_regen_1: {
id: 'mana_regen_1',
name: 'Trickle',
description: '+1 mana per hour regeneration',
category: 'mana',
baseCapacityCost: 15,
maxStacks: 5,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'regen', value: 1 }
},
mana_regen_2: {
id: 'mana_regen_2',
name: 'Stream',
description: '+2 mana per hour regeneration',
category: 'mana',
baseCapacityCost: 28,
maxStacks: 4,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'regen', value: 2 }
},
mana_regen_5: {
id: 'mana_regen_5',
name: 'River',
description: '+5 mana per hour regeneration',
category: 'mana',
baseCapacityCost: 50,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'regen', value: 5 }
},
click_mana_1: {
id: 'click_mana_1',
name: 'Mana Tap',
description: '+1 mana per click',
category: 'mana',
baseCapacityCost: 20,
maxStacks: 5,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'clickMana', value: 1 }
},
click_mana_3: {
id: 'click_mana_3',
name: 'Mana Surge',
description: '+3 mana per click',
category: 'mana',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: MANA_EQUIPMENT,
effect: { type: 'bonus', stat: 'clickMana', value: 3 }
},
// ═══════════════════════════════════════════════════════════════════════════
// WEAPON MANA EFFECTS - Enchanter level 3+ unlocks these
// These add mana capacity and regeneration to weapons for their enchantments
// ═══════════════════════════════════════════════════════════════════════════
weapon_mana_cap_20: {
id: 'weapon_mana_cap_20',
name: 'Mana Cell',
description: '+20 weapon mana capacity (for weapon enchantments)',
category: 'mana',
baseCapacityCost: 25,
maxStacks: 5,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaMax', value: 20 }
},
weapon_mana_cap_50: {
id: 'weapon_mana_cap_50',
name: 'Mana Vessel',
description: '+50 weapon mana capacity (for weapon enchantments)',
category: 'mana',
baseCapacityCost: 50,
maxStacks: 3,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaMax', value: 50 }
},
weapon_mana_cap_100: {
id: 'weapon_mana_cap_100',
name: 'Mana Core',
description: '+100 weapon mana capacity (for weapon enchantments)',
category: 'mana',
baseCapacityCost: 80,
maxStacks: 2,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaMax', value: 100 }
},
weapon_mana_regen_1: {
id: 'weapon_mana_regen_1',
name: 'Mana Wick',
description: '+1 weapon mana regeneration per hour',
category: 'mana',
baseCapacityCost: 20,
maxStacks: 5,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 1 }
},
weapon_mana_regen_2: {
id: 'weapon_mana_regen_2',
name: 'Mana Siphon',
description: '+2 weapon mana regeneration per hour',
category: 'mana',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 2 }
},
weapon_mana_regen_5: {
id: 'weapon_mana_regen_5',
name: 'Mana Well',
description: '+5 weapon mana regeneration per hour',
category: 'mana',
baseCapacityCost: 60,
maxStacks: 2,
allowedEquipmentCategories: WEAPON_EQUIPMENT,
effect: { type: 'bonus', stat: 'weaponManaRegen', value: 5 }
},
};
@@ -0,0 +1,47 @@
// ─── Special Enchantment Effects ─────────────────────────────────────────
// All special-related enchantment effects that can be applied to equipment
import type { EquipmentCategory } from '../equipment'
import type { EnchantmentEffectDef } from '../enchantment-types'
// Helper to define allowed equipment categories for each effect type
const ALL_CASTER: EquipmentCategory[] = ['caster']
const CASTER_AND_HANDS: EquipmentCategory[] = ['caster', 'hands']
const CASTER_CATALYST_ACCESSORY: EquipmentCategory[] = ['caster', 'catalyst', 'accessory']
export const SPECIAL_EFFECTS: Record<string, EnchantmentEffectDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// SPECIAL EFFECTS - Unique and powerful effects
// ═══════════════════════════════════════════════════════════════════════════
spell_echo_10: {
id: 'spell_echo_10',
name: 'Echo Chamber',
description: '10% chance to cast a spell twice',
category: 'special',
baseCapacityCost: 60,
maxStacks: 2,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'special', specialId: 'spellEcho10' }
},
guardian_dmg_10: {
id: 'guardian_dmg_10',
name: 'Bane',
description: '+10% damage to guardians',
category: 'special',
baseCapacityCost: 35,
maxStacks: 3,
allowedEquipmentCategories: CASTER_CATALYST_ACCESSORY,
effect: { type: 'multiplier', stat: 'guardianDamage', value: 1.10 }
},
overpower_80: {
id: 'overpower_80',
name: 'Overpower',
description: '+50% damage when mana above 80%',
category: 'special',
baseCapacityCost: 55,
maxStacks: 1,
allowedEquipmentCategories: CASTER_AND_HANDS,
effect: { type: 'special', specialId: 'overpower' }
},
};
@@ -0,0 +1,473 @@
// ─── Spell Enchantment Effects ────────────────────────────────────────────────
// All spell-related enchantment effects that can be applied to equipment
import type { EquipmentCategory } from '../equipment'
import type { EnchantmentEffectDef } from '../enchantment-types'
// Helper to define allowed equipment categories for each effect type
const ALL_CASTER: EquipmentCategory[] = ['caster']
export const SPELL_EFFECTS: Record<string, EnchantmentEffectDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// SPELL EFFECTS - Only for CASTER equipment (staves, wands, rods, orbs)
// ═══════════════════════════════════════════════════════════════════════════
// Tier 0 - Basic Spells
spell_manaBolt: {
id: 'spell_manaBolt',
name: 'Mana Bolt',
description: 'Grants the ability to cast Mana Bolt (5 base damage, raw mana cost)',
category: 'spell',
baseCapacityCost: 50,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'manaBolt' }
},
spell_manaStrike: {
id: 'spell_manaStrike',
name: 'Mana Strike',
description: 'Grants the ability to cast Mana Strike (8 base damage, raw mana cost)',
category: 'spell',
baseCapacityCost: 40,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'manaStrike' }
},
// Tier 1 - Basic Elemental Spells
spell_fireball: {
id: 'spell_fireball',
name: 'Fireball',
description: 'Grants the ability to cast Fireball (15 fire damage)',
category: 'spell',
baseCapacityCost: 80,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'fireball' }
},
spell_emberShot: {
id: 'spell_emberShot',
name: 'Ember Shot',
description: 'Grants the ability to cast Ember Shot (10 fire damage, fast cast)',
category: 'spell',
baseCapacityCost: 60,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'emberShot' }
},
spell_waterJet: {
id: 'spell_waterJet',
name: 'Water Jet',
description: 'Grants the ability to cast Water Jet (12 water damage)',
category: 'spell',
baseCapacityCost: 70,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'waterJet' }
},
spell_iceShard: {
id: 'spell_iceShard',
name: 'Ice Shard',
description: 'Grants the ability to cast Ice Shard (14 water damage)',
category: 'spell',
baseCapacityCost: 75,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'iceShard' }
},
spell_gust: {
id: 'spell_gust',
name: 'Gust',
description: 'Grants the ability to cast Gust (10 air damage, fast cast)',
category: 'spell',
baseCapacityCost: 60,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'gust' }
},
spell_stoneBullet: {
id: 'spell_stoneBullet',
name: 'Stone Bullet',
description: 'Grants the ability to cast Stone Bullet (16 earth damage)',
category: 'spell',
baseCapacityCost: 80,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'stoneBullet' }
},
spell_lightLance: {
id: 'spell_lightLance',
name: 'Light Lance',
description: 'Grants the ability to cast Light Lance (18 light damage)',
category: 'spell',
baseCapacityCost: 95,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'lightLance' }
},
spell_shadowBolt: {
id: 'spell_shadowBolt',
name: 'Shadow Bolt',
description: 'Grants the ability to cast Shadow Bolt (16 dark damage)',
category: 'spell',
baseCapacityCost: 95,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'shadowBolt' }
},
spell_drain: {
id: 'spell_drain',
name: 'Drain',
description: 'Grants the ability to cast Drain (10 death damage)',
category: 'spell',
baseCapacityCost: 85,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'drain' }
},
// Tier 2 - Advanced Spells
spell_inferno: {
id: 'spell_inferno',
name: 'Inferno',
description: 'Grants the ability to cast Inferno (60 fire damage)',
category: 'spell',
baseCapacityCost: 180,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'inferno' }
},
spell_tidalWave: {
id: 'spell_tidalWave',
name: 'Tidal Wave',
description: 'Grants the ability to cast Tidal Wave (55 water damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'tidalWave' }
},
spell_hurricane: {
id: 'spell_hurricane',
name: 'Hurricane',
description: 'Grants the ability to cast Hurricane (50 air damage)',
category: 'spell',
baseCapacityCost: 170,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'hurricane' }
},
spell_earthquake: {
id: 'spell_earthquake',
name: 'Earthquake',
description: 'Grants the ability to cast Earthquake (70 earth damage)',
category: 'spell',
baseCapacityCost: 200,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'earthquake' }
},
spell_solarFlare: {
id: 'spell_solarFlare',
name: 'Solar Flare',
description: 'Grants the ability to cast Solar Flare (65 light damage)',
category: 'spell',
baseCapacityCost: 190,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'solarFlare' }
},
spell_voidRift: {
id: 'spell_voidRift',
name: 'Void Rift',
description: 'Grants the ability to cast Void Rift (55 dark damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'voidRift' }
},
// Additional Tier 1 Spells
spell_windSlash: {
id: 'spell_windSlash',
name: 'Wind Slash',
description: 'Grants the ability to cast Wind Slash (12 air damage)',
category: 'spell',
baseCapacityCost: 72,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'windSlash' }
},
spell_rockSpike: {
id: 'spell_rockSpike',
name: 'Rock Spike',
description: 'Grants the ability to cast Rock Spike (18 earth damage)',
category: 'spell',
baseCapacityCost: 88,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'rockSpike' }
},
spell_radiance: {
id: 'spell_radiance',
name: 'Radiance',
description: 'Grants the ability to cast Radiance (14 light damage)',
category: 'spell',
baseCapacityCost: 80,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'radiance' }
},
spell_darkPulse: {
id: 'spell_darkPulse',
name: 'Dark Pulse',
description: 'Grants the ability to cast Dark Pulse (12 dark damage)',
category: 'spell',
baseCapacityCost: 68,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'darkPulse' }
},
// Additional Tier 2 Spells
spell_flameWave: {
id: 'spell_flameWave',
name: 'Flame Wave',
description: 'Grants the ability to cast Flame Wave (45 fire damage)',
category: 'spell',
baseCapacityCost: 165,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'flameWave' }
},
spell_iceStorm: {
id: 'spell_iceStorm',
name: 'Ice Storm',
description: 'Grants the ability to cast Ice Storm (50 water damage)',
category: 'spell',
baseCapacityCost: 170,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'iceStorm' }
},
spell_windBlade: {
id: 'spell_windBlade',
name: 'Wind Blade',
description: 'Grants the ability to cast Wind Blade (40 air damage)',
category: 'spell',
baseCapacityCost: 155,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'windBlade' }
},
spell_stoneBarrage: {
id: 'spell_stoneBarrage',
name: 'Stone Barrage',
description: 'Grants the ability to cast Stone Barrage (55 earth damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'stoneBarrage' }
},
spell_divineSmite: {
id: 'spell_divineSmite',
name: 'Divine Smite',
description: 'Grants the ability to cast Divine Smite (55 light damage)',
category: 'spell',
baseCapacityCost: 175,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'divineSmite' }
},
spell_shadowStorm: {
id: 'spell_shadowStorm',
name: 'Shadow Storm',
description: 'Grants the ability to cast Shadow Storm (48 dark damage)',
category: 'spell',
baseCapacityCost: 168,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'shadowStorm' }
},
// Tier 3 - Master Spells
spell_pyroclasm: {
id: 'spell_pyroclasm',
name: 'Pyroclasm',
description: 'Grants the ability to cast Pyroclasm (250 fire damage)',
category: 'spell',
baseCapacityCost: 400,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'pyroclasm' }
},
spell_tsunami: {
id: 'spell_tsunami',
name: 'Tsunami',
description: 'Grants the ability to cast Tsunami (220 water damage)',
category: 'spell',
baseCapacityCost: 380,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'tsunami' }
},
spell_meteorStrike: {
id: 'spell_meteorStrike',
name: 'Meteor Strike',
description: 'Grants the ability to cast Meteor Strike (280 earth damage)',
category: 'spell',
baseCapacityCost: 420,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'meteorStrike' }
},
// ═══════════════════════════════════════════════════════════════════════════
// LIGHTNING SPELL EFFECTS - Fast, armor-piercing, harder to dodge
// ═══════════════════════════════════════════════════════════════════════════
spell_spark: {
id: 'spell_spark',
name: 'Spark',
description: 'Grants the ability to cast Spark (8 lightning damage, very fast, armor pierce)',
category: 'spell',
baseCapacityCost: 70,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'spark' }
},
spell_lightningBolt: {
id: 'spell_lightningBolt',
name: 'Lightning Bolt',
description: 'Grants the ability to cast Lightning Bolt (14 lightning damage, armor pierce)',
category: 'spell',
baseCapacityCost: 90,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'lightningBolt' }
},
spell_chainLightning: {
id: 'spell_chainLightning',
name: 'Chain Lightning',
description: 'Grants the ability to cast Chain Lightning (25 lightning damage, hits 3 targets)',
category: 'spell',
baseCapacityCost: 160,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'chainLightning' }
},
spell_stormCall: {
id: 'spell_stormCall',
name: 'Storm Call',
description: 'Grants the ability to cast Storm Call (40 lightning damage, hits 2 targets)',
category: 'spell',
baseCapacityCost: 190,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'stormCall' }
},
spell_thunderStrike: {
id: 'spell_thunderStrike',
name: 'Thunder Strike',
description: 'Grants the ability to cast Thunder Strike (150 lightning damage, 50% armor pierce)',
category: 'spell',
baseCapacityCost: 350,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'thunderStrike' }
},
// ═══════════════════════════════════════════════════════════════════════════
// METAL SPELL EFFECTS - Fire + Earth compound, armor pierce focus
// ═══════════════════════════════════════════════════════════════════════════
spell_metalShard: {
id: 'spell_metalShard',
name: 'Metal Shard',
description: 'Grants the ability to cast Metal Shard (16 metal damage, 25% armor pierce)',
category: 'spell',
baseCapacityCost: 85,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'metalShard' }
},
spell_ironFist: {
id: 'spell_ironFist',
name: 'Iron Fist',
description: 'Grants the ability to cast Iron Fist (28 metal damage, 35% armor pierce)',
category: 'spell',
baseCapacityCost: 120,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'ironFist' }
},
spell_steelTempest: {
id: 'spell_steelTempest',
name: 'Steel Tempest',
description: 'Grants the ability to cast Steel Tempest (55 metal damage, 45% armor pierce)',
category: 'spell',
baseCapacityCost: 190,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'steelTempest' }
},
spell_furnaceBlast: {
id: 'spell_furnaceBlast',
name: 'Furnace Blast',
description: 'Grants the ability to cast Furnace Blast (200 metal damage, 60% armor pierce)',
category: 'spell',
baseCapacityCost: 400,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'furnaceBlast' }
},
// ═══════════════════════════════════════════════════════════════════════════
// SAND SPELL EFFECTS - Earth + Water compound, AOE focus
// ═══════════════════════════════════════════════════════════════════════════
spell_sandBlast: {
id: 'spell_sandBlast',
name: 'Sand Blast',
description: 'Grants the ability to cast Sand Blast (11 sand damage, very fast)',
category: 'spell',
baseCapacityCost: 72,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'sandBlast' }
},
spell_sandstorm: {
id: 'spell_sandstorm',
name: 'Sandstorm',
description: 'Grants the ability to cast Sandstorm (22 sand damage, hits 2 enemies)',
category: 'spell',
baseCapacityCost: 100,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'sandstorm' }
},
spell_desertWind: {
id: 'spell_desertWind',
name: 'Desert Wind',
description: 'Grants the ability to cast Desert Wind (38 sand damage, hits 3 enemies)',
category: 'spell',
baseCapacityCost: 155,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'desertWind' }
},
spell_duneCollapse: {
id: 'spell_duneCollapse',
name: 'Dune Collapse',
description: 'Grants the ability to cast Dune Collapse (100 sand damage, hits 5 enemies)',
category: 'spell',
baseCapacityCost: 300,
maxStacks: 1,
allowedEquipmentCategories: ALL_CASTER,
effect: { type: 'spell', spellId: 'duneCollapse' }
},
};
@@ -0,0 +1,45 @@
// ─── Utility Enchantment Effects ─────────────────────────────────────────
// All utility-related enchantment effects that can be applied to equipment
import type { EquipmentCategory } from '../equipment'
import type { EnchantmentEffectDef } from '../enchantment-types'
// Helper to define allowed equipment categories for each effect type
const UTILITY_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'hands', 'feet', 'accessory']
export const UTILITY_EFFECTS: Record<string, EnchantmentEffectDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// UTILITY EFFECTS - Study speed, insight, meditation
// ═══════════════════════════════════════════════════════════════════════════
meditate_10: {
id: 'meditate_10',
name: 'Meditative Focus',
description: '+10% meditation efficiency',
category: 'utility',
baseCapacityCost: 18,
maxStacks: 5,
allowedEquipmentCategories: ['head', 'body', 'accessory'],
effect: { type: 'multiplier', stat: 'meditationEfficiency', value: 1.10 }
},
study_10: {
id: 'study_10',
name: 'Quick Study',
description: '+10% study speed',
category: 'utility',
baseCapacityCost: 22,
maxStacks: 4,
allowedEquipmentCategories: UTILITY_EQUIPMENT,
effect: { type: 'multiplier', stat: 'studySpeed', value: 1.10 }
},
insight_5: {
id: 'insight_5',
name: 'Insightful',
description: '+5% insight gain',
category: 'utility',
baseCapacityCost: 25,
maxStacks: 4,
allowedEquipmentCategories: ['head', 'accessory'],
effect: { type: 'multiplier', stat: 'insightGain', value: 1.05 }
},
};
+13 -515
View File
@@ -1,516 +1,14 @@
// ─── Game Types ─────────────────────────────────────────────────────────────── // ─── Game Types ───────────────────────────────────────────────────────────────
// This file now re-exports types from domain-specific files in the types/ directory.
export type ElementCategory = 'base' | 'utility' | 'composite' | 'exotic'; // All type definitions have been moved to:
// - types/elements.ts - element-related types
// Attunement body slots // - types/attunements.ts - attunement-related types
export type AttunementSlot = 'rightHand' | 'leftHand' | 'head' | 'back' | 'chest' | 'leftLeg' | 'rightLeg'; // - types/spells.ts - spell-related types
// - types/skills.ts - skill-related types
// Attunement definition // - types/equipment.ts - equipment-related types
export interface AttunementDef { // - types/game.ts - core game state types
id: string; //
name: string; // Import from this file (types.ts) to maintain backward compatibility,
desc: string; // or import directly from the domain-specific files.
slot: AttunementSlot;
icon: string; export * from './types/index';
color: string;
primaryManaType?: string; // Primary mana type this attunement generates (null for Invoker)
rawManaRegen: number; // Raw mana regeneration per hour granted by this attunement
conversionRate: number; // Raw mana converted to primary type per hour
unlocked: boolean; // Whether this is unlocked by default
unlockCondition?: string; // Description of how to unlock (for future challenges)
capabilities: string[]; // What this attunement enables (e.g., 'enchanting', 'pacts', 'golemCrafting')
skillCategories: string[]; // Skill categories this attunement provides access to
}
// Attunement instance state (tracks player's attunements)
export interface AttunementState {
id: string;
active: boolean; // Whether this attunement is currently active
level: number; // Attunement level (for future progression)
experience: number; // Progress toward next level
}
export interface ElementDef {
name: string;
sym: string;
color: string;
glow: string;
cat: ElementCategory;
recipe?: string[];
}
export interface ElementState {
current: number;
max: number;
unlocked: boolean;
}
// Boon types that guardians can grant
export interface GuardianBoon {
type: 'maxMana' | 'manaRegen' | 'castingSpeed' | 'elementalDamage' | 'rawDamage' |
'critChance' | 'critDamage' | 'spellEfficiency' | 'manaGain' | 'insightGain' |
'studySpeed' | 'prestigeInsight';
value: number;
desc: string;
}
export interface GuardianDef {
name: string;
element: string;
hp: number;
pact: number; // Pact multiplier when signed
color: string;
boons: GuardianBoon[]; // Bonuses granted when pact is signed
pactCost: number; // Mana cost to perform pact ritual
pactTime: number; // Hours required for pact ritual
uniquePerk: string; // Description of unique perk
armor?: number; // Damage reduction (0-1, e.g., 0.2 = 20% reduction)
}
// Spell cost can be raw mana or elemental mana
export interface SpellCost {
type: 'raw' | 'element'; // 'raw' for raw mana, 'element' for specific elemental mana
element?: string; // Required if type is 'element'
amount: number; // Amount of mana required
}
export interface SpellDef {
name: string;
elem: string; // Element type for damage calculations
dmg: number;
cost: SpellCost; // Changed from number to SpellCost object
tier: number;
unlock: number; // Mana cost to start studying
studyTime?: number; // Hours needed to study (optional, defaults based on tier)
castSpeed?: number; // Casts per hour (default 1, higher = faster)
desc?: string; // Optional spell description
effects?: SpellEffect[]; // Optional special effects
isAoe?: boolean; // AOE spell that hits multiple enemies
aoeTargets?: number; // Number of enemies hit by AOE
isWeaponEnchant?: boolean; // Can be used as weapon enchantment (magic swords)
}
export interface SpellEffect {
type: 'burn' | 'freeze' | 'stun' | 'pierce' | 'multicast' | 'shield' | 'buff' | 'chain' | 'aoe' | 'armor_pierce';
value: number; // Effect potency
duration?: number; // Duration in hours for timed effects
targets?: number; // For AOE: number of targets
chance?: number; // For chance-based effects (e.g., stun chance)
}
export interface SpellState {
learned: boolean;
level: number;
studyProgress?: number; // Hours studied so far (for in-progress spells)
}
// ─── Room and Enemy Types ─────────────────────────────────────────────────────
export type RoomType = 'combat' | 'puzzle' | 'swarm' | 'speed' | 'guardian';
export interface EnemyState {
id: string;
hp: number;
maxHP: number;
armor: number; // Damage reduction (0-1)
dodgeChance: number; // For speed rooms (0-1)
element: string;
}
export interface FloorState {
roomType: RoomType;
enemies: EnemyState[]; // For swarm rooms, multiple enemies
puzzleProgress?: number; // For puzzle rooms (0-1)
puzzleRequired?: number; // Total progress needed
puzzleId?: string; // Which puzzle type
puzzleAttunements?: string[]; // Which attunements speed up this puzzle
}
export interface SkillDef {
name: string;
desc: string;
cat: string;
attunement?: string; // Which attunement this skill belongs to (null = core)
max: number;
base: number; // Mana cost to start studying
req?: Record<string, number>; // Skill prerequisites
attunementReq?: Record<string, number>; // Attunement level requirements (attunement id -> min level)
studyTime: number; // Hours needed to study
level?: number; // Current level (optional, for UI display)
tier?: number; // Skill tier (1-5)
tierUp?: string; // Skill ID this evolves into at max level
baseSkill?: string; // Original skill ID this evolved from
tierMultiplier?: number; // Multiplier for each tier (default 2)
}
// Skill upgrade choices at milestones (level 5 and level 10)
export interface SkillUpgradeDef {
id: string;
name: string;
desc: string;
skillId: string; // Which skill this upgrade belongs to
milestone: 5 | 10; // Level at which this upgrade is available
effect: SkillUpgradeEffect;
}
export interface SkillUpgradeEffect {
type: 'multiplier' | 'bonus' | 'special';
stat?: string; // Stat to modify
value?: number; // Multiplier or bonus value
specialId?: string; // Special effect identifier
specialDesc?: string; // Description of special effect
}
// Skill evolution system - 5-Tier Continuous Talent Tree
// Each skill with max level 10 follows this structure:
// - 5 Tiers (T1-T5)
// - Each tier has L5 and L10 milestone perk choices
// - 3 paths per tier (A, B, C columns) representing different playstyles
// - T3 L10 and T5 L10 have Elite Perks (game-changing mechanics)
export interface SkillEvolutionPath {
baseSkillId: string; // Starting skill ID
tiers: SkillTierDef[]; // 5 tiers of evolution
}
export interface SkillTierDef {
tier: number; // Tier number (1-5)
skillId: string; // Skill ID for this tier
name: string;
multiplier: number; // Base effect multiplier for this tier
// Perk choices organized by milestone and path (A, B, C)
l5Perks: SkillPerkChoice[]; // 3 choices (one per path) at level 5
l10Perks: SkillPerkChoice[]; // 3 choices (one per path) at level 10
}
// Perk choice at a milestone - belongs to a specific path (A, B, or C)
export interface SkillPerkChoice {
id: string;
name: string;
desc: string;
path: 'A' | 'B' | 'C'; // Which path this perk belongs to
isElite?: boolean; // True for T3 L10 and T5 L10 perks
effect: SkillUpgradeEffect;
// For path compounding - if player stays on same path, bonuses compound
pathCompoundBonus?: number; // Exponential bonus for staying on same path
}
export interface SkillUpgradeChoice {
id: string;
name: string;
desc: string;
milestone: 5 | 10; // Level at which this upgrade is available
effect: SkillUpgradeEffect;
}
export interface PrestigeDef {
name: string;
desc: string;
max: number;
cost: number;
}
// Legacy EquipmentDef for backward compatibility
export interface EquipmentDef {
id: string;
name: string;
slot: 'mainHand' | 'offHand' | 'head' | 'body' | 'hands' | 'accessory';
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' | 'mythic';
stats: Record<string, number>;
durability: number;
maxDurability: number;
element?: string;
}
// Equipment Instance (actual equipped item with enchantments)
export interface EquipmentInstance {
instanceId: string; // Unique ID for this specific item
typeId: string; // Reference to EquipmentType (e.g., 'basicStaff')
name: string; // Display name (defaults to type name)
enchantments: AppliedEnchantment[];
usedCapacity: number; // Currently used capacity
totalCapacity: number; // Base capacity + bonuses
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' | 'mythic';
quality: number; // 0-100, affects capacity efficiency
weaponMana?: number; // Current mana stored in weapon (for weapon enchantments)
weaponManaMax?: number; // Max mana the weapon can store
weaponManaRegen?: number; // Mana regen per hour for weapon
weaponManaType?: string; // Type of mana the weapon stores
activeWeaponEnchant?: string; // Active weapon enchantment (for magic swords)
}
export interface AppliedEnchantment {
effectId: string; // Reference to EnchantmentEffectDef
stacks: number; // Number of times this effect is applied
actualCost: number; // Actual capacity cost (after efficiency)
}
// Enchantment Design (saved design for later application)
export interface EnchantmentDesign {
id: string;
name: string;
equipmentType: string; // Which equipment type this is designed for
effects: DesignEffect[];
totalCapacityUsed: number;
designTime: number; // Hours required to design
created: number; // Timestamp
}
export interface DesignEffect {
effectId: string;
stacks: number;
capacityCost: number;
}
// Crafting Progress States
export interface DesignProgress {
designId: string;
progress: number; // Hours spent designing
required: number; // Total hours needed
// Design data stored during progress
name: string;
equipmentType: string;
effects: DesignEffect[];
}
export interface PreparationProgress {
equipmentInstanceId: string;
progress: number; // Hours spent preparing
required: number; // Total hours needed
manaCostPaid: number; // Mana cost already paid
}
export interface ApplicationProgress {
equipmentInstanceId: string;
designId: string;
progress: number; // Hours spent applying
required: number; // Total hours needed
manaPerHour: number; // Mana cost per hour
paused: boolean;
manaSpent: number; // Total mana spent so far
}
// Equipment crafting progress (from blueprints)
export interface EquipmentCraftingProgress {
blueprintId: string;
equipmentTypeId: string;
progress: number; // Hours spent crafting
required: number; // Total hours needed
manaSpent: number; // Total mana spent so far
}
// Equipment spell state (for multi-spell casting)
export interface EquipmentSpellState {
spellId: string;
sourceEquipment: string; // Equipment instance ID
castProgress: number; // 0-1 progress toward next cast
}
export interface BlueprintDef {
id: string;
name: string;
tier: number;
slot: string;
stats: Record<string, number>;
studyTime: number;
craftTime: number;
craftCost: number;
discovered: boolean;
learned: boolean;
}
// Loot inventory for materials and blueprints
export interface LootInventory {
materials: Record<string, number>; // materialId -> count
blueprints: string[]; // blueprint IDs discovered
}
// Achievement definitions
export interface AchievementDef {
id: string;
name: string;
desc: string;
category: string;
requirement: {
type: string;
value: number;
subType?: string;
};
reward: {
insight?: number;
manaBonus?: number;
damageBonus?: number;
regenBonus?: number;
title?: string;
unlockEffect?: string;
};
hidden?: boolean;
}
// Achievement state tracks unlocked achievements and progress
export interface AchievementState {
unlocked: string[]; // IDs of unlocked achievements
progress: Record<string, number>; // Progress toward achievement requirements
}
export type GameAction = 'meditate' | 'climb' | 'study' | 'craft' | 'repair' | 'convert' | 'design' | 'prepare' | 'enchant';
export interface ScheduleBlock {
id: string;
action: GameAction;
startHour: number;
endHour: number;
enabled: boolean;
target?: string; // spell id, blueprint id, skill id, element id
}
export interface StudyTarget {
type: 'skill' | 'spell' | 'blueprint';
id: string;
progress: number; // Hours studied
required: number; // Total hours needed
}
// ─── Golemancy Types ───────────────────────────────────────────────────────────
export interface SummonedGolem {
golemId: string; // Reference to GOLEMS_DEF
summonedFloor: number; // Floor when golem was summoned
attackProgress: number; // Progress toward next attack (0-1)
}
export interface GolemancyState {
enabledGolems: string[]; // Golem IDs the player wants active
summonedGolems: SummonedGolem[]; // Currently summoned golems on this floor
lastSummonFloor: number; // Floor golems were last summoned on
}
export interface GameState {
// Time
day: number;
hour: number;
loopCount: number;
gameOver: boolean;
victory: boolean;
paused: boolean;
// Raw Mana
rawMana: number;
meditateTicks: number;
totalManaGathered: number;
// Attunements (class-like system)
attunements: Record<string, AttunementState>; // attunement id -> state
// Elements
elements: Record<string, ElementState>;
// Spire
currentFloor: number;
floorHP: number;
floorMaxHP: number;
maxFloorReached: number;
signedPacts: number[];
activeSpell: string;
currentAction: GameAction;
castProgress: number; // Progress towards next spell cast (0-1)
// Room system for special floors
currentRoom: FloorState; // Current room state (swarm, puzzle, speed, etc.)
// Spells
spells: Record<string, SpellState>;
// Skills
skills: Record<string, number>;
skillProgress: Record<string, number>; // Saved study progress for skills
skillUpgrades: Record<string, string[]>; // Selected upgrade IDs per skill
skillTiers: Record<string, number>; // Current tier for each base skill
// Equipment System (new instance-based system)
equippedInstances: Record<string, string | null>; // slot -> instanceId
equipmentInstances: Record<string, EquipmentInstance>; // instanceId -> instance
enchantmentDesigns: EnchantmentDesign[]; // Saved enchantment designs
// Crafting Progress
designProgress: DesignProgress | null;
preparationProgress: PreparationProgress | null;
applicationProgress: ApplicationProgress | null;
equipmentCraftingProgress: EquipmentCraftingProgress | null;
// Unlocked enchantment effects for designing
unlockedEffects: string[]; // Effect IDs that have been researched
// Equipment spell states for multi-casting
equipmentSpellStates: EquipmentSpellState[];
// Legacy Equipment (for backward compatibility)
equipment: Record<string, EquipmentDef | null>;
inventory: EquipmentDef[];
// Blueprints
blueprints: Record<string, BlueprintDef>;
// Loot Inventory
lootInventory: LootInventory;
// Schedule
schedule: ScheduleBlock[];
autoSchedule: boolean;
studyQueue: string[];
craftQueue: string[];
// Current Study Target
currentStudyTarget: StudyTarget | null;
// Parallel Study Target (for Parallel Mind milestone upgrade)
parallelStudyTarget: StudyTarget | null;
// Golemancy (summoned golems)
golemancy: GolemancyState;
// Achievements
achievements: AchievementState;
// Stats tracking
totalSpellsCast: number;
totalDamageDealt: number;
totalCraftsCompleted: number;
// Prestige
insight: number;
totalInsight: number;
prestigeUpgrades: Record<string, number>;
memorySlots: number;
memories: string[];
// Incursion
incursionStrength: number;
containmentWards: number;
// Log
log: string[];
// Loop insight (earned at end of current loop)
loopInsight: number;
}
// Action types for the store
export type GameActionType =
| { type: 'TICK' }
| { type: 'GATHER_MANA' }
| { type: 'SET_ACTION'; action: GameAction }
| { type: 'SET_SPELL'; spellId: string }
| { type: 'LEARN_SPELL'; spellId: string }
| { type: 'START_STUDYING_SKILL'; skillId: string }
| { type: 'START_STUDYING_SPELL'; spellId: string }
| { type: 'CANCEL_STUDY' }
| { type: 'CONVERT_MANA'; element: string; amount: number }
| { type: 'UNLOCK_ELEMENT'; element: string }
| { type: 'CRAFT_COMPOSITE'; target: string }
| { type: 'DO_PRESTIGE'; id: string }
| { type: 'START_NEW_LOOP' }
| { type: 'TOGGLE_PAUSE' }
| { type: 'RESET_GAME' }
| { type: 'SELECT_SKILL_UPGRADE'; skillId: string; upgradeId: string }
| { type: 'TIER_UP_SKILL'; skillId: string };
+51
View File
@@ -0,0 +1,51 @@
// ─── Attunement Types ───────────────────────────────────────────────────────
// Attunement body slots
export type AttunementSlot = 'rightHand' | 'leftHand' | 'head' | 'back' | 'chest' | 'leftLeg' | 'rightLeg';
// Attunement definition
export interface AttunementDef {
id: string;
name: string;
desc: string;
slot: AttunementSlot;
icon: string;
color: string;
primaryManaType?: string; // Primary mana type this attunement generates (null for Invoker)
rawManaRegen: number; // Raw mana regeneration per hour granted by this attunement
conversionRate: number; // Raw mana converted to primary type per hour
unlocked: boolean; // Whether this is unlocked by default
unlockCondition?: string; // Description of how to unlock (for future challenges)
capabilities: string[]; // What this attunement enables (e.g., 'enchanting', 'pacts', 'golemCrafting')
skillCategories: string[]; // Skill categories this attunement provides access to
}
// Attunement instance state (tracks player's attunements)
export interface AttunementState {
id: string;
active: boolean; // Whether this attunement is currently active
level: number; // Attunement level (for future progression)
experience: number; // Progress toward next level
}
// Boon types that guardians can grant
export interface GuardianBoon {
type: 'maxMana' | 'manaRegen' | 'castingSpeed' | 'elementalDamage' | 'rawDamage' |
'critChance' | 'critDamage' | 'spellEfficiency' | 'manaGain' | 'insightGain' |
'studySpeed' | 'prestigeInsight';
value: number;
desc: string;
}
export interface GuardianDef {
name: string;
element: string;
hp: number;
pact: number; // Pact multiplier when signed
color: string;
boons: GuardianBoon[]; // Bonuses granted when pact is signed
pactCost: number; // Mana cost to perform pact ritual
pactTime: number; // Hours required for pact ritual
uniquePerk: string; // Description of unique perk
armor?: number; // Damage reduction (0-1, e.g., 0.2 = 20% reduction)
}
+18
View File
@@ -0,0 +1,18 @@
// ─── Element Types ───────────────────────────────────────────────────────────
export type ElementCategory = 'base' | 'utility' | 'composite' | 'exotic';
export interface ElementDef {
name: string;
sym: string;
color: string;
glow: string;
cat: ElementCategory;
recipe?: string[];
}
export interface ElementState {
current: number;
max: number;
unlocked: boolean;
}
+116
View File
@@ -0,0 +1,116 @@
// ─── Equipment Types ───────────────────────────────────────────────────────
// Legacy EquipmentDef for backward compatibility
export interface EquipmentDef {
id: string;
name: string;
slot: 'mainHand' | 'offHand' | 'head' | 'body' | 'hands' | 'accessory';
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' | 'mythic';
stats: Record<string, number>;
durability: number;
maxDurability: number;
element?: string;
}
// Equipment Instance (actual equipped item with enchantments)
export interface EquipmentInstance {
instanceId: string; // Unique ID for this specific item
typeId: string; // Reference to EquipmentType (e.g., 'basicStaff')
name: string; // Display name (defaults to type name)
enchantments: AppliedEnchantment[];
usedCapacity: number; // Currently used capacity
totalCapacity: number; // Base capacity + bonuses
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' | 'mythic';
quality: number; // 0-100, affects capacity efficiency
weaponMana?: number; // Current mana stored in weapon (for weapon enchantments)
weaponManaMax?: number; // Max mana the weapon can store
weaponManaRegen?: number; // Mana regen per hour for weapon
weaponManaType?: string; // Type of mana the weapon stores
activeWeaponEnchant?: string; // Active weapon enchantment (for magic swords)
}
export interface AppliedEnchantment {
effectId: string; // Reference to EnchantmentEffectDef
stacks: number; // Number of times this effect is applied
actualCost: number; // Actual capacity cost (after efficiency)
}
// Enchantment Design (saved design for later application)
export interface EnchantmentDesign {
id: string;
name: string;
equipmentType: string; // Which equipment type this is designed for
effects: DesignEffect[];
totalCapacityUsed: number;
designTime: number; // Hours required to design
created: number; // Timestamp
}
export interface DesignEffect {
effectId: string;
stacks: number;
capacityCost: number;
}
// Crafting Progress States
export interface DesignProgress {
designId: string;
progress: number; // Hours spent designing
required: number; // Total hours needed
// Design data stored during progress
name: string;
equipmentType: string;
effects: DesignEffect[];
}
export interface PreparationProgress {
equipmentInstanceId: string;
progress: number; // Hours spent preparing
required: number; // Total hours needed
manaCostPaid: number; // Mana cost already paid
}
export interface ApplicationProgress {
equipmentInstanceId: string;
designId: string;
progress: number; // Hours spent applying
required: number; // Total hours needed
manaPerHour: number; // Mana cost per hour
paused: boolean;
manaSpent: number; // Total mana spent so far
}
// Equipment crafting progress (from blueprints)
export interface EquipmentCraftingProgress {
blueprintId: string;
equipmentTypeId: string;
progress: number; // Hours spent crafting
required: number; // Total hours needed
manaSpent: number; // Total mana spent so far
}
// Equipment spell state (for multi-spell casting)
export interface EquipmentSpellState {
spellId: string;
sourceEquipment: string; // Equipment instance ID
castProgress: number; // 0-1 progress toward next cast
}
export interface BlueprintDef {
id: string;
name: string;
tier: number;
slot: string;
stats: Record<string, number>;
studyTime: number;
craftTime: number;
craftCost: number;
discovered: boolean;
learned: boolean;
}
// Loot inventory for materials and blueprints
export interface LootInventory {
materials: Record<string, number>; // materialId -> count
blueprints: string[]; // blueprint IDs discovered
}
+224
View File
@@ -0,0 +1,224 @@
// ─── Core Game State Types ───────────────────────────────────────────────
import type { AttunementState } from './attunements';
import type { ElementState } from './elements';
import type { SpellState } from './spells';
import type { EquipmentInstance, EnchantmentDesign, DesignProgress, PreparationProgress, ApplicationProgress, EquipmentCraftingProgress, EquipmentDef, BlueprintDef, LootInventory, EquipmentSpellState } from './equipment';
// ─── Room and Enemy Types ─────────────────────────────────────────────────────
export type RoomType = 'combat' | 'puzzle' | 'swarm' | 'speed' | 'guardian';
export interface EnemyState {
id: string;
hp: number;
maxHP: number;
armor: number; // Damage reduction (0-1)
dodgeChance: number; // For speed rooms (0-1)
element: string;
}
export interface FloorState {
roomType: RoomType;
enemies: EnemyState[]; // For swarm rooms, multiple enemies
puzzleProgress?: number; // For puzzle rooms (0-1)
puzzleRequired?: number; // Total progress needed
puzzleId?: string; // Which puzzle type
puzzleAttunements?: string[]; // Which attunements speed up this puzzle
}
// ─── Achievement Types ─────────────────────────────────────────────────────
export interface AchievementDef {
id: string;
name: string;
desc: string;
category: string;
requirement: {
type: string;
value: number;
subType?: string;
};
reward: {
insight?: number;
manaBonus?: number;
damageBonus?: number;
regenBonus?: number;
title?: string;
unlockEffect?: string;
};
hidden?: boolean;
}
// Achievement state tracks unlocked achievements and progress
export interface AchievementState {
unlocked: string[]; // IDs of unlocked achievements
progress: Record<string, number>; // Progress toward achievement requirements
}
// ─── Game Actions and Scheduling ─────────────────────────────────────────
export type GameAction = 'meditate' | 'climb' | 'study' | 'craft' | 'repair' | 'convert' | 'design' | 'prepare' | 'enchant';
export interface ScheduleBlock {
id: string;
action: GameAction;
startHour: number;
endHour: number;
enabled: boolean;
target?: string; // spell id, blueprint id, skill id, element id
}
export interface StudyTarget {
type: 'skill' | 'spell' | 'blueprint';
id: string;
progress: number; // Hours studied
required: number; // Total hours needed
}
// ─── Golemancy Types ─────────────────────────────────────────────────────────
export interface SummonedGolem {
golemId: string; // Reference to GOLEMS_DEF
summonedFloor: number; // Floor when golem was summoned
attackProgress: number; // Progress toward next attack (0-1)
}
export interface GolemancyState {
enabledGolems: string[]; // Golem IDs the player wants active
summonedGolems: SummonedGolem[]; // Currently summoned golems on this floor
lastSummonFloor: number; // Floor golems were last summoned on
}
// ─── Main Game State ─────────────────────────────────────────────────────
export interface GameState {
// Time
day: number;
hour: number;
loopCount: number;
gameOver: boolean;
victory: boolean;
paused: boolean;
// Raw Mana
rawMana: number;
meditateTicks: number;
totalManaGathered: number;
// Attunements (class-like system)
attunements: Record<string, AttunementState>; // attunement id -> state
// Elements
elements: Record<string, ElementState>;
// Spire
currentFloor: number;
floorHP: number;
floorMaxHP: number;
maxFloorReached: number;
signedPacts: number[];
activeSpell: string;
currentAction: GameAction;
castProgress: number; // Progress towards next spell cast (0-1)
// Room system for special floors
currentRoom: FloorState; // Current room state (swarm, puzzle, speed, etc.)
// Spells
spells: Record<string, SpellState>;
// Skills
skills: Record<string, number>;
skillProgress: Record<string, number>; // Saved study progress for skills
skillUpgrades: Record<string, string[]>; // Selected upgrade IDs per skill
skillTiers: Record<string, number>; // Current tier for each base skill
// Equipment System (new instance-based system)
equippedInstances: Record<string, string | null>; // slot -> instanceId
equipmentInstances: Record<string, EquipmentInstance>; // instanceId -> instance
enchantmentDesigns: EnchantmentDesign[]; // Saved enchantment designs
// Crafting Progress
designProgress: DesignProgress | null;
preparationProgress: PreparationProgress | null;
applicationProgress: ApplicationProgress | null;
equipmentCraftingProgress: EquipmentCraftingProgress | null;
// Unlocked enchantment effects for designing
unlockedEffects: string[]; // Effect IDs that have been researched
// Equipment spell states for multi-casting
equipmentSpellStates: EquipmentSpellState[];
// Legacy Equipment (for backward compatibility)
equipment: Record<string, EquipmentDef | null>;
inventory: EquipmentDef[];
// Blueprints
blueprints: Record<string, BlueprintDef>;
// Loot Inventory
lootInventory: LootInventory;
// Schedule
schedule: ScheduleBlock[];
autoSchedule: boolean;
studyQueue: string[];
craftQueue: string[];
// Current Study Target
currentStudyTarget: StudyTarget | null;
// Parallel Study Target (for Parallel Mind milestone upgrade)
parallelStudyTarget: StudyTarget | null;
// Golemancy (summoned golems)
golemancy: GolemancyState;
// Achievements
achievements: AchievementState;
// Stats tracking
totalSpellsCast: number;
totalDamageDealt: number;
totalCraftsCompleted: number;
// Prestige
insight: number;
totalInsight: number;
prestigeUpgrades: Record<string, number>;
memorySlots: number;
memories: string[];
// Incursion
incursionStrength: number;
containmentWards: number;
// Log
log: string[];
// Loop insight (earned at end of current loop)
loopInsight: number;
}
// ─── Action Types for Store ─────────────────────────────────────────────
export type GameActionType =
| { type: 'TICK' }
| { type: 'GATHER_MANA' }
| { type: 'SET_ACTION'; action: GameAction }
| { type: 'SET_SPELL'; spellId: string }
| { type: 'LEARN_SPELL'; spellId: string }
| { type: 'START_STUDYING_SKILL'; skillId: string }
| { type: 'START_STUDYING_SPELL'; spellId: string }
| { type: 'CANCEL_STUDY' }
| { type: 'CONVERT_MANA'; element: string; amount: number }
| { type: 'UNLOCK_ELEMENT'; element: string }
| { type: 'CRAFT_COMPOSITE'; target: string }
| { type: 'DO_PRESTIGE'; id: string }
| { type: 'START_NEW_LOOP' }
| { type: 'TOGGLE_PAUSE' }
| { type: 'RESET_GAME' }
| { type: 'SELECT_SKILL_UPGRADE'; skillId: string; upgradeId: string }
| { type: 'TIER_UP_SKILL'; skillId: string };
+56
View File
@@ -0,0 +1,56 @@
// ─── Game Types Index ──────────────────────────────────────────────────────
// Re-export all types from domain-specific files
// Element types
export type { ElementCategory, ElementDef, ElementState } from './elements';
// Attunement types
export type { AttunementSlot, AttunementDef, AttunementState, GuardianBoon, GuardianDef } from './attunements';
// Spell types
export type { SpellCost, SpellDef, SpellEffect, SpellState } from './spells';
// Skill types
export type {
SkillDef,
SkillUpgradeDef,
SkillUpgradeEffect,
SkillEvolutionPath,
SkillTierDef,
SkillPerkChoice,
SkillUpgradeChoice,
PrestigeDef
} from './skills';
// Equipment types
export type {
EquipmentDef,
EquipmentInstance,
AppliedEnchantment,
EnchantmentDesign,
DesignEffect,
DesignProgress,
PreparationProgress,
ApplicationProgress,
EquipmentCraftingProgress,
EquipmentSpellState,
BlueprintDef,
LootInventory
} from './equipment';
// Game state types
export type {
RoomType,
EnemyState,
FloorState,
AchievementDef,
AchievementState,
GameAction,
ScheduleBlock,
StudyTarget,
SummonedGolem,
GolemancyState,
GameState,
GameActionType
} from './game';
+85
View File
@@ -0,0 +1,85 @@
// ─── Skill Types ───────────────────────────────────────────────────────────
export interface SkillDef {
name: string;
desc: string;
cat: string;
attunement?: string; // Which attunement this skill belongs to (null = core)
max: number;
base: number; // Mana cost to start studying
req?: Record<string, number>; // Skill prerequisites
attunementReq?: Record<string, number>; // Attunement level requirements (attunement id -> min level)
studyTime: number; // Hours needed to study
level?: number; // Current level (optional, for UI display)
tier?: number; // Skill tier (1-5)
tierUp?: string; // Skill ID this evolves into at max level
baseSkill?: string; // Original skill ID this evolved from
tierMultiplier?: number; // Multiplier for each tier (default 2)
}
// Skill upgrade choices at milestones (level 5 and level 10)
export interface SkillUpgradeDef {
id: string;
name: string;
desc: string;
skillId: string; // Which skill this upgrade belongs to
milestone: 5 | 10; // Level at which this upgrade is available
effect: SkillUpgradeEffect;
}
export interface SkillUpgradeEffect {
type: 'multiplier' | 'bonus' | 'special';
stat?: string; // Stat to modify
value?: number; // Multiplier or bonus value
specialId?: string; // Special effect identifier
specialDesc?: string; // Description of special effect
}
// Skill evolution system - 5-Tier Continuous Talent Tree
// Each skill with max level 10 follows this structure:
// - 5 Tiers (T1-T5)
// - Each tier has L5 and L10 milestone perk choices
// - 3 paths per tier (A, B, C columns) representing different playstyles
// - T3 L10 and T5 L10 have Elite Perks (game-changing mechanics)
export interface SkillEvolutionPath {
baseSkillId: string; // Starting skill ID
tiers: SkillTierDef[]; // 5 tiers of evolution
}
export interface SkillTierDef {
tier: number; // Tier number (1-5)
skillId: string; // Skill ID for this tier
name: string;
multiplier: number; // Base effect multiplier for this tier
// Perk choices organized by milestone and path (A, B, C)
l5Perks: SkillPerkChoice[]; // 3 choices (one per path) at level 5
l10Perks: SkillPerkChoice[]; // 3 choices (one per path) at level 10
}
// Perk choice at a milestone - belongs to a specific path (A, B, or C)
export interface SkillPerkChoice {
id: string;
name: string;
desc: string;
path: 'A' | 'B' | 'C'; // Which path this perk belongs to
isElite?: boolean; // True for T3 L10 and T5 L10 perks
effect: SkillUpgradeEffect;
// For path compounding - if player stays on same path, bonuses compound
pathCompoundBonus?: number; // Exponential bonus for staying on same path
}
export interface SkillUpgradeChoice {
id: string;
name: string;
desc: string;
milestone: 5 | 10; // Level at which this upgrade is available
effect: SkillUpgradeEffect;
}
export interface PrestigeDef {
name: string;
desc: string;
max: number;
cost: number;
}
+38
View File
@@ -0,0 +1,38 @@
// ─── Spell Types ────────────────────────────────────────────────────────────
// Spell cost can be raw mana or elemental mana
export interface SpellCost {
type: 'raw' | 'element'; // 'raw' for raw mana, 'element' for specific elemental mana
element?: string; // Required if type is 'element'
amount: number; // Amount of mana required
}
export interface SpellDef {
name: string;
elem: string; // Element type for damage calculations
dmg: number;
cost: SpellCost; // Changed from number to SpellCost object
tier: number;
unlock: number; // Mana cost to start studying
studyTime?: number; // Hours needed to study (optional, defaults based on tier)
castSpeed?: number; // Casts per hour (default 1, higher = faster)
desc?: string; // Optional spell description
effects?: SpellEffect[]; // Optional special effects
isAoe?: boolean; // AOE spell that hits multiple enemies
aoeTargets?: number; // Number of enemies hit by AOE
isWeaponEnchant?: boolean; // Can be used as weapon enchantment (magic swords)
}
export interface SpellEffect {
type: 'burn' | 'freeze' | 'stun' | 'pierce' | 'multicast' | 'shield' | 'buff' | 'chain' | 'aoe' | 'armor_pierce';
value: number; // Effect potency
duration?: number; // Duration in hours for timed effects
targets?: number; // For AOE: number of targets
chance?: number; // For chance-based effects (e.g., stun chance)
}
export interface SpellState {
learned: boolean;
level: number;
studyProgress?: number; // Hours studied so far (for in-progress spells)
}
+1
View File
@@ -94,6 +94,7 @@ export const SPECIAL_EFFECTS = {
FIRST_STRIKE: 'firstStrike', // +15% damage on first attack each floor FIRST_STRIKE: 'firstStrike', // +15% damage on first attack each floor
OVERPOWER: 'overpower', // +50% damage when mana above 80% OVERPOWER: 'overpower', // +50% damage when mana above 80%
BERSERKER: 'berserker', // +50% damage when below 50% mana BERSERKER: 'berserker', // +50% damage when below 50% mana
EXECUTIONER: 'executioner', // +50% damage when enemy below 25% HP
COMBO_MASTER: 'comboMaster', // Every 5th attack deals 3x damage COMBO_MASTER: 'comboMaster', // Every 5th attack deals 3x damage
ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana