All files / web/src/arcade-games/know-your-world/components PlayingPhase.tsx

84.73% Statements 161/190
75% Branches 15/20
33.33% Functions 1/3
84.73% Lines 161/190

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 1911x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x     12x 12x 12x 12x 12x         12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 10x 2x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x       12x 12x 12x 12x 12x 12x 12x 12x         12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x   12x 12x 12x 12x                               12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x  
'use client'
 
import { css } from '@styled/css'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useGameMode, useUserId } from '@/lib/arcade/game-sdk'
import { getAssistanceLevel, getFilteredMapDataBySizesSync } from '../maps'
import { CROP_UPDATE_EVENT, CROP_MODE_EVENT, type CropModeEventDetail } from '../customCrops'
import { useKnowYourWorld } from '../Provider'
import { GameInfoPanel } from './GameInfoPanel'
import { MapRenderer } from './MapRenderer'
 
export function PlayingPhase() {
  const {
    state,
    clickRegion,
    giveUp,
    otherPlayerCursors,
    sendCursorUpdate,
    memberPlayers,
    sharedContainerRef,
  } = useKnowYourWorld()
  const { data: viewerId } = useUserId()
  const { activePlayers, players } = useGameMode()
 
  // Find the local player ID (the player that belongs to this viewer)
  // Look for a player marked as isLocal, or fall back to first active player
  const localPlayerId = useMemo(() => {
    for (const playerId of activePlayers) {
      const player = players.get(playerId)
      if (player?.isLocal) {
        return playerId
      }
    }
    // Fallback: return first active player (shouldn't happen in normal flow)
    return Array.from(activePlayers)[0] || ''
  }, [activePlayers, players])
 
  // Wrap sendCursorUpdate to include localPlayerId and viewerId (session ID)
  const handleCursorUpdate = useCallback(
    (cursorPosition: { x: number; y: number } | null, hoveredRegionId: string | null) => {
      if (viewerId) {
        sendCursorUpdate(localPlayerId, viewerId, cursorPosition, hoveredRegionId)
      }
    },
    [localPlayerId, viewerId, sendCursorUpdate]
  )
 
  const mapData = getFilteredMapDataBySizesSync(
    state.selectedMap,
    state.selectedContinent,
    state.includeSizes
  )
  const totalRegions = mapData.regions.length
  const foundCount = state.regionsFound.length
  const progress = (foundCount / totalRegions) * 100
 
  // Get the display name and ID for the current prompt
  const currentRegion = state.currentPrompt
    ? mapData.regions.find((r) => r.id === state.currentPrompt)
    : null
  const currentRegionName = currentRegion?.name ?? null
  const currentRegionId = currentRegion?.id ?? null
 
  // Check if hints are locked (name confirmation required but not yet done)
  const assistanceConfig = getAssistanceLevel(state.assistanceLevel)
  const requiresNameConfirmation = assistanceConfig.nameConfirmationLetters ?? 0
 
  // Track whether hints have been unlocked for the current region
  const [hintsUnlocked, setHintsUnlocked] = useState(false)
 
  // Counter to force re-render when crop is updated via DevCropTool
  // This ensures getFilteredMapDataBySizesSync is called again with the new runtime crop
  const [cropUpdateCounter, setCropUpdateCounter] = useState(0)
 
  // Track whether crop mode is active (dev only) to hide floating UI
  const [cropModeActive, setCropModeActive] = useState(false)
 
  // Listen for crop update events from DevCropTool
  useEffect(() => {
    const handleCropUpdate = () => {
      console.log('[PlayingPhase] Received crop update event, forcing re-render')
      setCropUpdateCounter((c) => c + 1)
    }
 
    window.addEventListener(CROP_UPDATE_EVENT, handleCropUpdate)
    return () => window.removeEventListener(CROP_UPDATE_EVENT, handleCropUpdate)
  }, [])
 
  // Listen for crop mode state changes to hide floating UI
  useEffect(() => {
    const handleCropModeChange = (e: Event) => {
      const detail = (e as CustomEvent<CropModeEventDetail>).detail
      console.log('[PlayingPhase] Crop mode changed:', detail.active)
      setCropModeActive(detail.active)
    }
 
    window.addEventListener(CROP_MODE_EVENT, handleCropModeChange)
    return () => window.removeEventListener(CROP_MODE_EVENT, handleCropModeChange)
  }, [])
 
  // Reset hints locked state when region changes
  useEffect(() => {
    setHintsUnlocked(false)
  }, [state.currentPrompt])
 
  // Hints are locked if name confirmation is required and not yet unlocked
  const hintsLocked = requiresNameConfirmation > 0 && !hintsUnlocked
 
  // Callback for GameInfoPanel to notify when hints are unlocked
  const handleHintsUnlock = useCallback(() => {
    setHintsUnlocked(true)
  }, [])
 
  // Error if prompt not found in filtered regions (indicates server/client filter mismatch)
  if (state.currentPrompt && !currentRegion) {
    const errorInfo = {
      currentPrompt: state.currentPrompt,
      includeSizes: state.includeSizes,
      selectedMap: state.selectedMap,
      selectedContinent: state.selectedContinent,
      clientFilteredCount: mapData.regions.length,
      serverRegionsToFindCount: state.regionsToFind.length,
      clientRegionIds: mapData.regions.map((r) => r.id).slice(0, 10), // First 10 for debugging
    }
    console.error('[PlayingPhase] CRITICAL: Prompt not in filtered regions!', errorInfo)
    throw new Error(
      `Server/client filter mismatch: prompt "${state.currentPrompt}" not found in client's ${mapData.regions.length} filtered regions. ` +
        `Server has ${state.regionsToFind.length} regions. includeSizes=${JSON.stringify(state.includeSizes)}`
    )
  }
 
  return (
    <div
      ref={sharedContainerRef}
      data-component="playing-phase"
      className={css({
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        overflow: 'hidden',
        zIndex: 0,
      })}
    >
      {/* Full-viewport Map */}
      <MapRenderer
        mapData={mapData}
        regionsFound={state.regionsFound}
        currentPrompt={state.currentPrompt}
        assistanceLevel={state.assistanceLevel}
        selectedMap={state.selectedMap}
        selectedContinent={state.selectedContinent}
        onRegionClick={clickRegion}
        guessHistory={state.guessHistory}
        playerMetadata={state.playerMetadata}
        giveUpReveal={state.giveUpReveal}
        hintActive={state.hintActive ?? null}
        onGiveUp={giveUp}
        gameMode={state.gameMode}
        currentPlayer={state.currentPlayer}
        localPlayerId={localPlayerId}
        otherPlayerCursors={otherPlayerCursors}
        onCursorUpdate={handleCursorUpdate}
        giveUpVotes={state.giveUpVotes}
        activeUserIds={state.activeUserIds}
        viewerId={viewerId ?? undefined}
        memberPlayers={memberPlayers}
        hintsLocked={hintsLocked}
        fillContainer
        difficulty={state.difficulty}
        mapName={mapData.name}
      />
 
      {/* Floating Game Info UI - hidden during crop mode to allow unobstructed dragging */}
      {!cropModeActive && (
        <GameInfoPanel
          mapData={mapData}
          currentRegionName={currentRegionName}
          currentRegionId={currentRegionId}
          selectedMap={state.selectedMap}
          foundCount={foundCount}
          totalRegions={totalRegions}
          progress={progress}
          onHintsUnlock={handleHintsUnlock}
        />
      )}
    </div>
  )
}