All files / web/src/components/toys/number-line/lcmHopper useLcmHopperParty.ts

0% Statements 0/124
0% Branches 0/1
0% Functions 0/1
0% Lines 0/124

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                                                                                                                                                                                                                                                         
import { useCallback, useRef, useState } from 'react'
import type { MutableRefObject } from 'react'
import {
  pickCombo,
  buildPartyCombo,
  emojiForStride,
  wouldExceedLcmLimit,
} from './lcmComboGenerator'
import { setActiveCombo, clearGuess } from './renderLcmHopperOverlay'
import { DYNAMIC_DEMO_VIEWPORTS } from '../constants/demos/useConstantDemo'
import { NARRATION_CONFIGS } from '../constants/demos/narrationConfigs'
import { buildLcmHopperNarration } from './lcmHopperNarration'
import type { PartyState } from '../primes/PrimeTooltip'

interface UseLcmHopperPartyOptions {
  demoStateRef: MutableRefObject<{ phase: string }>
  narration: { reset: () => void }
  startDemo: (id: string) => void
  cancelDemo: () => void
  onDismissTooltip: () => void
}

export function useLcmHopperParty({
  demoStateRef,
  narration,
  startDemo,
  cancelDemo,
  onDismissTooltip,
}: UseLcmHopperPartyOptions) {
  const lcmRoundRef = useRef(0)
  const [partyInvitees, setPartyInvitees] = useState<number[]>([])
  const partyInviteesRef = useRef<number[]>([])
  partyInviteesRef.current = partyInvitees

  const startLcmHopperDemo = useCallback(() => {
    const combo = pickCombo(lcmRoundRef.current)
    lcmRoundRef.current++
    setActiveCombo(combo)
    clearGuess()
    DYNAMIC_DEMO_VIEWPORTS.set('lcm_hopper', (cssWidth) => {
      const padding = Math.max(2, combo.lcm * 0.1)
      const range = combo.lcm + padding * 2
      const center = combo.lcm / 2
      const pixelsPerUnit = cssWidth / range
      return { center, pixelsPerUnit }
    })
    NARRATION_CONFIGS['lcm_hopper'] = buildLcmHopperNarration(combo)
    narration.reset()
    startDemo('lcm_hopper')
  }, [narration, startDemo])

  const handleToggleInvite = useCallback(
    (value: number) => {
      setPartyInvitees((prev) => {
        if (prev.includes(value)) {
          return prev.filter((v) => v !== value)
        }
        if (prev.length >= 3) return prev
        if (wouldExceedLcmLimit(prev, value)) return prev
        return [...prev, value]
      })
      onDismissTooltip()
    },
    [onDismissTooltip]
  )

  const startHoppingParty = useCallback(() => {
    if (partyInviteesRef.current.length < 2) return
    const combo = buildPartyCombo(partyInviteesRef.current)
    setActiveCombo(combo)
    clearGuess()
    DYNAMIC_DEMO_VIEWPORTS.set('lcm_hopper', (cssWidth) => {
      const padding = Math.max(2, combo.lcm * 0.1)
      const range = combo.lcm + padding * 2
      const center = combo.lcm / 2
      const pixelsPerUnit = cssWidth / range
      return { center, pixelsPerUnit }
    })
    NARRATION_CONFIGS['lcm_hopper'] = buildLcmHopperNarration(combo)
    narration.reset()
    startDemo('lcm_hopper')
  }, [narration, startDemo])

  const handleDismissPartyDemo = useCallback(() => {
    cancelDemo()
    setPartyInvitees([])
  }, [cancelDemo])

  const getPartyState = useCallback(
    (value: number): PartyState | undefined => {
      if (value < 2) return undefined
      const demoPhase = demoStateRef.current.phase
      if (demoPhase !== 'idle') return undefined
      const invited = partyInvitees.includes(value)
      const emoji = emojiForStride(value)
      if (invited) {
        return { invited: true, canInvite: true, emoji }
      }
      if (partyInvitees.length >= 3) {
        return { invited: false, canInvite: false, rejectReason: 'Party full (max 3)', emoji }
      }
      if (partyInvitees.length > 0 && wouldExceedLcmLimit(partyInvitees, value)) {
        return {
          invited: false,
          canInvite: false,
          rejectReason: 'LCM would be too large',
          emoji,
        }
      }
      return { invited: false, canInvite: true, emoji }
    },
    [partyInvitees, demoStateRef]
  )

  return {
    partyInvitees,
    setPartyInvitees,
    startLcmHopperDemo,
    handleToggleInvite,
    startHoppingParty,
    handleDismissPartyDemo,
    getPartyState,
  }
}