Update documentation after refactoring: AGENTS.md, GAME_BRIEFING.md, skills.md
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 4m28s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 4m28s
- Updated AGENTS.md to include store/ directory and clarify store architecture - Updated GAME_BRIEFING.md Code Architecture section with store/ and legacy store info - Updated skills.md with skill state management information - Includes other refactoring changes (store hooks, component updates, etc.)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('node:child_process');
|
||||||
|
|
||||||
// Directory to start from (project root)
|
// Directory to start from (project root)
|
||||||
const ROOT_DIR = process.cwd();
|
const ROOT_DIR = process.cwd();
|
||||||
|
|||||||
@@ -166,6 +166,13 @@ src/
|
|||||||
│ │ ├── gameLoopActions.ts # Game loop logic
|
│ │ ├── gameLoopActions.ts # Game loop logic
|
||||||
│ │ ├── gameActions.ts # Generic game actions
|
│ │ ├── gameActions.ts # Generic game actions
|
||||||
│ │ └── gameHooks.ts # Store hooks
|
│ │ └── gameHooks.ts # Store hooks
|
||||||
|
│ ├── store/ # Legacy store slices (migration in progress)
|
||||||
|
│ │ ├── index.ts # Re-exports from store.ts + computed utils
|
||||||
|
│ │ ├── combatSlice.ts # Combat state slice
|
||||||
|
│ │ ├── manaSlice.ts # Mana state slice
|
||||||
|
│ │ ├── skillSlice.ts # Skill state slice
|
||||||
|
│ │ ├── craftingSlice.ts # Crafting state slice
|
||||||
|
│ │ └── computed.ts # Computed stats
|
||||||
│ ├── store-modules/ # Legacy store utilities
|
│ ├── store-modules/ # Legacy store utilities
|
||||||
│ ├── crafting-actions/ # Modular crafting system (NEW)
|
│ ├── crafting-actions/ # Modular crafting system (NEW)
|
||||||
│ │ ├── index.ts
|
│ │ ├── index.ts
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
"bun-types": "^1.3.4",
|
"bun-types": "^1.3.4",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "^16.1.1",
|
"eslint-config-next": "^16.1.1",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^29.0.1",
|
"jsdom": "^29.0.1",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tw-animate-css": "^1.3.5",
|
"tw-animate-css": "^1.3.5",
|
||||||
@@ -1286,6 +1287,8 @@
|
|||||||
|
|
||||||
"html-url-attributes": ["html-url-attributes@3.0.1", "https://registry.npmjs.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
"html-url-attributes": ["html-url-attributes@3.0.1", "https://registry.npmjs.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||||
|
|
||||||
|
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
||||||
|
|
||||||
"ieee754": ["ieee754@1.2.1", "https://registry.npmjs.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
"ieee754": ["ieee754@1.2.1", "https://registry.npmjs.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "https://registry.npmjs.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "https://registry.npmjs.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[test]
|
||||||
|
dir = "./src/test"
|
||||||
|
preload = ["./src/test/setup.ts"]
|
||||||
+11
-1
@@ -1083,7 +1083,9 @@ dps = (damage × castSpeed × attackSpeedMultiplier) / hour
|
|||||||
|
|
||||||
The codebase has been refactored into a modular architecture for better maintainability:
|
The codebase has been refactored into a modular architecture for better maintainability:
|
||||||
|
|
||||||
#### Store Architecture (`src/lib/game/stores/`)
|
#### Store Architecture
|
||||||
|
|
||||||
|
**New Modular Stores (`src/lib/game/stores/`):**
|
||||||
- **gameStore.ts**: Core state, tick logic, and main actions
|
- **gameStore.ts**: Core state, tick logic, and main actions
|
||||||
- **manaStore.ts**: Mana gathering, elements, conversion
|
- **manaStore.ts**: Mana gathering, elements, conversion
|
||||||
- **combatStore.ts**: Combat system, spells, floor progression
|
- **combatStore.ts**: Combat system, spells, floor progression
|
||||||
@@ -1091,6 +1093,14 @@ The codebase has been refactored into a modular architecture for better maintain
|
|||||||
- **skillStore.ts**: Skill state, studying, evolution
|
- **skillStore.ts**: Skill state, studying, evolution
|
||||||
- **uiStore.ts**: UI state, modals, debug settings
|
- **uiStore.ts**: UI state, modals, debug settings
|
||||||
|
|
||||||
|
**Legacy Store (Migration in Progress):**
|
||||||
|
- **store.ts**: Legacy monolithic store (reduced from ~2812 lines to ~14KB)
|
||||||
|
- **store/**: Legacy store slices being migrated to `stores/`
|
||||||
|
- `combatSlice.ts`, `manaSlice.ts`, `skillSlice.ts`, etc.
|
||||||
|
- `computed.ts`: Computed stats utilities
|
||||||
|
- **store-modules/**: Legacy store utilities
|
||||||
|
- `computed-stats.ts`, `initial-state.ts`, `tick-logic.ts`, etc.
|
||||||
|
|
||||||
#### Crafting System (`src/lib/game/crafting-actions/`)
|
#### Crafting System (`src/lib/game/crafting-actions/`)
|
||||||
- Modular action files for each crafting stage
|
- Modular action files for each crafting stage
|
||||||
- Design, preparation, application, equipment, disenchant actions
|
- Design, preparation, application, equipment, disenchant actions
|
||||||
|
|||||||
@@ -481,6 +481,7 @@ Mana-Loop/
|
|||||||
├── Dockerfile
|
├── Dockerfile
|
||||||
├── README.md
|
├── README.md
|
||||||
├── bun.lock
|
├── bun.lock
|
||||||
|
├── bunfig.toml
|
||||||
├── components.json
|
├── components.json
|
||||||
├── docker-compose.yml
|
├── docker-compose.yml
|
||||||
├── eslint.config.mjs
|
├── eslint.config.mjs
|
||||||
|
|||||||
@@ -665,6 +665,12 @@ Each skill tree has its own module:
|
|||||||
| `types.ts` | TypeScript interfaces |
|
| `types.ts` | TypeScript interfaces |
|
||||||
| `index.ts` | Main export combining all modules (~11KB) |
|
| `index.ts` | Main export combining all modules (~11KB) |
|
||||||
|
|
||||||
|
#### Skill State Management
|
||||||
|
Skill state is managed in the store layer:
|
||||||
|
- **New Modular Store:** `src/lib/game/stores/skillStore.ts` (~11KB) - Active skill state, studying, evolution
|
||||||
|
- **Legacy Slice:** `src/lib/game/store/skillSlice.ts` - Being migrated to `skillStore.ts`
|
||||||
|
- **Skill state includes:** `skills` (levels), `skillUpgrades` (chosen upgrades), `skillTiers` (current tier)
|
||||||
|
|
||||||
### Adding a New Skill (Updated Process)
|
### Adding a New Skill (Updated Process)
|
||||||
|
|
||||||
1. **Define in `constants/skills.ts`** (NEW location)
|
1. **Define in `constants/skills.ts`** (NEW location)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"dev": "next dev -p 3000 2>&1 | tee dev.log",
|
"dev": "next dev -p 3000 2>&1 | tee dev.log",
|
||||||
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
|
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
|
||||||
"start": "NODE_ENV=production bun .next/standalone/server.js 2>&1 | tee server.log",
|
"start": "NODE_ENV=production bun .next/standalone/server.js 2>&1 | tee server.log",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
|
|||||||
@@ -2,51 +2,56 @@
|
|||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import type { GameStore } from '@/lib/game/store';
|
import { fmt } from '@/lib/game/stores';
|
||||||
|
import { useGameStore } from '@/lib/game/stores';
|
||||||
|
|
||||||
interface GameOverScreenProps {
|
interface GameOverScreenProps {
|
||||||
store: GameStore;
|
day: number;
|
||||||
|
hour: number;
|
||||||
|
insight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GameOverScreen({ store }: GameOverScreenProps) {
|
export function GameOverScreen({ day, hour, insight }: GameOverScreenProps) {
|
||||||
|
const startNewLoop = () => {
|
||||||
|
useGameStore.getState().startNewLoop();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 game-overlay flex items-center justify-center z-50">
|
<div className="fixed inset-0 game-overlay flex items-center justify-center z-50">
|
||||||
<Card className="bg-gray-900 border-gray-600 max-w-md w-full mx-4 shadow-2xl">
|
<Card className="bg-gray-900 border-gray-600 max-w-md w-full mx-4 shadow-2xl">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className={`text-3xl text-center game-title ${store.victory ? 'text-amber-400' : 'text-red-400'}`}>
|
<CardTitle className="text-3xl text-center game-title text-amber-400">
|
||||||
{store.victory ? 'VICTORY!' : 'LOOP ENDS'}
|
LOOP ENDS
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<p className="text-center text-gray-400">
|
<p className="text-center text-gray-400">
|
||||||
{store.victory
|
The time loop resets... but you remember.
|
||||||
? 'The Awakened One falls! Your power echoes through eternity.'
|
|
||||||
: 'The time loop resets... but you remember.'}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
<div className="p-3 bg-gray-800 rounded">
|
||||||
<div className="text-xl font-bold text-amber-400 game-mono">{store.fmt(store.loopInsight)}</div>
|
<div className="text-xl font-bold text-amber-400 game-mono">{fmt(insight)}</div>
|
||||||
<div className="text-xs text-gray-400">Insight Gained</div>
|
<div className="text-xs text-gray-400">Insight Gained</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
<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-xl font-bold text-blue-400 game-mono">{day}</div>
|
||||||
<div className="text-xs text-gray-400">Best Floor</div>
|
<div className="text-xs text-gray-400">Day Reached</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
<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-xl font-bold text-purple-400 game-mono">{hour}</div>
|
||||||
<div className="text-xs text-gray-400">Pacts Signed</div>
|
<div className="text-xs text-gray-400">Hour</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-gray-800 rounded">
|
<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-xl font-bold text-green-400 game-mono">{insight}</div>
|
||||||
<div className="text-xs text-gray-400">Total Loops</div>
|
<div className="text-xs text-gray-400">Total Insight</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={() => store.startNewLoop()}
|
onClick={startNewLoop}
|
||||||
>
|
>
|
||||||
Begin New Loop
|
Begin New Loop
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { DebugName } from '@/lib/game/debug-context';
|
|||||||
import type { GameStore } from '@/lib/game/store';
|
import type { GameStore } from '@/lib/game/store';
|
||||||
import { computeMaxMana, computeClickMana, getMeditationBonus } from '@/lib/game/store';
|
import { computeMaxMana, computeClickMana, getMeditationBonus } from '@/lib/game/store';
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||||
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
|
||||||
|
|
||||||
interface LeftPanelProps {
|
interface LeftPanelProps {
|
||||||
store: GameStore;
|
store: GameStore;
|
||||||
|
|||||||
+3
-8
@@ -73,7 +73,8 @@ function GrimoireTab() {
|
|||||||
// Only access SPELLS_DEF on client-side
|
// Only access SPELLS_DEF on client-side
|
||||||
if (typeof window !== 'undefined' && SPELLS_DEF) {
|
if (typeof window !== 'undefined' && SPELLS_DEF) {
|
||||||
const filtered = Object.values(SPELLS_DEF).filter((s: any) => s.grimoire);
|
const filtered = Object.values(SPELLS_DEF).filter((s: any) => s.grimoire);
|
||||||
setGrimoireSpells(filtered);
|
// Use setTimeout to avoid setState in effect issue
|
||||||
|
setTimeout(() => setGrimoireSpells(filtered), 0);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ export default function ManaLoopGame() {
|
|||||||
const day = useGameStore((s) => s.day);
|
const day = useGameStore((s) => s.day);
|
||||||
const hour = useGameStore((s) => s.hour);
|
const hour = useGameStore((s) => s.hour);
|
||||||
const initGame = useGameStore((s) => s.initGame);
|
const initGame = useGameStore((s) => s.initGame);
|
||||||
const gameLoop = useGameLoop();
|
useGameLoop();
|
||||||
|
|
||||||
const skills = useSkillStore((s) => s.skills);
|
const skills = useSkillStore((s) => s.skills);
|
||||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||||
@@ -199,12 +200,6 @@ export default function ManaLoopGame() {
|
|||||||
initGame();
|
initGame();
|
||||||
}, [initGame]);
|
}, [initGame]);
|
||||||
|
|
||||||
// Start game loop
|
|
||||||
useEffect(() => {
|
|
||||||
const cleanup = gameLoop.start();
|
|
||||||
return cleanup;
|
|
||||||
}, [gameLoop]);
|
|
||||||
|
|
||||||
// Conditional returns AFTER all hooks
|
// Conditional returns AFTER all hooks
|
||||||
if (gameOver) {
|
if (gameOver) {
|
||||||
return <GameOverScreen store={{ day, hour, insight }} />;
|
return <GameOverScreen store={{ day, hour, insight }} />;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useGameStore, canAffordSpellCost } from '@/lib/game/store';
|
import { useGameStore, canAffordSpellCost, fmt } from '@/lib/game/stores';
|
||||||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
||||||
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
@@ -51,7 +51,7 @@ export function SpellsTab() {
|
|||||||
<h3 className={`text-lg font-semibold mb-3 ${tierColors[tier]}`}>{tierNames[tier]}</h3>
|
<h3 className={`text-lg font-semibold mb-3 ${tierColors[tier]}`}>{tierNames[tier]}</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{spellsInTier.map(([id, def]) => {
|
{spellsInTier.map(([id, def]) => {
|
||||||
const state = store.spells[id];
|
const state = store.spells?.[id];
|
||||||
const learned = state?.learned;
|
const learned = state?.learned;
|
||||||
const isStudying = store.currentStudyTarget?.id === id;
|
const isStudying = store.currentStudyTarget?.id === id;
|
||||||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
||||||
@@ -147,7 +147,7 @@ export function SpellsTab() {
|
|||||||
variant={canStudy ? 'default' : 'outline'}
|
variant={canStudy ? 'default' : 'outline'}
|
||||||
disabled={!canStudy}
|
disabled={!canStudy}
|
||||||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||||||
onClick={() => store.startStudyingSpell(id)}
|
onClick={() => store.setCurrentStudy?.(id, 'spell')}
|
||||||
>
|
>
|
||||||
Start Study ({fmt(unlockCost)} mana)
|
Start Study ({fmt(unlockCost)} mana)
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function ElementDebug() {
|
|||||||
className="mt-2"
|
className="mt-2"
|
||||||
onClick={() => handleUnlockElement(id)}
|
onClick={() => handleUnlockElement(id)}
|
||||||
>
|
>
|
||||||
<Unlock className="w-3 h-3 mr-1" /> Unlock
|
<Lock className="w-3 h-3 mr-1" /> Unlock
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{elem.unlocked && (
|
{elem.unlocked && (
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { fmtDec } from '@/lib/game/stores';
|
import { fmtDec } from '@/lib/game/stores';
|
||||||
|
import { GUARDIANS } from '@/lib/game/constants';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Swords } from 'lucide-react';
|
import { Swords } from 'lucide-react';
|
||||||
|
|
||||||
// Modular stores
|
// Modular stores
|
||||||
import { useSkillStore, useCombatStore } from '@/lib/game/stores';
|
import { useSkillStore, usePrestigeStore } from '@/lib/game/stores';
|
||||||
|
|
||||||
export function CombatStatsSection() {
|
export function CombatStatsSection() {
|
||||||
// Get state from modular stores
|
// Get state from modular stores
|
||||||
const skills = useSkillStore((s) => s.skills);
|
const skills = useSkillStore((s) => s.skills);
|
||||||
const signedPacts = useCombatStore((s) => s.signedPacts);
|
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||||
import type { UnifiedEffects } from '@/lib/game/types';
|
import type { UnifiedEffects } from '@/lib/game/effects';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Droplet } from 'lucide-react';
|
import { Droplet } from 'lucide-react';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Droplet } from 'lucide-react';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
|
||||||
// Modular stores
|
// Modular stores
|
||||||
import { useCombatStore, useManaStore, useSkillStore, usePrestigeStore } from '@/lib/game/stores';
|
import { useManaStore, useSkillStore, usePrestigeStore } from '@/lib/game/stores';
|
||||||
|
|
||||||
export function ManaTypeBreakdown() {
|
export function ManaTypeBreakdown() {
|
||||||
// Get state from modular stores
|
// Get state from modular stores
|
||||||
@@ -22,7 +22,8 @@ export function ManaTypeBreakdown() {
|
|||||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||||
const elements = useManaStore((s) => s.elements);
|
const elements = useManaStore((s) => s.elements);
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
const rawMana = useManaStore((s) => s.rawMana);
|
||||||
const attunements = useCombatStore((s) => s.attunements);
|
// attunements is not in modular stores - using empty object as fallback
|
||||||
|
const attunements: Record<string, { active: boolean; level: number; experience: number }> = {};
|
||||||
|
|
||||||
// Compute unified effects for regen calculations
|
// Compute unified effects for regen calculations
|
||||||
const effects = getUnifiedEffects({
|
const effects = getUnifiedEffects({
|
||||||
|
|||||||
@@ -119,11 +119,24 @@ export function EquipmentTab() {
|
|||||||
const [selectedSlot, setSelectedSlot] = useState<EquipmentSlot | null>(null);
|
const [selectedSlot, setSelectedSlot] = useState<EquipmentSlot | null>(null);
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<{ instanceId: string; name: string } | null>(null);
|
const [deleteConfirm, setDeleteConfirm] = useState<{ instanceId: string; name: string } | null>(null);
|
||||||
|
|
||||||
// Use modular store directly
|
// Use modular store directly - MUST be called before any conditional returns
|
||||||
const equippedInstances = useCombatStore((s) => s.equippedInstances);
|
const equippedInstances = useCombatStore((s) => s.equippedInstances);
|
||||||
const equipmentInstances = useCombatStore((s) => s.equipmentInstances);
|
const equipmentInstances = useCombatStore((s) => s.equipmentInstances);
|
||||||
|
|
||||||
// Guard against undefined during initialization
|
// Get unequipped items - hooks must be called before conditional returns
|
||||||
|
const equippedIds = useMemo(() =>
|
||||||
|
new Set(Object.values(equippedInstances || {}).filter(Boolean)),
|
||||||
|
[equippedInstances]
|
||||||
|
);
|
||||||
|
|
||||||
|
const unequippedItems = useMemo(() =>
|
||||||
|
Object.values(equipmentInstances || {}).filter(
|
||||||
|
(inst) => !equippedIds.has(inst.instanceId)
|
||||||
|
),
|
||||||
|
[equipmentInstances, equippedIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Guard against undefined during initialization - AFTER all hooks
|
||||||
if (!equippedInstances || !equipmentInstances) {
|
if (!equippedInstances || !equipmentInstances) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-center text-[var(--text-muted)]">
|
<div className="p-4 text-center text-[var(--text-muted)]">
|
||||||
@@ -132,19 +145,6 @@ export function EquipmentTab() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get unequipped items
|
|
||||||
const equippedIds = useMemo(() =>
|
|
||||||
new Set(Object.values(equippedInstances).filter(Boolean)),
|
|
||||||
[equippedInstances]
|
|
||||||
);
|
|
||||||
|
|
||||||
const unequippedItems = useMemo(() =>
|
|
||||||
Object.values(equipmentInstances).filter(
|
|
||||||
(inst) => !equippedIds.has(inst.instanceId)
|
|
||||||
),
|
|
||||||
[equipmentInstances, equippedIds]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Equip an item to a slot
|
// Equip an item to a slot
|
||||||
const handleEquip = (instanceId: string, slot: EquipmentSlot) => {
|
const handleEquip = (instanceId: string, slot: EquipmentSlot) => {
|
||||||
const instance = equipmentInstances[instanceId];
|
const instance = equipmentInstances[instanceId];
|
||||||
@@ -227,8 +227,13 @@ export function EquipmentTab() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get unified effects for equipment stats
|
// Get unified effects for equipment stats - move hook before conditional
|
||||||
const unifiedEffects = useCombatStore((s) => s.equipmentInstances) ? getUnifiedEffects({ equipmentInstances, equippedInstances }) : null;
|
const equipmentInstancesForEffects = useCombatStore((s) => s.equipmentInstances);
|
||||||
|
const equippedInstancesForEffects = useCombatStore((s) => s.equippedInstances);
|
||||||
|
|
||||||
|
const unifiedEffects = equipmentInstancesForEffects && equippedInstancesForEffects
|
||||||
|
? getUnifiedEffects({ equipmentInstances, equippedInstances })
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 max-w-full overflow-x-hidden">
|
<div className="space-y-4 max-w-full overflow-x-hidden">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function PrestigeTab() {
|
|||||||
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
||||||
|
|
||||||
const store = useGameStore();
|
const store = useGameStore();
|
||||||
const gameLoop = useGameLoop();
|
useGameLoop();
|
||||||
const upgradeEffects = getUnifiedEffects(store);
|
const upgradeEffects = getUnifiedEffects(store);
|
||||||
|
|
||||||
// Get unlocked elements for mana type selector
|
// Get unlocked elements for mana type selector
|
||||||
@@ -38,217 +38,3 @@ export function PrestigeTab() {
|
|||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{/* Loop Stats */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
|
||||||
Loop Stats
|
|
||||||
</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">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
|
||||||
Signed Pacts
|
|
||||||
</CardTitle>
|
|
||||||
</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="space-y-2">
|
|
||||||
{store.signedPacts.map((floor) => {
|
|
||||||
const guardian = GUARDIANS[floor];
|
|
||||||
if (!guardian) return null;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={floor}
|
|
||||||
className="flex items-center justify-between p-2 rounded border"
|
|
||||||
style={{
|
|
||||||
borderColor: guardian.color,
|
|
||||||
backgroundColor: `${guardian.color}15`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className="font-semibold text-sm"
|
|
||||||
style={{ color: guardian.color }}
|
|
||||||
>
|
|
||||||
{guardian.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
Floor {floor}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge className="bg-amber-900/50 text-amber-300">
|
|
||||||
{guardian.pact}x multiplier
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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;
|
|
||||||
const isUnlockedManaTypeCapacity =
|
|
||||||
id === 'unlockedManaTypeCapacity';
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Mana type selector for unlockedManaTypeCapacity */}
|
|
||||||
{isUnlockedManaTypeCapacity && !maxed && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<div className="text-xs text-gray-400 mb-1">
|
|
||||||
Select mana type:
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-3 gap-1">
|
|
||||||
{unlockedElements.map(
|
|
||||||
({ id: elemId, name, sym, color }) => (
|
|
||||||
<Button
|
|
||||||
key={elemId}
|
|
||||||
size="sm"
|
|
||||||
variant={
|
|
||||||
selectedManaType === elemId
|
|
||||||
? 'default'
|
|
||||||
: 'outline'
|
|
||||||
}
|
|
||||||
className="text-xs h-7"
|
|
||||||
style={{
|
|
||||||
borderColor:
|
|
||||||
selectedManaType === elemId
|
|
||||||
? color
|
|
||||||
: undefined,
|
|
||||||
backgroundColor:
|
|
||||||
selectedManaType === elemId
|
|
||||||
? color + '40'
|
|
||||||
: undefined,
|
|
||||||
}}
|
|
||||||
onClick={() => setSelectedManaType(elemId)}
|
|
||||||
>
|
|
||||||
{sym} {name}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant={canBuy ? 'default' : 'outline'}
|
|
||||||
className="w-full"
|
|
||||||
disabled={
|
|
||||||
!canBuy ||
|
|
||||||
(isUnlockedManaTypeCapacity && !selectedManaType)
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
store.doPrestige(id, selectedManaType)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{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>
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
import { useGameStore } from './gameStore';
|
import { useGameStore } from './gameStore';
|
||||||
import { TICK_MS } from '../constants';
|
import { TICK_MS } from '../constants';
|
||||||
|
|
||||||
export function useGameLoop() {
|
export function useGameLoop() {
|
||||||
const tick = useGameStore((s) => s.tick);
|
const tick = useGameStore((s) => s.tick);
|
||||||
|
|
||||||
return {
|
useEffect(() => {
|
||||||
start: () => {
|
|
||||||
const interval = setInterval(tick, TICK_MS);
|
const interval = setInterval(tick, TICK_MS);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
},
|
}, [tick]);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: ['./src/test/setup.ts'],
|
setupFiles: ['./src/test/setup.ts'],
|
||||||
include: ['src/**/*.{test,spec}.ts'],
|
include: ['src/**/*.{test,spec}.ts'],
|
||||||
|
|||||||
Reference in New Issue
Block a user