update method

ShotStatus update(
  1. DetectedObjects? objects
)

Update the state with new detection data.

Uses two complementary signals:

  1. YOLO action labels: "jump-shot" or "layup" on the player detection indicates a shot attempt is in progress (high confidence signal).
  2. Ball-hoop trajectory: traditional ball position tracking to confirm whether the shot was made or missed.

Implementation

ShotStatus update(DetectedObjects? objects) {
  if (objects == null) return _currentStatus;

  // Track player action from YOLO model
  _lastPlayerAction = objects.player?.action;

  // If no ball or hoop, we can't track the shot trajectory
  if (objects.basketball == null || objects.hoop == null) {
    // But if YOLO detects a shot action, we can still note the attempt
    if (_currentStatus == ShotStatus.none &&
        (_lastPlayerAction == 'jump-shot' || _lastPlayerAction == 'layup')) {
      debugPrint('DEBUG_SHOT: YOLO détecte un tir ($_lastPlayerAction) mais pas de ballon/arceau visible');
    }
    return _currentStatus;
  }

  final ball = objects.basketball!.rect;
  final hoop = objects.hoop!.rect;
  final ballCenter = Point(ball.center.dx, ball.center.dy);

  _ballHistory.add(ballCenter);
  if (_ballHistory.length > _maxHistory) {
    _ballHistory.removeAt(0);
  }

  // Shot Detection Logic:
  // Signal 1: YOLO detects player-jump-shot or player-layup → strong shot intent
  // Signal 2: Ball is above the hoop → trajectory-based shot detection
  //
  // Both signals can trigger the shot detection pipeline.

  if (_currentStatus == ShotStatus.none) {
    // Check YOLO action signal first (more reliable)
    bool yoloShotDetected = _lastPlayerAction == 'jump-shot' ||
                             _lastPlayerAction == 'layup';

    // Check trajectory signal (ball above hoop)
    bool trajectorySignal = ballCenter.y < hoop.top;

    if (yoloShotDetected || trajectorySignal) {
      _currentStatus = ShotStatus.ballAboveHoop;
      debugPrint('DEBUG_SHOT: Tir détecté! '
          'YOLO=$yoloShotDetected(${_lastPlayerAction ?? "none"}), '
          'Trajectory=$trajectorySignal');
    }
  }

  // If ball center is inside hoop horizontal bounds and passes the rim Y
  if (_currentStatus == ShotStatus.ballAboveHoop) {
    bool isInsideHoopX = ballCenter.x > hoop.left && ballCenter.x < hoop.right;
    bool isPassingHoopY = ballCenter.y > hoop.top && ballCenter.y < hoop.bottom;

    if (isInsideHoopX && isPassingHoopY) {
      _currentStatus = ShotStatus.ballInHoop;
      debugPrint('DEBUG_SHOT: Ballon DANS l\'arceau !');
    }
  }

  // Completion of the shot
  if (_currentStatus == ShotStatus.ballInHoop) {
    // If it goes below the hoop after being inside
    if (ballCenter.y > hoop.bottom) {
      _currentStatus = ShotStatus.made;
      _score++;
      _attempts++;
      debugPrint('DEBUG_SHOT: PANIER RÉUSSI ! Score: $_score/$_attempts');

      // Reset status after a short delay or next frame
      Future.delayed(const Duration(seconds: 1), () {
        _currentStatus = ShotStatus.none;
      });
      return ShotStatus.made;
    }
  }

  return _currentStatus;
}