Commit
This commit is contained in:
411
pwa/GAME_REFACTORING_GUIDE.md
Normal file
411
pwa/GAME_REFACTORING_GUIDE.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# Game.tsx Refactoring - Implementation Guide
|
||||
|
||||
## Current Status
|
||||
|
||||
The 3,315-line `Game.tsx` file has been partially refactored into a modular structure.
|
||||
|
||||
## Completed Work
|
||||
|
||||
### ✅ Created Structure
|
||||
```
|
||||
src/components/game/
|
||||
├── types.ts # All TypeScript interfaces
|
||||
├── hooks/
|
||||
│ └── useGameEngine.ts # Core state management hook
|
||||
└── MovementControls.tsx # Movement/compass component
|
||||
```
|
||||
|
||||
### ✅ types.ts
|
||||
Contains all game-related interfaces:
|
||||
- `PlayerState` - Player location, health, stamina, inventory
|
||||
- `DirectionDetail` - Movement directions with costs
|
||||
- `Location` - Current location data
|
||||
- `Profile` - Character stats and attributes
|
||||
- `CombatLogEntry` - Combat message format
|
||||
- `LocationMessage` - Location event messages
|
||||
- `Equipment` - Equipment slots
|
||||
- `CombatState` - Combat status (PvE and PvP)
|
||||
- `WorkbenchTab` - Crafting menu tabs
|
||||
- `MobileMenuState` - Mobile UI state
|
||||
|
||||
### ✅ useGameEngine.ts
|
||||
Core state management hook that exports:
|
||||
|
||||
**State Object:**
|
||||
- All game state (player, location, profile, combat, etc.)
|
||||
- UI state (menus, mobile, filters, etc.)
|
||||
- Cooldowns and timers
|
||||
|
||||
**Actions Object:**
|
||||
- Data fetching functions
|
||||
- Movement handlers
|
||||
- Item handlers (pickup, use, equip, drop)
|
||||
- Crafting/workbench handlers
|
||||
- Combat handlers (PvE and PvP)
|
||||
- Interaction handlers
|
||||
- UI state setters
|
||||
|
||||
**Usage Pattern:**
|
||||
```tsx
|
||||
const [state, actions] = useGameEngine(token, handleWebSocketMessage)
|
||||
|
||||
// Access state
|
||||
state.playerState
|
||||
state.location
|
||||
state.combatState
|
||||
|
||||
// Call actions
|
||||
actions.handleMove('north')
|
||||
actions.handlePickup(itemId)
|
||||
actions.fetchGameData()
|
||||
```
|
||||
|
||||
### ✅ MovementControls.tsx
|
||||
Extracted movement UI component:
|
||||
- Compass grid (8 directions)
|
||||
- Special movement buttons (up/down/enter/exit)
|
||||
- Stamina cost display
|
||||
- Cooldown indicators
|
||||
- Direction availability logic
|
||||
|
||||
## Next Steps to Complete Refactoring
|
||||
|
||||
### 1. Create Remaining Components
|
||||
|
||||
**CombatView.tsx** - Extract combat UI (approx. 400-500 lines)
|
||||
```tsx
|
||||
interface CombatViewProps {
|
||||
combatState: CombatState
|
||||
combatLog: CombatLogEntry[]
|
||||
profile: Profile
|
||||
equipment: Equipment
|
||||
onCombatAction: (action: string) => void
|
||||
onFlee: () => void
|
||||
onPvPAction: (action: string, targetId: number) => void
|
||||
onPvPAcknowledge: () => void
|
||||
}
|
||||
```
|
||||
|
||||
**LocationView.tsx** - Extract location display (approx. 800-1000 lines)
|
||||
```tsx
|
||||
interface LocationViewProps {
|
||||
location: Location
|
||||
profile: Profile
|
||||
locationMessages: LocationMessage[]
|
||||
interactableCooldowns: Record<string, number>
|
||||
onPickup: (itemId: number, quantity?: number) => void
|
||||
onInteract: (interactableId: string, actionId: string) => void
|
||||
onInitiateCombat: (enemyId: number) => void
|
||||
onLootCorpse: (corpseId: string) => void
|
||||
onViewCorpseDetails: (corpseId: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
**PlayerSidebar.tsx** - Extract inventory/equipment UI (approx. 600-800 lines)
|
||||
```tsx
|
||||
interface PlayerSidebarProps {
|
||||
profile: Profile
|
||||
playerState: PlayerState
|
||||
equipment: Equipment
|
||||
collapsedCategories: Set<string>
|
||||
onUseItem: (itemId: string) => void
|
||||
onEquipItem: (inventoryId: number) => void
|
||||
onUnequipItem: (slot: string) => void
|
||||
onDropItem: (itemId: string, quantity?: number) => void
|
||||
onSpendPoint: (stat: string) => void
|
||||
toggleCategory: (category: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
**Workbench.tsx** - Extract crafting/repair UI (approx. 400-500 lines)
|
||||
```tsx
|
||||
interface WorkbenchProps {
|
||||
showCraftingMenu: boolean
|
||||
showRepairMenu: boolean
|
||||
workbenchTab: WorkbenchTab
|
||||
craftableItems: any[]
|
||||
repairableItems: any[]
|
||||
uncraftableItems: any[]
|
||||
craftFilter: string
|
||||
repairFilter: string
|
||||
uncraftFilter: string
|
||||
onClose: () => void
|
||||
onCraft: (itemId: string) => void
|
||||
onRepair: (uniqueItemId: number, inventoryId?: number) => void
|
||||
onUncraft: (uniqueItemId: number, inventoryId: number) => void
|
||||
onSwitchTab: (tab: WorkbenchTab) => void
|
||||
setCraftFilter: (filter: string) => void
|
||||
setRepairFilter: (filter: string) => void
|
||||
setUncraftFilter: (filter: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Update Game.tsx
|
||||
|
||||
Refactored Game.tsx should become a simple orchestrator:
|
||||
|
||||
```tsx
|
||||
import { useGameWebSocket } from '../hooks/useGameWebSocket'
|
||||
import { useGameEngine } from './game/hooks/useGameEngine'
|
||||
import GameHeader from './GameHeader'
|
||||
import MovementControls from './game/MovementControls'
|
||||
import CombatView from './game/CombatView'
|
||||
import LocationView from './game/LocationView'
|
||||
import PlayerSidebar from './game/PlayerSidebar'
|
||||
import Workbench from './game/Workbench'
|
||||
import './Game.css'
|
||||
|
||||
function Game() {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
// WebSocket handler
|
||||
const handleWebSocketMessage = async (message: any) => {
|
||||
// WebSocket message handling logic
|
||||
}
|
||||
|
||||
// Use WebSocket hook
|
||||
useGameWebSocket(token, handleWebSocketMessage)
|
||||
|
||||
// Use game engine hook
|
||||
const [state, actions] = useGameEngine(token, handleWebSocketMessage)
|
||||
|
||||
// Loading/error states
|
||||
if (state.loading) {
|
||||
return <div className="loading">Loading game...</div>
|
||||
}
|
||||
|
||||
if (!state.playerState || !state.location) {
|
||||
return <div className="error">Failed to load game state</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="game-container">
|
||||
{/* Death Overlay */}
|
||||
{state.profile?.is_dead && (
|
||||
<div className="death-overlay">
|
||||
<div className="death-modal">
|
||||
<h1>💀 You Have Died</h1>
|
||||
<p>Your character has been defeated in combat.</p>
|
||||
<button onClick={() => window.location.href = '/characters'}>
|
||||
Return to Character Selection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<GameHeader className={state.mobileHeaderOpen ? 'open' : ''} />
|
||||
|
||||
<main className="game-main">
|
||||
<div className="explore-tab-desktop">
|
||||
{/* Left Sidebar */}
|
||||
<div className={`left-sidebar mobile-menu-panel ${state.mobileMenuOpen === 'left' ? 'open' : ''}`}>
|
||||
<MovementControls
|
||||
location={state.location}
|
||||
profile={state.profile}
|
||||
combatState={state.combatState}
|
||||
movementCooldown={state.movementCooldown}
|
||||
onMove={actions.handleMove}
|
||||
/>
|
||||
|
||||
<LocationView
|
||||
location={state.location}
|
||||
profile={state.profile}
|
||||
locationMessages={state.locationMessages}
|
||||
interactableCooldowns={state.interactableCooldowns}
|
||||
onPickup={actions.handlePickup}
|
||||
onInteract={actions.handleInteract}
|
||||
onInitiateCombat={actions.handleInitiateCombat}
|
||||
onLootCorpse={actions.handleLootCorpse}
|
||||
onViewCorpseDetails={actions.handleViewCorpseDetails}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Center - Combat or Location */}
|
||||
<div className="center-panel mobile-menu-panel">
|
||||
{state.combatState ? (
|
||||
<CombatView
|
||||
combatState={state.combatState}
|
||||
combatLog={state.combatLog}
|
||||
profile={state.profile}
|
||||
equipment={state.equipment}
|
||||
onCombatAction={actions.handleCombatAction}
|
||||
onFlee={actions.handleFlee}
|
||||
onPvPAction={actions.handlePvPAction}
|
||||
onPvPAcknowledge={actions.handlePvPAcknowledge}
|
||||
/>
|
||||
) : (
|
||||
<div className="location-display">
|
||||
{/* Location image and description */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Sidebar */}
|
||||
<div className={`right-sidebar mobile-menu-panel ${state.mobileMenuOpen === 'right' ? 'open' : ''}`}>
|
||||
<PlayerSidebar
|
||||
profile={state.profile}
|
||||
playerState={state.playerState}
|
||||
equipment={state.equipment}
|
||||
collapsedCategories={state.collapsedCategories}
|
||||
onUseItem={actions.handleUseItem}
|
||||
onEquipItem={actions.handleEquipItem}
|
||||
onUnequipItem={actions.handleUnequipItem}
|
||||
onDropItem={actions.handleDropItem}
|
||||
onSpendPoint={actions.handleSpendPoint}
|
||||
toggleCategory={actions.toggleCategoryCollapse}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<div className="mobile-menu-buttons">
|
||||
<button onClick={() => actions.setMobileMenuOpen(state.mobileMenuOpen === 'left' ? 'none' : 'left')}>
|
||||
<span>🧭</span>
|
||||
</button>
|
||||
<button onClick={() => actions.setMobileMenuOpen(state.mobileMenuOpen === 'bottom' ? 'none' : 'bottom')} disabled={!!state.combatState}>
|
||||
<span>📍</span>
|
||||
</button>
|
||||
<button onClick={() => actions.setMobileMenuOpen(state.mobileMenuOpen === 'right' ? 'none' : 'right')}>
|
||||
<span>🎒</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Workbench Modal */}
|
||||
{(state.showCraftingMenu || state.showRepairMenu) && (
|
||||
<Workbench
|
||||
showCraftingMenu={state.showCraftingMenu}
|
||||
showRepairMenu={state.showRepairMenu}
|
||||
workbenchTab={state.workbenchTab}
|
||||
craftableItems={state.craftableItems}
|
||||
repairableItems={state.repairableItems}
|
||||
uncraftableItems={state.uncraftableItems}
|
||||
craftFilter={state.craftFilter}
|
||||
repairFilter={state.repairFilter}
|
||||
uncraftFilter={state.uncraftFilter}
|
||||
onClose={actions.handleCloseCrafting}
|
||||
onCraft={actions.handleCraft}
|
||||
onRepair={actions.handleRepairFromMenu}
|
||||
onUncraft={actions.handleUncraft}
|
||||
onSwitchTab={actions.handleSwitchWorkbenchTab}
|
||||
setCraftFilter={actions.setCraftFilter}
|
||||
setRepairFilter={actions.setRepairFilter}
|
||||
setUncraftFilter={actions.setUncraftFilter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Game
|
||||
```
|
||||
|
||||
### 3. Implementation Strategy
|
||||
|
||||
**Phase 1: Extract Combat (Highest Priority)**
|
||||
- Combat is self-contained and frequently used
|
||||
- ~400-500 lines reduction
|
||||
- Create `CombatView.tsx` with all combat UI logic
|
||||
|
||||
**Phase 2: Extract Location View**
|
||||
- Second largest section (~800-1000 lines)
|
||||
- Create `LocationView.tsx` with NPCs, items, interactables, corpses, other players
|
||||
|
||||
**Phase 3: Extract Player Sidebar**
|
||||
- Inventory, equipment, stats display
|
||||
- ~600-800 lines
|
||||
- Create `PlayerSidebar.tsx`
|
||||
|
||||
**Phase 4: Extract Workbench**
|
||||
- Crafting, repair, salvage UI
|
||||
- ~400-500 lines
|
||||
- Create `Workbench.tsx`
|
||||
|
||||
**Phase 5: Final Integration**
|
||||
- Update Game.tsx to use all components
|
||||
- Test thoroughly
|
||||
- Fix any integration issues
|
||||
|
||||
### 4. Benefits After Completion
|
||||
|
||||
**Before:**
|
||||
- 1 file: 3,315 lines
|
||||
- Hard to navigate
|
||||
- Difficult to test individual features
|
||||
- Merge conflicts likely
|
||||
|
||||
**After:**
|
||||
- Main file: ~200-300 lines (orchestration)
|
||||
- useGameEngine: ~600-800 lines (logic)
|
||||
- 5 component files: ~400-1000 lines each
|
||||
- Clear separation of concerns
|
||||
- Easy to test components individually
|
||||
- Parallel development possible
|
||||
|
||||
### 5. Testing Checklist
|
||||
|
||||
After refactoring, verify:
|
||||
- ✅ Movement works (compass, special directions)
|
||||
- ✅ Combat starts and ends correctly
|
||||
- ✅ Inventory management (use, equip, drop)
|
||||
- ✅ Crafting/repair/salvage
|
||||
- ✅ Interactables and cooldowns
|
||||
- ✅ Corpse looting
|
||||
- ✅ PvP combat
|
||||
- ✅ WebSocket updates
|
||||
- ✅ Mobile responsive menus
|
||||
- ✅ Death overlay
|
||||
- ✅ Stat point spending
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**useGameEngine Hook Pattern:**
|
||||
- Separates state management from UI
|
||||
- Provides clean API for actions
|
||||
- Can be tested independently
|
||||
- Reduces prop drilling
|
||||
|
||||
**Component Props Pattern:**
|
||||
- Each component receives only what it needs
|
||||
- Clear interfaces defined
|
||||
- Easy to understand dependencies
|
||||
- Facilitates unit testing
|
||||
|
||||
**File Organization:**
|
||||
- `types.ts` - Single source of truth for interfaces
|
||||
- `hooks/` - Reusable logic hooks
|
||||
- Component files - UI-only, minimal logic
|
||||
- `Game.tsx` - Orchestration and layout only
|
||||
|
||||
## Current File Sizes
|
||||
|
||||
```
|
||||
Original:
|
||||
└── Game.tsx (3,315 lines)
|
||||
|
||||
Refactored:
|
||||
├── types.ts (89 lines)
|
||||
├── hooks/useGameEngine.ts (~600 lines, with placeholders)
|
||||
├── MovementControls.tsx (168 lines)
|
||||
└── Game.tsx (pending refactor)
|
||||
|
||||
To Create:
|
||||
├── CombatView.tsx (~400-500 lines)
|
||||
├── LocationView.tsx (~800-1000 lines)
|
||||
├── PlayerSidebar.tsx (~600-800 lines)
|
||||
└── Workbench.tsx (~400-500 lines)
|
||||
```
|
||||
|
||||
## Next Immediate Steps
|
||||
|
||||
1. Complete all handler implementations in `useGameEngine.ts` (currently placeholders)
|
||||
2. Extract combat UI to `CombatView.tsx`
|
||||
3. Extract location UI to `LocationView.tsx`
|
||||
4. Extract inventory UI to `PlayerSidebar.tsx`
|
||||
5. Extract crafting UI to `Workbench.tsx`
|
||||
6. Refactor main `Game.tsx` to use all components
|
||||
7. Test thoroughly
|
||||
|
||||
---
|
||||
|
||||
**Note:** The refactoring foundation is complete. The remaining work is to extract the JSX sections from the original Game.tsx into the respective component files, following the interfaces defined above.
|
||||
Reference in New Issue
Block a user