13 KiB
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, inventoryDirectionDetail- Movement directions with costsLocation- Current location dataProfile- Character stats and attributesCombatLogEntry- Combat message formatLocationMessage- Location event messagesEquipment- Equipment slotsCombatState- Combat status (PvE and PvP)WorkbenchTab- Crafting menu tabsMobileMenuState- 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:
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)
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)
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)
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)
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:
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.tsxwith all combat UI logic
Phase 2: Extract Location View
- Second largest section (~800-1000 lines)
- Create
LocationView.tsxwith 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 interfaceshooks/- 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
- Complete all handler implementations in
useGameEngine.ts(currently placeholders) - Extract combat UI to
CombatView.tsx - Extract location UI to
LocationView.tsx - Extract inventory UI to
PlayerSidebar.tsx - Extract crafting UI to
Workbench.tsx - Refactor main
Game.tsxto use all components - 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.