Files
echoes-of-the-ash/pwa/GAME_REFACTORING_GUIDE.md
2025-11-27 16:27:01 +01:00

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, 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:

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.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.