Compare commits
11 Commits
ac70ed1910
...
c46981d81b
| Author | SHA1 | Date | |
|---|---|---|---|
| c46981d81b | |||
| f8520e15b8 | |||
| eb81ccb5aa | |||
| 3d05f03570 | |||
| c974f2e4d9 | |||
| 8e28fb0bac | |||
| 4c96eae286 | |||
| c19ce5111e | |||
| 8995a31527 | |||
| c20ab62827 | |||
| 94830509c2 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
@@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
|
||||
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 { getDamageBreakdown } from '@/lib/game/computed-stats';
|
||||
|
||||
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 { formatHour } from '@/lib/game/formatting';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { useCombatStore } from '@/lib/game/stores/combatStore';
|
||||
import { useGameStore, useGameLoop } from '@/lib/game/stores/gameStore';
|
||||
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/upgrade-effects';
|
||||
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import {
|
||||
computeMaxMana,
|
||||
computeRegen,
|
||||
|
||||
@@ -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,9 +2,9 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
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 { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/upgrade-effects';
|
||||
import { computeEffects } from '@/lib/game/upgrade-effects';
|
||||
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useGameStore, canAffordSpellCost, fmt, fmtDec, calcDamage } from '@/lib/game/store';
|
||||
import { ELEMENTS, SPELLS_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||
import { useGameStore, canAffordSpellCost } from '@/lib/game/store';
|
||||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useGameStore, canAffordSpellCost, fmt, fmtDec, calcDamage, computePactMultiplier } from '@/lib/game/store';
|
||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, MANA_PER_ELEMENT } from '@/lib/game/constants';
|
||||
import { useGameStore, canAffordSpellCost, fmt, fmtDec, calcDamage } from '@/lib/game/store';
|
||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useGameStore, fmt, fmtDec, calcDamage, computePactMultiplier, computePactInsightMultiplier } from '@/lib/game/store';
|
||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { useGameStore, fmt, fmtDec } from '@/lib/game/store';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 { Badge } from '@/components/ui/badge';
|
||||
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 { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { getTierMultiplier, getBaseSkillId } from '@/lib/game/skill-evolution';
|
||||
|
||||
@@ -5,9 +5,8 @@ import { ELEMENTS } from '@/lib/game/constants';
|
||||
import type { GameStore, AttunementState } from '@/lib/game/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Lock, Sparkles, TrendingUp } from 'lucide-react';
|
||||
import { Lock, TrendingUp } from 'lucide-react';
|
||||
|
||||
export interface AttunementsTabProps {
|
||||
store: GameStore;
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
Wand2, Scroll, Hammer, Sparkles, Trash2, Plus, Minus,
|
||||
Package, Zap, Clock, ChevronRight, Circle, Anvil
|
||||
} from 'lucide-react';
|
||||
import { EQUIPMENT_TYPES, type EquipmentType, type EquipmentSlot } from '@/lib/game/data/equipment';
|
||||
import { ENCHANTMENT_EFFECTS, type EnchantmentEffectDef, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects';
|
||||
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects';
|
||||
import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes';
|
||||
import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
||||
import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
@@ -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">"{guardian.uniquePerk}"</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">"{guardian.uniquePerk}"</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>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
||||
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';
|
||||
|
||||
interface SpellsTabProps {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
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 { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ELEMENTS, GUARDIANS, SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import { fmt, fmtDec, calcDamage } from '@/lib/game/store';
|
||||
import { fmt, fmtDec } from '@/lib/game/store';
|
||||
import type { SkillUpgradeChoice, GameStore, UnifiedEffects } from '@/lib/game/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { getUpgradesForSkillAtMilestone, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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'];
|
||||
@@ -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"
|
||||
},
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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 },
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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'];
|
||||
@@ -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.",
|
||||
},
|
||||
};
|
||||
@@ -1,846 +1,21 @@
|
||||
// ─── 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'
|
||||
|
||||
// Helper to define allowed equipment categories for each effect type
|
||||
const ALL_CASTER: EquipmentCategory[] = ['caster']
|
||||
const CASTER_AND_SWORD: EquipmentCategory[] = ['caster', 'sword']
|
||||
const WEAPON_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'sword'] // All main hand equipment
|
||||
const CASTER_AND_HANDS: EquipmentCategory[] = ['caster', 'hands']
|
||||
const BODY_AND_SHIELD: EquipmentCategory[] = ['body', 'shield']
|
||||
const CASTER_CATALYST_ACCESSORY: EquipmentCategory[] = ['caster', 'catalyst', 'accessory']
|
||||
const MANA_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'accessory']
|
||||
const UTILITY_EQUIPMENT: EquipmentCategory[] = ['caster', 'catalyst', 'head', 'body', 'hands', 'feet', 'accessory']
|
||||
const ALL_EQUIPMENT: EquipmentCategory[] = ['caster', 'shield', 'catalyst', 'sword', 'head', 'body', 'hands', 'feet', 'accessory']
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
export {
|
||||
ENCHANTMENT_EFFECTS,
|
||||
type EnchantmentEffectCategory,
|
||||
type EnchantmentEffectDef,
|
||||
getEnchantmentEffect,
|
||||
getEffectsForEquipment,
|
||||
canApplyEffect,
|
||||
calculateEffectCapacityCost,
|
||||
// Also export category-specific collections
|
||||
SPELL_EFFECTS,
|
||||
MANA_EFFECTS,
|
||||
COMBAT_EFFECTS,
|
||||
ELEMENTAL_EFFECTS,
|
||||
DEFENSE_EFFECTS,
|
||||
UTILITY_EFFECTS,
|
||||
SPECIAL_EFFECTS,
|
||||
} from './enchantments/index'
|
||||
|
||||
@@ -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' }
|
||||
},
|
||||
};
|
||||
@@ -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
@@ -1,516 +1,14 @@
|
||||
// ─── Game Types ───────────────────────────────────────────────────────────────
|
||||
|
||||
export type ElementCategory = 'base' | 'utility' | 'composite' | 'exotic';
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 };
|
||||
// This file now re-exports types from domain-specific files in the types/ directory.
|
||||
// All type definitions have been moved to:
|
||||
// - types/elements.ts - element-related types
|
||||
// - types/attunements.ts - attunement-related types
|
||||
// - types/spells.ts - spell-related types
|
||||
// - types/skills.ts - skill-related types
|
||||
// - types/equipment.ts - equipment-related types
|
||||
// - types/game.ts - core game state types
|
||||
//
|
||||
// Import from this file (types.ts) to maintain backward compatibility,
|
||||
// or import directly from the domain-specific files.
|
||||
|
||||
export * from './types/index';
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -94,6 +94,7 @@ export const SPECIAL_EFFECTS = {
|
||||
FIRST_STRIKE: 'firstStrike', // +15% damage on first attack each floor
|
||||
OVERPOWER: 'overpower', // +50% damage when mana above 80%
|
||||
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
|
||||
ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana
|
||||
|
||||
|
||||
Reference in New Issue
Block a user