Skip to content

Dev tazi 007#9

Merged
swoosh1337 merged 7 commits into
mainfrom
dev-tazi-007
Feb 11, 2026
Merged

Dev tazi 007#9
swoosh1337 merged 7 commits into
mainfrom
dev-tazi-007

Conversation

@swoosh1337

@swoosh1337 swoosh1337 commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Full possession game mode with momentum, halftime tactics, shot-on-goal and penalty shootout flows.
    • New in-match HUDs, pitch & penalty visualizations, question panels, overlays and end-of-match screens.
    • Real-time match state sync and analytics instrumentation.
    • Integrated in-game sound effects with mute/volume controls, preloading, and a sound toggle.
  • Documentation

    • Added downloadable sound assets listing.

Install @vercel/analytics and add <Analytics /> component to root layout
for tracking page views and user behavior in production.
…ic, dedicated UI components, and game sound effects.
…and enhance game logic and timer management.
…ndling, prop types, and unicode support in game components.
@vercel

vercel Bot commented Feb 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quizball-web Ready Ready Preview, Comment Feb 11, 2026 2:56am

Request Review

@coderabbitai

coderabbitai Bot commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds a possession-based realtime match mode (types, store, hooks, UI, visuals, shot/penalty flows), a Howler-based sound system and useGameSounds hook, Vercel Analytics in layout, a development quick-match page and dev overlays, realtime socket type/handler extensions, and removes the legacy PossessionMatchDemo. Developer tooling and many new possession UI components/hooks/stores were added.

Changes

Cohort / File(s) Summary
Deps & Layout
package.json, src/app/layout.tsx
Added @vercel/analytics, howler, @types/howler; integrated <Analytics /> into root layout.
Sounds
src/lib/sounds/*, src/lib/sounds/index.ts, public/sounds/DOWNLOAD.md
New Howler-based sound manager (gameSounds), useGameSounds hook, preload/unload, mute/volume API, and download docs for SFX/music.
Realtime types & handlers
src/lib/realtime/socket.types.ts, src/lib/realtime/socket-handlers.ts
Added MatchEngine/MatchStatePayload and phase metadata; new server event match:state; new client events match:tactic_select, dev:quick_match, dev:skip_to; extended payload fields.
Realtime store
src/stores/realtimeMatch.store.ts
Added engine, mySeat, possessionState and setMatchState to realtime store.
Possession core types & store
src/features/possession/types/possession.types.ts, src/stores/possessionMatch.store.ts
Defined possession phases/types/constants and a comprehensive Zustand possession match store with state/actions.
Possession logic hooks
src/features/possession/hooks/*
New hooks: usePossessionGameLogic, usePossessionMovement, useShotOnGoal — full game loop, movement/shot calculations, timers, question selection, dev shortcuts.
Question data
src/features/possession/data/mockQuestions.ts
Added mock question pools and selection utilities with non-repeating selection.
Realtime & Local Match Screens
src/features/possession/RealtimePossessionMatchScreen.tsx, src/features/possession/PossessionMatchScreen.tsx, src/app/(fullscreen)/dev/match/page.tsx
New realtime and local possession match screens wired to hooks, stores, socket interactions; dev-only quick-match page and placeholder.
Possession UI components
src/features/possession/components/*
Large set of new components: PitchVisualization (+types), HUDs (Possession, Shot, Penalty), Overlays (Shot/ShotOverlay/PenaltyOverlay/PenaltyTransition/Halftime/Intro/Pregame), PenaltyField/Camera views, QuestionPanel, Feed updates, DevToolbar/DevOverlay, and others; several component prop/interface changes.
Game router & logic updates
src/features/game/GameStageRouter.tsx, src/features/game/hooks/useRealtimeGameLogic.ts
Routed possession_v1 engine to new realtime screen; avatar URL resolution standardized; roundResolved logic simplified.
Dev UX & Mode selection
src/features/play/ModeSelectionScreen.tsx, src/app/test/1/page.tsx
Dev quick-match link on mode card; test page now redirects to /play; replaced legacy demo usage.
Possession demo removal
src/features/possession/PossessionMatchDemo.tsx
Removed large legacy PossessionMatchDemo component.
Pitch/Feed/HUD adjustments
src/features/possession/components/PitchVisualization.tsx, src/features/possession/components/PossessionFeed.tsx, src/features/possession/components/PossessionHUD.tsx
Refactored pitch and markers, added shot/penalty visuals, added pitch-related types/props; PossessionFeed and PossessionHUD signatures/props changed (feed side/penaltyResult; onQuit instead of momentum).
Stores & State
src/stores/possessionMatch.store.ts, src/stores/realtimeMatch.store.ts
New possession match store with full state/actions; realtime store extended to hold possessionState and match engine/seat.
Small import changes
multiple files (framer-motion → motion/react)
Replaced several framer-motion imports with motion/react.
Misc UI fixes
src/features/home/components/dashboard/HomeRecentMatches.tsx, src/features/friend/FriendMatchHubPage.tsx
Added recent matches error handling and friend match join/cleanup/timing refinements.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(220,240,255,0.5)
    participant Player
    participant UI as RealtimePossessionMatchScreen
    participant Logic as usePossessionGameLogic
    participant Store as PossessionMatchStore
    participant Socket as ClientSocket
    participant Server
    end

    Player->>UI: start / select answer / select tactic
    UI->>Logic: handleAnswer / handleTacticSelected
    Logic->>Store: update answer states, phase, scores
    Logic->>Socket: emit match events (answer, tactic, dev events)
    Socket->>Server: send events
    Server->>Socket: broadcast match:state
    Socket->>Store: setMatchState(payload)
    Store->>UI: state updates (phase, question, opponent)
    UI->>Player: render HUD/overlays/feedback
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Dev tazi 007 #9 — Appears to contain the same frontend changes (package deps and possession feature additions).
  • Dev tazi 010 quizball-backend#8 — Backend wiring for possession_v1 engine, match:state payload and dev events that align with these frontend socket typings.
  • Dev tazi 006 #8 — Overlapping edits to possession gameplay components and types; likely to conflict on same files.

Poem

🐰 Hoppity-hop, the match springs loud and bright,

New sounds, new goals, and a pitch full of light.
Dev toggles twitch, analytics take flight,
I nibble the code and cheer into the night.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Dev tazi 007' is vague and does not meaningfully convey the changes in this large, multi-feature pull request. Use a descriptive title that summarizes the main change, such as 'Add possession match game mode with real-time gameplay' or 'Implement possession engine with phase-based game flow'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev-tazi-007

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In `@package.json`:
- Line 53: Replace all imports that reference "framer-motion" with imports from
"motion/react" (e.g., change import { motion, AnimatePresence, useAnimation }
from "framer-motion" to import { motion, AnimatePresence, useAnimation } from
"motion/react"); search codebase for any "from 'framer-motion'" occurrences and
update them, verify any default imports are converted to named imports if
needed, and then remove the "framer-motion" entry from package.json dependencies
to avoid duplicate bundling; run a typecheck/build to catch any leftover
references or typing changes and update imports accordingly.

In `@src/features/dev/DevOverlay.tsx`:
- Around line 46-48: The current Row entries access nested properties directly
(ps.seatMomentum.seat1, ps.goals.seat1, ps.penaltyGoals.seat1) after only
checking ps; update the rendering in DevOverlay so each nested object is safely
accessed (e.g. use optional chaining or explicit null checks for
ps.seatMomentum, ps.goals, ps.penaltyGoals) and provide a sensible fallback
string when any nested value is missing; target the three Row lines that
reference ps.seatMomentum, ps.goals, and ps.penaltyGoals and ensure you
reference seat1 and seat2 via safe access (ps?.seatMomentum?.seat1, etc.) or
equivalent guards.
- Around line 7-12: The component DevOverlay calls hooks (useState and
useRealtimeMatchStore) before conditionally returning based on
process.env.NODE_ENV, violating hook ordering safety; fix by moving the NODE_ENV
check to the top of the DevOverlay function so it returns null immediately when
not in development, and only call useState and useRealtimeMatchStore after that
guard (i.e., place the environment check before any calls to useState and
useRealtimeMatchStore to ensure hooks are only invoked when the component
proceeds).

In `@src/features/game/GameStageRouter.tsx`:
- Around line 47-48: Replace the hardcoded POSSESSION_TOTAL_QUESTIONS and
CLASSIC_TOTAL_QUESTIONS with a dynamic source: when handling incoming
MatchQuestionPayload (e.g., in the GameStageRouter's
onMatchQuestion/handleMatchQuestion code), capture and store payload.total as
the canonical question count for that match and use that stored value when
computing accuracy (instead of the constants); alternatively, if you control
server responses, add a total field to MatchFinalResultsPayload and read that in
GameStageRouter to compute final accuracy. Ensure references to
POSSESSION_TOTAL_QUESTIONS/CLASSIC_TOTAL_QUESTIONS in accuracy calculation are
replaced with the stored dynamic total (or final results total) so the client
always matches server question counts.

In `@src/features/possession/components/HalftimeScreen.tsx`:
- Around line 133-168: showHint is created with useState but never updated, so
its value doesn't reflect localStorage changes on mounts; change the declaration
to const [showHint, setShowHint] = useState(...) and inside the useEffect that
runs on visible change (the existing useEffect referencing visible, timerRef,
hasFiredRef) compute the current hint value (checking typeof window !==
'undefined' && !localStorage.getItem('halftime-hint-seen')) and call
setShowHint(...) when visible becomes true so the component respects the
one-time hint per session; keep the existing
localStorage.setItem('halftime-hint-seen','true') only when you actually show
the hint.

In `@src/features/possession/components/PenaltyCameraView.tsx`:
- Around line 52-56: The overlay motion.div in PenaltyCameraView uses separate
scale and x props (animate={{ scale: 2.5, x: '-35%' }}), which applies scale
before translate and causes misalignment with the pitch's translate+scale;
replace those with a single transform string that applies translate first then
scale (e.g. animate={{ transform: 'translateX(-35%) scale(2.5)' }} and initial
with matching order) so the transform order matches the pitch; make the same
change for the other overlay motion.div instance (the one around lines 87-91) so
both overlays use transform strings with translate then scale.

In `@src/features/possession/components/PenaltyOverlay.tsx`:
- Line 29: PenaltyOverlayProps declares result as 'pending' | 'goal' | 'saved' |
null but PenaltyCameraView only handles 'goal' | 'saved' | null, so filter or
remap 'pending' before passing to PenaltyCameraView: in the PenaltyOverlay
component (where you pass result into <PenaltyCameraView ... />) convert result
=== 'pending' to null (or omit the prop) so PenaltyCameraView only ever receives
'goal' | 'saved' | null; alternatively tighten the type on PenaltyOverlayProps
to match PenaltyCameraView if 'pending' should never be forwarded.

In `@src/features/possession/components/PenaltyTransition.tsx`:
- Around line 59-63: In PenaltyTransition.tsx the status line is hardcoded to
"Match Tied" regardless of the scores; update the JSX around the <div
className="text-gray-400..."> that currently renders "Match Tied" to render that
text only when playerGoals === opponentGoals and otherwise render an appropriate
status (e.g., show a different label or nothing). Use a conditional expression
based on the playerGoals and opponentGoals variables in the PenaltyTransition
component to control the rendered string so the UI reflects the actual score.

In `@src/features/possession/hooks/usePossessionMovement.ts`:
- Around line 70-85: In usePossessionMovement, the "Both wrong" return sets
posDelta: -2 but direction: 'neutral', causing UI mismatch; change the return to
use direction: 'backward' (or compute direction based on posDelta < 0) so the
direction aligns with the negative posDelta; update the object returned in the
both-wrong branch (the literal with posDelta: -2, momDelta: 0, message: 'Both
wrong → -2', direction: 'neutral') to use direction 'backward' (or derive
direction from posDelta) so arrows/animations reflect the backward movement.

In `@src/features/possession/hooks/useShotOnGoal.ts`:
- Around line 34-41: The bug is that when usedQuestionIdsRef.current is null you
create a new Set in usedIds but never persist it back, allowing repeats; inside
initializeShot (in useShotOnGoal.ts) ensure that after const usedIds =
usedQuestionIdsRef.current ?? new Set<string>() you assign
usedQuestionIdsRef.current = usedIds when it was previously null so the Set is
stored for subsequent calls before calling pickQuestion(HARD_QUESTIONS,
usedIds); this guarantees usedQuestionIdsRef retains chosen IDs across shots.

In `@src/features/possession/PossessionMatchScreen.tsx`:
- Around line 85-91: The button currently calls both g.toggleMute() and
usePossessionMatchStore.getState().setMuted(!g.muted), risking stale reads of
g.muted; change this so the mute value is computed once and used for both or,
better, let g.toggleMute() be the single source of truth that also updates the
store internally. Specifically, either (A) compute newMuted = !g.muted and then
call usePossessionMatchStore.getState().setMuted(newMuted) and g.toggleMute()
(or await g.toggleMute() if it returns a promise) so both use the same value, or
(B) refactor g.toggleMute() to call
usePossessionMatchStore.getState().setMuted(...) itself and remove the direct
store call from the button. Ensure references: g.toggleMute,
usePossessionMatchStore.getState().setMuted, and g.muted are updated
accordingly.
🧹 Nitpick comments (14)
src/features/home/components/dashboard/HomeRecentMatches.tsx (1)

66-69: Add ARIA alert semantics to the error banner.

This improves screen-reader announcement for failed loads.

✅ Suggested tweak
-          {!isLoading && error && (
-            <div className="p-3.5 rounded-xl bg-red-500/10 border border-red-500/20 text-sm font-semibold text-red-500">
+          {!isLoading && error && (
+            <div
+              role="alert"
+              aria-live="polite"
+              className="p-3.5 rounded-xl bg-red-500/10 border border-red-500/20 text-sm font-semibold text-red-500"
+            >
               Failed to load recent matches. Please try again later.
             </div>
           )}
src/features/possession/components/PenaltyOverlay.tsx (1)

71-84: Consider extracting renderScorePips outside the component.

This helper creates new array elements on every render. For better performance, consider moving it outside the component or wrapping with useCallback. This is a minor optimization since the arrays are small.

src/features/possession/components/ShotOverlay.tsx (1)

41-47: Use CONFETTI_COLORS.length for modulo.
Avoids drift if the palette changes.

♻️ Suggested tweak
-        colorIndex: i % 6,
+        colorIndex: i % CONFETTI_COLORS.length,
src/lib/sounds/useGameSounds.ts (1)

3-23: useRef guard won’t prevent Strict Mode remount preloads.
React Strict Mode remounts components (resetting refs), so preloadAll can still run twice in dev. If you’re trying to avoid duplicate preloads, consider a module-level guard or ensure preloadAll is idempotent.

♻️ One way to make the guard effective
-import { useEffect, useRef } from 'react';
+import { useEffect } from 'react';
 import {
   playSfx,
   toggleMute,
   isMuted,
   preloadAll,
 } from './gameSounds';
 
+let hasPreloaded = false;
+
 export function useGameSounds() {
-  const initialized = useRef(false);
-
   useEffect(() => {
-    if (!initialized.current) {
-      preloadAll();
-      initialized.current = true;
-    }
+    if (!hasPreloaded) {
+      preloadAll();
+      hasPreloaded = true;
+    }
   }, []);
src/stores/possessionMatch.store.ts (1)

150-206: resetMatch should rebuild fresh nested state to avoid shared references.
Line 287 uses a shallow copy of initialState, which reuses nested arrays/objects if they were mutated elsewhere. Consider a small factory function so each reset gets fresh nested state.

♻️ Suggested refactor
-const initialState: PossessionMatchState = {
+const createInitialState = (): PossessionMatchState => ({
   phase: 'intro',
   half: 1,
   normalQuestionsInHalf: 0,
   tactic: null,
@@
   penaltyAnswerStates: [...DEFAULT_ANSWER_STATES],
   penaltyResult: null,
   penaltyShowOptions: false,
   penaltyFieldPhase: 'setup',
-};
+});
+
+const initialState = createInitialState();
@@
-  resetMatch: () => set({ ...initialState }),
+  resetMatch: () => set(createInitialState()),
 }));

Also applies to: 287-287

src/features/possession/components/PenaltyTransition.tsx (1)

14-140: Inconsistent indentation within AnimatePresence block.

The motion.div on line 15 and its children have inconsistent indentation relative to the {visible && ( condition. This affects readability.

src/features/game/GameStageRouter.tsx (1)

235-305: Significant duplication in quit/forfeit handlers.

The onQuit and onForfeit handlers for RealtimePossessionMatchScreen (lines 242-267) and RealtimeQuizBallGameScreen (lines 278-304) are nearly identical. Consider extracting these into shared callbacks.

Proposed refactor
+  const handleMatchLeave = useCallback(() => {
+    if (realtimeMatch?.matchId) {
+      getSocket().emit("match:leave", { matchId: realtimeMatch.matchId });
+      logger.info("Socket emit match:leave", { matchId: realtimeMatch.matchId });
+    } else {
+      logger.info("Socket emit match:leave skipped (missing matchId)");
+    }
+    exitToPlay();
+  }, [realtimeMatch?.matchId, exitToPlay]);
+
+  const handleMatchForfeit = useCallback(() => {
+    if (realtimeMatch?.matchId) {
+      getSocket().emit("match:forfeit", { matchId: realtimeMatch.matchId });
+      logger.info("Socket emit match:forfeit", { matchId: realtimeMatch.matchId });
+    } else {
+      logger.info("Socket emit match:forfeit skipped (missing matchId)");
+    }
+    exitToPlay();
+  }, [realtimeMatch?.matchId, exitToPlay]);

Then use onQuit={handleMatchLeave} and onForfeit={handleMatchForfeit} for both screens.

src/features/possession/components/PenaltyHUD.tsx (1)

54-56: Hardcoded player names "You" and "CPU" limit reusability.

The component receives playerAvatarUrl and opponentAvatarUrl but hardcodes the display names as "You" and "CPU". For multiplayer scenarios where the opponent is a real player, consider accepting playerName and opponentName props similar to PossessionHUD.

Proposed interface extension
 interface PenaltyHUDProps {
   penaltyPlayerScore: number;
   penaltyOpponentScore: number;
   penaltyRound: number;
   isPenaltySuddenDeath: boolean;
   isPlayerShooter: boolean;
+  playerName: string;
+  opponentName: string;
   playerAvatarUrl: string;
   opponentAvatarUrl: string;
   timeRemaining: number;
   phase: Phase;
   onQuit?: () => void;
 }

Also applies to: 79-81

src/features/possession/components/PenaltyFieldView.tsx (1)

26-27: Fixed overlay with z-50 may conflict with modals or other overlays.

The component uses fixed inset-0 z-50 which will overlay the entire viewport. If any modals, toasts, or other overlays use similar z-index values, there could be stacking conflicts.

src/features/possession/PossessionMatchScreen.tsx (1)

57-79: Consider using useMemo for computed feed values.

The IIFE pattern for feedSide and feedPenaltyResult works but is not idiomatic React. Using useMemo would make the dependencies explicit and allow React to optimize re-computation.

Proposed refactor
-  const feedSide = (() => {
-    if (g.isPenaltyPhase && g.phase === 'penalty-result') {
-      if (g.penaltyResult === 'goal') return g.isPlayerShooter ? 'left' as const : 'right' as const;
-      if (g.penaltyResult === 'saved') return g.isPlayerShooter ? 'right' as const : 'left' as const;
-    }
-    // ... rest
-    return 'left' as const;
-  })();
+  const feedSide = useMemo(() => {
+    if (g.isPenaltyPhase && g.phase === 'penalty-result') {
+      if (g.penaltyResult === 'goal') return 'left' as const;
+      if (g.penaltyResult === 'saved') return g.isPlayerShooter ? 'right' as const : 'left' as const;
+    }
+    // ... rest
+    return 'left' as const;
+  }, [g.isPenaltyPhase, g.phase, g.penaltyResult, g.isPlayerShooter, g.isShotPhase, g.shotResult]);
src/features/possession/components/PitchVisualization.tsx (1)

241-277: Consider scoping SVG defs IDs per instance.
Fixed IDs (e.g., pitchGrad, blueGlow, penNet) can collide if multiple PitchVisualization components render simultaneously, breaking filters/clipPaths. A per‑instance prefix (prop or generated) would avoid conflicts.

src/features/possession/components/HalftimeScreen.tsx (1)

383-408: Use a button for the info icon to keep it keyboard-accessible.
The clickable div isn’t focusable, which blocks keyboard users from opening the tooltip.

♿️ Suggested fix
-                      <div
-                        className="relative"
-                        onClick={(e) => {
-                          e.stopPropagation();
-                          setHoveredTactic(hoveredTactic === tactic.id ? null : tactic.id);
-                        }}
-                      >
+                      <button
+                        type="button"
+                        className="relative"
+                        aria-label={`More info: ${tactic.name}`}
+                        onClick={(e) => {
+                          e.stopPropagation();
+                          setHoveredTactic(hoveredTactic === tactic.id ? null : tactic.id);
+                        }}
+                      >
                         <Info
                           className="size-3.5 opacity-30 hover:opacity-60 transition-opacity cursor-pointer"
                           style={{ color: isSelected ? tactic.color : '#56707A' }}
                           strokeWidth={2}
                         />
                         {hoveredTactic === tactic.id && (
                           <motion.div
                             initial={{ opacity: 0, y: -4 }}
                             animate={{ opacity: 1, y: 0 }}
                             className="absolute top-5 right-0 w-44 p-2 rounded-lg bg-black/90 backdrop-blur-sm border border-white/15 shadow-xl z-50"
                           >
                             <div className="text-[10px] text-white/90 leading-relaxed">
                               {tactic.tooltip}
                             </div>
                           </motion.div>
                         )}
-                      </div>
+                      </button>
src/features/possession/types/possession.types.ts (1)

1-1: Avoid duplicating TacticalCard across modules.
This union also exists in src/lib/realtime/socket.types.ts; keeping a single source prevents drift and accidental mismatches.

♻️ Possible consolidation
-export type TacticalCard = 'press-high' | 'play-safe' | 'all-in';
+export type { TacticalCard } from '@/lib/realtime/socket.types';
src/features/possession/hooks/usePossessionMovement.ts (1)

29-46: Add a defensive default in getTacticModifiers.
If an unexpected tactic slips in at runtime, the function currently returns undefined, which will crash consumers.

🛡️ Suggested fix
   switch (tactic) {
     case 'press-high':
       return { correctVsWrongGain: 12, wrongVsCorrectPenalty: -12, speedBonusMultiplier: 1.25, shotMomentumThreshold: 4 };
     case 'play-safe':
       return { correctVsWrongGain: 9, wrongVsCorrectPenalty: -8, speedBonusMultiplier: 1, shotMomentumThreshold: 4 };
     case 'all-in':
       return { correctVsWrongGain: 14, wrongVsCorrectPenalty: -14, speedBonusMultiplier: 1, shotMomentumThreshold: 3 };
+    default:
+      return defaults;
   }

Comment thread package.json Outdated
Comment thread src/features/dev/DevOverlay.tsx
Comment thread src/features/dev/DevOverlay.tsx Outdated
Comment thread src/features/game/GameStageRouter.tsx Outdated
Comment thread src/features/possession/components/HalftimeScreen.tsx
Comment thread src/features/possession/components/PenaltyOverlay.tsx
Comment thread src/features/possession/components/PenaltyTransition.tsx
Comment thread src/features/possession/hooks/usePossessionMovement.ts
Comment thread src/features/possession/hooks/useShotOnGoal.ts
Comment thread src/features/possession/PossessionMatchScreen.tsx
…st game question logic, and refine various UI components and animations.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/features/possession/PossessionMatchScreen.tsx`:
- Around line 178-194: Pass the phase-specific opponent answer into the
PossessionQuestionPanel instead of always g.opponentAnswer: update the
opponentAnswer prop on the PossessionQuestionPanel (where
onAnswer={activeHandler} is set) to choose g.penaltyOpponentAnswer when
g.isPenaltyPhase, g.shotOpponentAnswer when g.isShotPhase, otherwise
g.opponentAnswer so the shot-phase reveal uses the shot-specific data; reference
the PossessionQuestionPanel prop opponentAnswer and the flags g.isPenaltyPhase /
g.isShotPhase and fields g.penaltyOpponentAnswer / g.shotOpponentAnswer to
implement the conditional selection.
🧹 Nitpick comments (9)
src/features/possession/hooks/useShotOnGoal.ts (2)

10-16: Type parameter for usedQuestionIdsRef should include null.

The type React.RefObject<Set<string>> implies current is never null, but lines 39-40 handle the null case with nullish coalescing and a guard. Update the type to match actual usage.

🔧 Suggested fix
 export function useShotOnGoal(
-  usedQuestionIdsRef: React.RefObject<Set<string>>,
+  usedQuestionIdsRef: React.RefObject<Set<string> | null>,
   advanceToNextQuestion: (position: number) => void,
 ) {

53-60: Consider extracting AI behavior constants.

The magic numbers for AI defender behavior (delay range, correctness probability) could be extracted to named constants for clarity and easier tuning.

🔧 Suggested constants
// In possession.types.ts or at top of this file
const AI_MIN_DELAY_SEC = 2;
const AI_MAX_DELAY_SEC = 8;
const AI_CORRECT_PROBABILITY = 0.55;
-    const aiDelay = 2 + Math.random() * 6;
-    const isCorrect = Math.random() < 0.55;
+    const aiDelay = AI_MIN_DELAY_SEC + Math.random() * (AI_MAX_DELAY_SEC - AI_MIN_DELAY_SEC);
+    const isCorrect = Math.random() < AI_CORRECT_PROBABILITY;
src/features/game/GameStageRouter.tsx (1)

235-305: Consider extracting shared quit/forfeit handlers to reduce duplication.

The onQuit and onForfeit callbacks are nearly identical between RealtimePossessionMatchScreen (lines 242-267) and RealtimeQuizBallGameScreen (lines 278-303). Extracting these into shared callbacks would reduce duplication and simplify maintenance.

♻️ Proposed refactor
+  const handleMatchQuit = useCallback(() => {
+    if (realtimeMatch?.matchId) {
+      getSocket().emit("match:leave", {
+        matchId: realtimeMatch.matchId,
+      });
+      logger.info("Socket emit match:leave", {
+        matchId: realtimeMatch.matchId,
+      });
+    } else {
+      logger.info("Socket emit match:leave skipped (missing matchId)");
+    }
+    exitToPlay();
+  }, [realtimeMatch?.matchId, exitToPlay]);
+
+  const handleMatchForfeit = useCallback(() => {
+    if (realtimeMatch?.matchId) {
+      getSocket().emit("match:forfeit", {
+        matchId: realtimeMatch.matchId,
+      });
+      logger.info("Socket emit match:forfeit", {
+        matchId: realtimeMatch.matchId,
+      });
+    } else {
+      logger.info("Socket emit match:forfeit skipped (missing matchId)");
+    }
+    exitToPlay();
+  }, [realtimeMatch?.matchId, exitToPlay]);

Then use these in both screen components:

<RealtimePossessionMatchScreen
  ...
  onQuit={handleMatchQuit}
  onForfeit={handleMatchForfeit}
/>
src/features/possession/hooks/usePossessionMovement.ts (1)

11-16: Consider extracting randomness for testability.

getDifficultyForZone uses Math.random() directly, making unit tests non-deterministic. Consider accepting an optional random function parameter or using a seeded generator for testing scenarios.

♻️ Proposed refactor
-export function getDifficultyForZone(position: number): 'easy' | 'medium' | 'hard' {
-  if (position >= 71) return Math.random() < 0.5 ? 'medium' : 'hard';
+export function getDifficultyForZone(position: number, rand: () => number = Math.random): 'easy' | 'medium' | 'hard' {
+  if (position >= 71) return rand() < 0.5 ? 'medium' : 'hard';
   if (position >= 46) return 'medium';
-  if (position >= 21) return Math.random() < 0.5 ? 'easy' : 'medium';
+  if (position >= 21) return rand() < 0.5 ? 'easy' : 'medium';
   return 'easy';
 }
src/features/possession/PossessionMatchScreen.tsx (2)

58-79: Consider consolidating feedSide and feedPenaltyResult logic.

Both IIFEs share similar phase-checking patterns. A small helper or combined derivation could reduce duplication and make the relationship between side and result clearer.


209-210: Hardcoded ready states may cause UI inconsistency.

myReady={false} and opponentReady={true} are hardcoded. If HalftimeScreen displays ready indicators, this will always show the player as not ready and opponent as ready, which may not reflect actual game state in future multiplayer scenarios.

src/features/possession/components/PenaltyOverlay.tsx (2)

221-226: Hardcoded difficulty may not reflect actual question difficulty.

difficulty="Hard" is always passed to QuestionArena, but penalty questions may have varying difficulties. Consider passing the actual difficulty from currentQuestion.difficulty if available.

♻️ Proposed fix
               <QuestionArena
                 question={currentQuestion.prompt}
                 category={currentQuestion.categoryName ?? 'Penalty'}
                 categoryIcon="⚽"
-                difficulty="Hard"
+                difficulty={currentQuestion.difficulty ?? 'Hard'}
               />

71-84: Consider memoizing renderScorePips to avoid recreation on each render.

renderScorePips is recreated on every render. Since it depends only on stable values (pipColors, fallbackPip), extracting it outside the component or wrapping with useCallback would be a minor optimization.

src/features/possession/components/PenaltyCameraView.tsx (1)

15-15: Type mismatch: result includes 'pending' but is not handled.

The result prop accepts 'pending' | 'goal' | 'saved' | null, but the component only animates for 'goal' and 'saved'. When result === 'pending', no ball animation plays, which may be intentional, but the type should be tightened or explicitly documented.

Note: The caller (PenaltyOverlay) converts 'pending' to null before passing, so this is currently safe, but the interface is misleading.

♻️ Proposed type fix
 interface PenaltyCameraViewProps {
   // ...
-  result: 'pending' | 'goal' | 'saved' | null;
+  result: 'goal' | 'saved' | null;
   // ...
 }

Comment thread src/features/possession/PossessionMatchScreen.tsx
…shot/penalty results, and document ESLint `exhaustive-deps` warnings.
…`PossessionMatchScreen` to display the correct opponent answer based on the game phase.
@swoosh1337 swoosh1337 merged commit 21efbe1 into main Feb 11, 2026
2 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@src/features/possession/components/PitchVisualization.tsx`:
- Around line 349-369: The SVG clipPath ids are static and collide when multiple
PitchVisualization instances render; update the PitchVisualization component to
generate a unique id using React's useId() (or a similar unique prefix) and pass
scoped clipIds into the PitchMarker props (e.g., `${id}-oppClipInst` and
`${id}-playerClipInst`) everywhere the static "oppClipInst"/"playerClipInst"
strings are used so each PitchVisualization instance gets unique clipPath ids
and avoids cross-instance collisions.

In `@src/features/possession/components/PossessionQuestionPanel.tsx`:
- Around line 127-149: The mapping for option labels uses ANSWER_LABELS but
doesn't guard against questions with more options than ANSWER_LABELS, so label
can be undefined; in the render loop around question.options.map(...) (and the
AnswerCard prop label) add a safe fallback when ANSWER_LABELS[i] is missing
(e.g., generate a label from index such as a letter or numeric fallback like
`${i+1}` or String.fromCharCode(65 + i)) so AnswerCard always receives a defined
label.

In `@src/features/possession/RealtimePossessionMatchScreen.tsx`:
- Around line 417-422: The penalty feed is incorrectly using the live shooter
flag shooterIsMe which can drift; replace shooterIsMe with the round-result flag
resultShooterIsMe when computing the penalty side and any logic tied to the
penalty outcome (the block handling kind === 'penalty' and the similar block at
the other location around line 441). Specifically, compute side using
resultShooterIsMe (e.g., side = penaltyResult === 'goal' ? (resultShooterIsMe ?
'left' : 'right') : (resultShooterIsMe ? 'right' : 'left')) and preserve
feedResult as before so the feed reflects the shooter seat at the time of the
result rather than the current live state.
- Around line 455-464: The mute toggle button in RealtimePossessionMatchScreen
is icon-only and needs an accessible label for screen readers; update the button
(the element that calls toggleMute and setMuted and uses the muted state) to
include an appropriate ARIA attribute such as aria-label (e.g.,
aria-label={muted ? 'Unmute audio' : 'Mute audio'}) and consider adding
aria-pressed={muted} to convey toggle state, keeping the existing title and
click handlers (toggleMute, setMuted) unchanged.
- Around line 35-41: toAnswerStates currently returns DEFAULT_ANSWER_STATES when
selectedAnswer is null which can mismatch optionsCount; instead, when
selectedAnswer === null construct and return a new AnswerStateArray of length
optionsCount filled with the neutral/default state (rather than using
DEFAULT_ANSWER_STATES), e.g. create an array of size optionsCount and fill each
entry with the same default AnswerState value so downstream consumers always
receive an array sized to optionsCount; update the toAnswerStates function to
use optionsCount for the empty-case construction and keep existing behavior for
non-null selectedAnswer/selfAnsweredCorrectly.

Comment thread src/features/possession/components/PitchVisualization.tsx
Comment thread src/features/possession/components/PossessionQuestionPanel.tsx
Comment thread src/features/possession/RealtimePossessionMatchScreen.tsx
Comment thread src/features/possession/RealtimePossessionMatchScreen.tsx
Comment thread src/features/possession/RealtimePossessionMatchScreen.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant