<?php
declare(strict_types=1);

function new_manager_watch_match(array &$state, string $user, string $seasonId, int $roundIndex, int $fixtureIndex, bool $register = true): array {
  $season =& $state['managerSeasons'][$seasonId];
  $fixture =& $season['rounds'][$roundIndex]['fixtures'][$fixtureIndex];
  $roundMeta = $season['rounds'][$roundIndex] ?? [];
  $homeId = (string)$fixture['home'];
  $awayId = (string)$fixture['away'];
  $homeData = manager_side_data($state, $season, $homeId);
  $awayData = manager_side_data($state, $season, $awayId);
  manager_lock_fixture_form_for_side($season, $fixture, $homeId, $homeData, $roundIndex, $fixtureIndex);
  manager_lock_fixture_form_for_side($season, $fixture, $awayId, $awayData, $roundIndex, $fixtureIndex);
  $homeData = manager_apply_fixture_form_to_side_data($season, $fixture, $homeId, $homeData, $roundIndex, $fixtureIndex);
  $awayData = manager_apply_fixture_form_to_side_data($season, $fixture, $awayId, $awayData, $roundIndex, $fixtureIndex);
  $homeData = manager_apply_league_form_to_side_data($season, $homeId, $homeData);
  $awayData = manager_apply_league_form_to_side_data($season, $awayId, $awayData);
  $homeData['squad'] = manager_annotate_squad_injuries($homeData['squad'] ?? ['starters' => [], 'bench' => []], $season, $homeId);
  $homeData['squad'] = manager_annotate_squad_training($homeData['squad'], $season, $homeId);
  $awayData['squad'] = manager_annotate_squad_injuries($awayData['squad'] ?? ['starters' => [], 'bench' => []], $season, $awayId);
  $awayData['squad'] = manager_annotate_squad_training($awayData['squad'], $season, $awayId);
  $homeBench = array_slice(array_values(array_filter($homeData['squad']['bench'] ?? [], fn($player) => empty($player['injured']) && empty($player['trainingAway']))), 0, MANAGER_MAX_BENCH_PLAYERS);
  $awayBench = array_slice(array_values(array_filter($awayData['squad']['bench'] ?? [], fn($player) => empty($player['injured']) && empty($player['trainingAway']))), 0, MANAGER_MAX_BENCH_PLAYERS);
  $fixtureHumans = array_flip(manager_fixture_human_users($season, $fixture));
  $homePlayer = isset($fixtureHumans[$homeId]) ? $homeId : ($homeId === $user && $user !== '' ? $user : '__manager_cpu_home__');
  $awayPlayer = isset($fixtureHumans[$awayId]) ? $awayId : ($awayId === $user && $user !== '' ? $user : '__manager_cpu_away__');
  $match = [
    'id' => 'm' . bin2hex(random_bytes(5)),
    'status' => 'ready',
    'created' => time(),
    'updated' => time(),
    'lastTick' => microtime(true),
    'elapsed' => 0,
    'players' => ['home' => $homePlayer, 'away' => $awayPlayer],
    'teams' => ['home' => $homeData['team'], 'away' => $awayData['team']],
    'units' => ['home' => side_squad($homeData['squad']['starters'] ?? [], 'home'), 'away' => side_squad($awayData['squad']['starters'] ?? [], 'away')],
    'bench' => ['home' => side_squad($homeBench, 'home', 11), 'away' => side_squad($awayBench, 'away', 11)],
    'score' => [0, 0],
    'ball' => stopped_kickoff_ball(),
    'tactics' => ['home' => 'balanced', 'away' => 'balanced'],
    'engine' => default_engine((int)$homeData['rating'], (int)$awayData['rating']),
    'events' => [],
    'managerFixture' => [
      'seasonId' => $seasonId,
      'roundIndex' => $roundIndex,
      'fixtureIndex' => $fixtureIndex,
      'home' => $homeId,
      'away' => $awayId,
      'round' => (string)($roundMeta['round'] ?? $fixture['round'] ?? ''),
      'phase' => (string)($fixture['phase'] ?? $roundMeta['phase'] ?? 'league'),
      'playoff' => !empty($fixture['playoff']) || !empty($roundMeta['playoff']),
      'cupName' => (string)($fixture['cupName'] ?? $roundMeta['cupName'] ?? ''),
    ],
  ];
  reset_units($match);
  if (!$register) return $match;
  $fixture['status'] = 'watching';
  $fixture['matchId'] = $match['id'];
  $state['matches'][$match['id']] = $match;
  unset($fixture);
  return $state['matches'][$match['id']];
}

function tactic_power(string $mode): float {
  return match ($mode) {
    'defend' => 0.38,
    'attack' => 0.72,
    default => 0.55,
  };
}

function kickoff_ball(int $direction): array {
  $direction = $direction < 0 ? -1 : 1;
  return [
    'x' => 0.5,
    'y' => 0.5,
    'z' => 0.0,
    'vx' => mt_rand(-45, 45) / 1000,
    'vy' => $direction * (0.13 + mt_rand(0, 45) / 1000),
    'vz' => 0.0,
  ];
}

function stopped_ball_at(float $x, float $y): array {
  return [
    'x' => clamp_float($x, 0.02, 0.98),
    'y' => clamp_float($y, 0.01, 0.99),
    'z' => 0.0,
    'vx' => 0.0,
    'vy' => 0.0,
    'vz' => 0.0,
  ];
}

function stopped_kickoff_ball(): array {
  return stopped_ball_at(0.5, 0.5);
}

function ball_short_line_crossed(array $ball): bool {
  $y = (float)($ball['y'] ?? 0.5);
  return $y < -BALL_SHORT_LINE_OVER_MARGIN || $y > 1.0 + BALL_SHORT_LINE_OVER_MARGIN;
}

function ball_short_line_visible_out(array $ball): bool {
  $y = (float)($ball['y'] ?? 0.5);
  return $y < -BALL_SHORT_LINE_VISIBLE_MARGIN || $y > 1.0 + BALL_SHORT_LINE_VISIBLE_MARGIN;
}

function ball_short_line_stalled_out(array $ball): bool {
  if (!ball_short_line_crossed($ball)) return false;
  $speed = sqrt((float)($ball['vx'] ?? 0.0) ** 2 + (float)($ball['vy'] ?? 0.0) ** 2);
  return $speed < 0.018;
}

function distance2(array $a, array $b): float {
  $dx = (float)$a['x'] - (float)$b['x'];
  $dy = (float)$a['y'] - (float)$b['y'];
  return $dx * $dx + $dy * $dy;
}

function player_touch_control(array $player, bool $highBall = false): float {
  $control = $highBall
    ? player_stat($player, 'heading') * 0.55 + player_stat($player, 'positioning') * 0.25 + player_stat($player, 'strength') * 0.20
    : player_stat($player, 'passcontrol') * 0.48 + player_stat($player, 'shotcontrol') * 0.20 + player_stat($player, 'dribbling') * 0.18 + player_stat($player, 'positioning') * 0.14;
  if (($player['role'] ?? '') === 'GK') $control = max($control, player_stat($player, 'keeping'));
  return $control;
}

function player_touch_radius(array $player, bool $highBall = false): float {
  $isKeeper = ($player['role'] ?? '') === 'GK';
  $base = $isKeeper ? 0.021 : 0.017;
  $max = $isKeeper ? 0.046 : 0.038;
  return clamp_float($base + player_touch_control($player, $highBall) / 100 * 0.020, $base, $max);
}

function ball_touches_player_circle(array $ball, array $player, float $extra = 0.0): bool {
  $radius = player_touch_radius($player, (float)($ball['z'] ?? 0.0) > 0.08) + max(0.0, $extra);
  return distance2($player, $ball) <= $radius * $radius;
}

function player_ball_release_point(array $player, string $side, float $scale = 0.92): array {
  $radius = player_touch_radius($player, false);
  return [
    'x' => clamp_float((float)$player['x'], 0.02, 0.98),
    'y' => clamp_float((float)$player['y'] + attack_dir($side) * $radius * $scale, -0.02, 1.02),
  ];
}

function nearest_index(array $players, array $ball): ?int {
  $best = null;
  $bestDist = PHP_FLOAT_MAX;
  foreach ($players as $index => $player) {
    $dist = distance2($player, $ball);
    if ($dist < $bestDist) {
      $bestDist = $dist;
      $best = $index;
    }
  }
  return $best;
}

function role_zone(string $side, string $role): array {
  $zones = [
    'GK' => [0.88, 0.98],
    'RB' => [0.56, 0.92],
    'LB' => [0.56, 0.92],
    'CB' => [0.58, 0.94],
    'RM' => [0.31, 0.74],
    'LM' => [0.31, 0.74],
    'CM' => [0.30, 0.76],
    'ST' => [0.08, 0.56],
  ];
  [$min, $max] = $zones[$role] ?? [0.24, 0.78];
  if ($side === 'away') return [1.0 - $max, 1.0 - $min];
  return [$min, $max];
}

function role_zone_contains(string $side, string $role, float $y, float $margin = 0.0): bool {
  [$min, $max] = role_zone($side, $role);
  return $y >= $min - $margin && $y <= $max + $margin;
}

function clamp_role_y(string $side, string $role, float $y, float $margin = 0.0): float {
  [$min, $max] = role_zone($side, $role);
  return clamp_float($y, max(0.03, $min - $margin), min(0.97, $max + $margin));
}

function side_half_min(string $side): float {
  return $side === 'home' ? 0.535 : 0.03;
}

function side_half_max(string $side): float {
  return $side === 'home' ? 0.97 : 0.465;
}

function clamp_side_y(string $side, float $y): float {
  return clamp_float($y, side_half_min($side), side_half_max($side));
}

function keep_player_on_side(array &$player, string $side): void {
  $player['y'] = clamp_side_y($side, (float)$player['y']);
}

function nearest_role_index(array $players, array $ball, string $side): ?int {
  $best = null;
  $bestDist = PHP_FLOAT_MAX;
  $ballY = (float)$ball['y'];
  foreach ($players as $index => $player) {
    $role = (string)($player['role'] ?? 'CM');
    $margin = match ($role) {
      'GK' => 0.07,
      'CB', 'RB', 'LB' => 0.10 + player_stat($player, 'speed') / 900,
      'ST' => 0.12 + player_stat($player, 'speed') / 850,
      default => 0.14 + player_stat($player, 'speed') / 800,
    };
    if (!role_zone_contains($side, $role, $ballY, $margin)) continue;
    $dist = distance2($player, $ball);
    if ($dist < $bestDist) {
      $bestDist = $dist;
      $best = $index;
    }
  }
  return $best ?? nearest_index($players, $ball);
}

function move_towards(array &$player, float $targetX, float $targetY, float $speed, float $step): void {
  $dx = $targetX - (float)$player['x'];
  $dy = $targetY - (float)$player['y'];
  $dist = sqrt($dx * $dx + $dy * $dy);
  if ($dist <= 0.0001) {
    $player['vx'] = (float)($player['vx'] ?? 0.0) * 0.70;
    $player['vy'] = (float)($player['vy'] ?? 0.0) * 0.70;
    return;
  }
  if ($dist <= $speed * $step * 1.12) {
    $player['x'] = clamp_float($targetX, 0.03, 0.97);
    $player['y'] = clamp_float($targetY, 0.03, 0.97);
    $player['vx'] = 0.0;
    $player['vy'] = 0.0;
    return;
  }
  $desiredVx = $dx / $dist * $speed;
  $desiredVy = $dy / $dist * $speed;
  $blend = clamp_float(8.0 * $step, 0.12, 0.82);
  $vx = (float)($player['vx'] ?? 0.0);
  $vy = (float)($player['vy'] ?? 0.0);
  $vx += ($desiredVx - $vx) * $blend;
  $vy += ($desiredVy - $vy) * $blend;
  $currentSpeed = sqrt($vx * $vx + $vy * $vy);
  if ($currentSpeed > $speed) {
    $vx *= $speed / $currentSpeed;
    $vy *= $speed / $currentSpeed;
  }
  $move = min($dist, sqrt($vx * $vx + $vy * $vy) * $step);
  $moveDir = sqrt($vx * $vx + $vy * $vy);
  if ($moveDir <= 0.00001) {
    $vx = $dx / $dist * min($speed, $dist / max($step, 0.001));
    $vy = $dy / $dist * min($speed, $dist / max($step, 0.001));
    $moveDir = sqrt($vx * $vx + $vy * $vy);
    $move = min($dist, $moveDir * $step);
  }
  $player['x'] = clamp_float((float)$player['x'] + $vx / $moveDir * $move, 0.03, 0.97);
  $player['y'] = clamp_float((float)$player['y'] + $vy / $moveDir * $move, 0.03, 0.97);
  $damp = pow(0.18, $step);
  $player['vx'] = $vx * $damp;
  $player['vy'] = $vy * $damp;
}

function rand_float(): float {
  return mt_rand() / mt_getrandmax();
}

function player_index_by_id(array $players, ?string $id): ?int {
  if ($id === null) return null;
  foreach ($players as $index => $player) {
    if (($player['id'] ?? null) === $id) return $index;
  }
  return null;
}

function owner_ref(array $match): ?array {
  $owner = $match['engine']['owner'] ?? null;
  if (!is_array($owner)) return null;
  $side = $owner['side'] ?? '';
  if (!in_array($side, ['home', 'away'], true)) return null;
  $index = player_index_by_id($match['units'][$side] ?? [], $owner['id'] ?? null);
  return $index === null ? null : ['side' => $side, 'index' => $index];
}

function set_owner(array &$match, string $side, int $index): void {
  $player = $match['units'][$side][$index];
  $match['engine']['owner'] = ['side' => $side, 'id' => $player['id']];
  $match['engine']['passTarget'] = null;
  $match['engine']['shot'] = null;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.08;
}

function clear_owner(array &$match): void {
  $match['engine']['owner'] = null;
}

function attack_dir(string $side): int {
  return $side === 'home' ? -1 : 1;
}

function opponent_side(string $side): string {
  return $side === 'home' ? 'away' : 'home';
}

function side_team_name(array $match, string $side): string {
  return (string)($match['teams'][$side]['name'] ?? strtoupper($side));
}

function player_name_by_id(array $match, string $side, ?string $id): ?string {
  if ($id === null) return null;
  foreach ($match['units'][$side] ?? [] as $player) {
    if (($player['id'] ?? null) === $id) return (string)($player['name'] ?? $id);
  }
  return null;
}

function goal_scorer_name(array $match, string $scoringSide): ?string {
  $shot = $match['engine']['shot'] ?? null;
  if (is_array($shot) && ($shot['side'] ?? null) === $scoringSide) {
    $name = player_name_by_id($match, $scoringSide, is_string($shot['id'] ?? null) ? $shot['id'] : null);
    if ($name) return $name;
  }
  $lastTouch = (string)($match['engine']['lastTouch'] ?? '');
  if (str_contains($lastTouch, ':')) {
    [$side, $id] = explode(':', $lastTouch, 2);
    if ($side === $scoringSide) {
      $name = player_name_by_id($match, $scoringSide, $id);
      if ($name) return $name;
    }
  }
  return null;
}

function goal_assist_name(array $match, string $scoringSide, ?string $scorer): ?string {
  $shot = $match['engine']['shot'] ?? null;
  if (is_array($shot) && !empty($shot['penalty'])) return null;
  $pass = $match['engine']['lastPass'] ?? null;
  if (is_array($pass) && ($pass['side'] ?? '') === $scoringSide) {
    $at = (float)($pass['at'] ?? -99.0);
    if (((float)($match['elapsed'] ?? 0.0) - $at) <= 12.0) {
      $name = is_string($pass['name'] ?? null) ? (string)$pass['name'] : player_name_by_id($match, $scoringSide, is_string($pass['id'] ?? null) ? $pass['id'] : null);
      if ($name !== '' && ($scorer === null || $name !== $scorer)) return $name;
    }
  }
  $fallback = pick_assist_player($match['units'][$scoringSide] ?? [], ['name' => (string)($scorer ?? '')]);
  $name = (string)($fallback['name'] ?? '');
  if ($name === '' || ($scorer !== null && $name === $scorer)) return null;
  return $name;
}

function place_ball_on_player(array &$ball, array $player, string $side): void {
  $isKeeper = ($player['role'] ?? '') === 'GK';
  $radius = player_touch_radius($player, false);
  $offset = attack_dir($side) * ($isKeeper ? $radius * 0.34 : $radius * 0.72);
  $ball['x'] = clamp_float((float)$player['x'], 0.02, 0.98);
  $ball['y'] = clamp_float((float)$player['y'] + $offset, 0.01, 0.99);
  $ball['z'] = 0.0;
  $ball['vx'] = $isKeeper ? 0.0 : 0.008 * (0.5 - (float)$player['x']);
  $ball['vy'] = $isKeeper ? 0.0 : 0.024 * attack_dir($side);
  $ball['vz'] = 0.0;
}

function kickoff_taker_index(array $match, string $side): int {
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if (($player['role'] ?? '') === 'ST') return (int)$index;
  }
  return min(9, max(0, count($match['units'][$side] ?? []) - 1));
}

function kickoff_receiver_index(array $match, string $side, int $takerIndex): ?int {
  $taker = $match['units'][$side][$takerIndex] ?? null;
  if ($taker) {
    $best = null;
    $bestDist = PHP_FLOAT_MAX;
    foreach ($match['units'][$side] ?? [] as $index => $player) {
      if ($index === $takerIndex || ($player['role'] ?? '') === 'GK') continue;
      $dist = distance2($player, $taker);
      if (in_array($player['role'] ?? '', ['ST', 'CM'], true)) $dist *= 0.92;
      if ($dist < $bestDist) {
        $bestDist = $dist;
        $best = (int)$index;
      }
    }
    if ($best !== null) return $best;
  }
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($index !== $takerIndex && ($player['role'] ?? '') === 'ST') return (int)$index;
  }
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($index !== $takerIndex && ($player['role'] ?? '') === 'CM') return (int)$index;
  }
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($index !== $takerIndex && ($player['role'] ?? '') !== 'GK') return (int)$index;
  }
  return null;
}

function prepare_kickoff_units(array &$match, string $kickingSide): void {
  reset_units($match);
  $dir = attack_dir($kickingSide);
  $taker = kickoff_taker_index($match, $kickingSide);
  $receiver = kickoff_receiver_index($match, $kickingSide, $taker);
  $match['engine']['kickoffSide'] = $kickingSide;
  $match['engine']['kickoffTaker'] = $match['units'][$kickingSide][$taker]['id'] ?? null;
  $match['engine']['kickoffReceiver'] = $receiver !== null ? ($match['units'][$kickingSide][$receiver]['id'] ?? null) : null;
  if (isset($match['units'][$kickingSide][$taker])) {
    $match['units'][$kickingSide][$taker]['x'] = 0.485;
    $match['units'][$kickingSide][$taker]['y'] = 0.5 - $dir * 0.080;
    $match['units'][$kickingSide][$taker]['vx'] = 0.0;
    $match['units'][$kickingSide][$taker]['vy'] = 0.0;
  }
  if ($receiver !== null && isset($match['units'][$kickingSide][$receiver])) {
    $match['units'][$kickingSide][$receiver]['x'] = 0.565;
    $match['units'][$kickingSide][$receiver]['y'] = 0.5 - $dir * 0.070;
    $match['units'][$kickingSide][$receiver]['vx'] = 0.0;
    $match['units'][$kickingSide][$receiver]['vy'] = 0.0;
  }
}

function begin_kickoff(array &$match, string $kickingSide, float $pauseGameMinutes): void {
  ensure_match_engine($match);
  prepare_kickoff_units($match, $kickingSide);
  $match['ball'] = stopped_kickoff_ball();
  $match['engine']['kickoffDirection'] = attack_dir($kickingSide);
  $match['engine']['kickoffUntil'] = (float)$match['elapsed'] + max(0.0, $pauseGameMinutes);
  $match['engine']['kickoffWhistled'] = false;
  $match['engine']['kickoffWhistleUntil'] = 0.0;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + max(0.0, $pauseGameMinutes) + 0.85;
}

function arrange_kickoff_players(array &$match, string $side, float $step): void {
  $takerId = is_string($match['engine']['kickoffTaker'] ?? null) ? $match['engine']['kickoffTaker'] : null;
  $receiverId = is_string($match['engine']['kickoffReceiver'] ?? null) ? $match['engine']['kickoffReceiver'] : null;
  $takerIndex = player_index_by_id($match['units'][$side] ?? [], $takerId) ?? kickoff_taker_index($match, $side);
  $receiverIndex = player_index_by_id($match['units'][$side] ?? [], $receiverId);
  $dir = attack_dir($side);
  if (isset($match['units'][$side][$takerIndex])) {
    $speed = 0.030 + player_stat($match['units'][$side][$takerIndex], 'speed') / 1500;
    move_towards($match['units'][$side][$takerIndex], 0.5, 0.5 - $dir * 0.012, $speed, $step);
  }
  if ($receiverIndex !== null && isset($match['units'][$side][$receiverIndex])) {
    $speed = 0.020 + player_stat($match['units'][$side][$receiverIndex], 'speed') / 2200;
    move_towards($match['units'][$side][$receiverIndex], 0.565, 0.5 - $dir * 0.070, $speed, $step);
  }
}

function kickoff_ball_for_side(array &$match, string $side): array {
  $direction = attack_dir($side);
  $takerId = is_string($match['engine']['kickoffTaker'] ?? null) ? $match['engine']['kickoffTaker'] : null;
  $takerIndex = player_index_by_id($match['units'][$side] ?? [], $takerId) ?? kickoff_taker_index($match, $side);
  $receiverId = is_string($match['engine']['kickoffReceiver'] ?? null) ? $match['engine']['kickoffReceiver'] : null;
  $receiverIndex = player_index_by_id($match['units'][$side] ?? [], $receiverId);
  if ($receiverIndex === null || $receiverIndex === $takerIndex) {
    $receiverIndex = kickoff_receiver_index($match, $side, $takerIndex);
  }
  if (isset($match['units'][$side][$takerIndex])) {
    $match['units'][$side][$takerIndex]['x'] = 0.5;
    $match['units'][$side][$takerIndex]['y'] = 0.5 - $direction * 0.012;
    $match['units'][$side][$takerIndex]['vx'] = 0.0;
    $match['units'][$side][$takerIndex]['vy'] = 0.0;
    $player = $match['units'][$side][$takerIndex];
    $match['engine']['lastTouch'] = $side . ':' . ($player['id'] ?? $takerIndex);
    $match['engine']['lastTouchAt'] = (float)$match['elapsed'];
  }
  if ($receiverIndex !== null && isset($match['units'][$side][$receiverIndex])) {
    $match['units'][$side][$receiverIndex]['x'] = 0.565;
    $match['units'][$side][$receiverIndex]['y'] = 0.5 - $direction * 0.070;
    $match['units'][$side][$receiverIndex]['vx'] = 0.0;
    $match['units'][$side][$receiverIndex]['vy'] = 0.0;
  }
  $match['engine']['kickoffSide'] = null;
  $match['engine']['kickoffTaker'] = null;
  $match['engine']['kickoffReceiver'] = null;
  $match['engine']['kickoffWhistled'] = false;
  $match['engine']['kickoffWhistleUntil'] = 0.0;
  $target = $receiverIndex !== null && isset($match['units'][$side][$receiverIndex])
    ? $match['units'][$side][$receiverIndex]
    : ['x' => 0.565, 'y' => 0.5 - $direction * 0.070, 'id' => null];
  $dx = (float)$target['x'] - 0.5;
  $dy = (float)$target['y'] - (0.5 - $direction * 0.004);
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $speed = clamp_float($dist / 0.42, 0.16, 0.28);
  $match['engine']['passTarget'] = [
    'side' => $side,
    'id' => $target['id'] ?? null,
    'quality' => 0.92,
    'type' => 'kickoff',
  ];
  return [
    'x' => 0.5,
    'y' => 0.5 - $direction * 0.004,
    'z' => 0.0,
    'vx' => $dx / $dist * $speed,
    'vy' => $dy / $dist * $speed,
    'vz' => 0.0,
  ];
}

function last_touch_side(array $match): ?string {
  $lastTouch = (string)($match['engine']['lastTouch'] ?? '');
  if (str_contains($lastTouch, ':')) {
    [$side] = explode(':', $lastTouch, 2);
    if (in_array($side, ['home', 'away'], true)) return $side;
  }
  $shot = $match['engine']['shot'] ?? null;
  if (is_array($shot) && in_array($shot['side'] ?? '', ['home', 'away'], true)) return (string)$shot['side'];
  return null;
}

function restart_kicker_index(array $match, string $side, string $type, array $spot): ?int {
  if ($type === 'goal_kick') {
    foreach ($match['units'][$side] ?? [] as $index => $player) {
      if (($player['role'] ?? '') === 'GK') return (int)$index;
    }
  }
  $best = null;
  $bestDist = PHP_FLOAT_MAX;
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($type === 'corner' && ($player['role'] ?? '') === 'GK') continue;
    $dist = distance2($player, $spot);
    if ($type === 'corner' && in_array($player['role'] ?? '', ['LM', 'RM', 'ST'], true)) $dist *= 0.75;
    if ($dist < $bestDist) {
      $bestDist = $dist;
      $best = (int)$index;
    }
  }
  return $best;
}

function restart_receiver_index(array $match, string $side, string $type, int $kickerIndex): ?int {
  $roles = $type === 'goal_kick' ? ['CB', 'RB', 'LB', 'CM', 'RM', 'LM'] : ['ST', 'CM', 'RM', 'LM', 'CB'];
  foreach ($roles as $role) {
    foreach ($match['units'][$side] ?? [] as $index => $player) {
      if ($index !== $kickerIndex && ($player['role'] ?? '') === $role) return (int)$index;
    }
  }
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if ($index !== $kickerIndex && ($player['role'] ?? '') !== 'GK') return (int)$index;
  }
  return null;
}

function prepare_restart_units(array &$match, string $side, string $type, array $spot): void {
  reset_units($match);
  $kickIndex = restart_kicker_index($match, $side, $type, $spot);
  if ($kickIndex === null || empty($match['units'][$side][$kickIndex])) return;
  $match['engine']['restartKicker'] = $match['units'][$side][$kickIndex]['id'] ?? null;
  $match['units'][$side][$kickIndex]['x'] = clamp_float((float)$spot['x'], 0.02, 0.98);
  $match['units'][$side][$kickIndex]['y'] = clamp_float((float)$spot['y'], 0.02, 0.98);
  $match['units'][$side][$kickIndex]['vx'] = 0.0;
  $match['units'][$side][$kickIndex]['vy'] = 0.0;

  $receiverIndex = restart_receiver_index($match, $side, $type, $kickIndex);
  if ($receiverIndex !== null && isset($match['units'][$side][$receiverIndex])) {
    $dir = attack_dir($side);
    $match['units'][$side][$receiverIndex]['x'] = $type === 'corner' ? 0.50 : 0.42;
    $match['units'][$side][$receiverIndex]['y'] = $type === 'corner'
      ? clamp_float($dir < 0 ? 0.13 : 0.87, 0.04, 0.96)
      : clamp_float((float)$spot['y'] + $dir * 0.20, 0.08, 0.92);
    $match['units'][$side][$receiverIndex]['vx'] = 0.0;
    $match['units'][$side][$receiverIndex]['vy'] = 0.0;
  }
}

function begin_restart(array &$match, string $type, string $side, array $spot, ?array $outBall = null, ?array $lineBall = null): void {
  ensure_match_engine($match);
  clear_owner($match);
  $outBall = $outBall ?: $spot;
  $out = [
    'x' => clamp_float((float)($outBall['x'] ?? $spot['x']), -0.30, 1.30),
    'y' => clamp_float((float)($outBall['y'] ?? $spot['y']), -0.35, 1.35),
  ];
  $line = $lineBall
    ? [
      'x' => clamp_float((float)($lineBall['x'] ?? $out['x']), -0.08, 1.08),
      'y' => clamp_float((float)($lineBall['y'] ?? ($out['y'] < 0.5 ? -0.028 : 1.028)), -0.16, 1.16),
    ]
    : [
      'x' => $out['x'],
      'y' => $out['y'] < 0.5 ? -0.028 : 1.028,
    ];
  $match['engine']['passTarget'] = null;
  $match['engine']['shot'] = null;
  $match['engine']['restartType'] = $type;
  $match['engine']['restartSide'] = $side;
  $match['engine']['restartSpot'] = ['x' => (float)$spot['x'], 'y' => (float)$spot['y']];
  $match['engine']['restartStartedAt'] = (float)$match['elapsed'];
  $match['engine']['restartLineBall'] = $line;
  $match['engine']['restartOutBall'] = $out;
  $match['engine']['restartRollUntil'] = (float)$match['elapsed'] + 0.38;
  $match['engine']['restartDeliveryUntil'] = (float)$match['elapsed'] + 1.12;
  $match['engine']['restartUntil'] = (float)$match['elapsed'] + 1.42;
  $match['engine']['restartSignalUntil'] = 0.0;
  $match['engine']['restartWhistled'] = false;
  prepare_restart_units($match, $side, $type, $match['engine']['restartSpot']);
  $match['ball'] = ['x' => $line['x'], 'y' => $line['y'], 'z' => 0.0, 'vx' => 0.0, 'vy' => 0.0, 'vz' => 0.0];
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 2.05;
  $label = $type === 'corner' ? ' CORNER' : ' GOAL KICK';
  $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $side) . $label];
}

function take_restart(array &$match, string $side, string $type, array $spot): void {
  $kickIndex = player_index_by_id($match['units'][$side] ?? [], is_string($match['engine']['restartKicker'] ?? null) ? $match['engine']['restartKicker'] : null);
  $kickIndex = $kickIndex ?? restart_kicker_index($match, $side, $type, $spot);
  if ($kickIndex === null || empty($match['units'][$side][$kickIndex])) return;
  $receiverIndex = restart_receiver_index($match, $side, $type, $kickIndex);
  $dir = attack_dir($side);
  $target = $receiverIndex !== null && isset($match['units'][$side][$receiverIndex])
    ? $match['units'][$side][$receiverIndex]
    : ['id' => null, 'x' => 0.5, 'y' => clamp_float((float)$spot['y'] + $dir * ($type === 'corner' ? 0.22 : 0.28), 0.06, 0.94)];
  $dx = (float)$target['x'] - (float)$spot['x'];
  $dy = (float)$target['y'] - (float)$spot['y'];
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $power = $type === 'corner' ? 0.36 : 0.30;
  $speed = clamp_float($dist / 0.68, 0.18, $power);
  $kicker = $match['units'][$side][$kickIndex];
  $match['engine']['lastTouch'] = $side . ':' . ($kicker['id'] ?? $kickIndex);
  $match['engine']['lastTouchAt'] = (float)$match['elapsed'];
  $match['engine']['passTarget'] = [
    'side' => $side,
    'id' => $target['id'] ?? null,
    'quality' => $type === 'corner' ? 0.78 : 0.86,
    'type' => $type === 'corner' ? 'long' : 'ground',
  ];
  $match['ball'] = [
    'x' => (float)$spot['x'],
    'y' => (float)$spot['y'],
    'z' => $type === 'corner' ? 0.035 : 0.0,
    'vx' => $dx / $dist * $speed,
    'vy' => $dy / $dist * $speed,
    'vz' => $type === 'corner' ? 0.24 : 0.0,
  ];
  $match['engine']['restartType'] = null;
  $match['engine']['restartSide'] = null;
  $match['engine']['restartSpot'] = null;
  $match['engine']['restartKicker'] = null;
  $match['engine']['restartStartedAt'] = 0.0;
  $match['engine']['restartRollUntil'] = 0.0;
  $match['engine']['restartDeliveryUntil'] = 0.0;
  $match['engine']['restartUntil'] = 0.0;
  $match['engine']['restartSignalUntil'] = 0.0;
  $match['engine']['restartWhistled'] = false;
  $match['engine']['restartLineBall'] = null;
  $match['engine']['restartOutBall'] = null;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + ($type === 'corner' ? 0.34 : 0.22);
}

function award_foul(array &$match, string $fouledSide, string $foulerSide, array &$ball, bool $yellow, ?array $foulerPlayer = null, ?array $fouledPlayer = null): void {
  ensure_match_engine($match);
  $spot = [
    'x' => clamp_float((float)($ball['x'] ?? 0.5), 0.08, 0.92),
    'y' => clamp_float((float)($ball['y'] ?? 0.5), 0.08, 0.92),
  ];
  $match['engine']['fouls'][$foulerSide]++;
  if ($foulerPlayer) telemetry_add($match, $foulerSide, $foulerPlayer, ['fouls' => 1]);
  $red = false;
  $secondYellow = false;
  $directRed = false;
  if ($foulerPlayer) {
    telemetry_register_player($match, $foulerSide, $foulerPlayer);
    $key = telemetry_key($foulerPlayer);
    $currentYellows = (int)($match['engine']['playerStats'][$foulerSide][$key]['yellows'] ?? 0);
    $directRedChance = clamp_float(0.002 + player_stat($foulerPlayer, 'aggression') / 2900 + (100 - player_stat($foulerPlayer, 'discipline')) / 3600, 0.002, 0.055);
    $directRed = rand_float() < $directRedChance;
    if ($directRed) {
      $yellow = false;
      $red = true;
    } elseif ($yellow && $currentYellows >= 1) {
      $secondYellow = true;
      $red = true;
    }
  }
  if ($yellow) {
    $match['engine']['yellows'][$foulerSide]++;
    if ($foulerPlayer) telemetry_add($match, $foulerSide, $foulerPlayer, ['yellows' => 1]);
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $foulerSide) . ($secondYellow ? ' SECOND YELLOW CARD' : ' YELLOW CARD')];
  }
  if ($red) {
    $match['engine']['reds'][$foulerSide]++;
    if ($foulerPlayer) {
      telemetry_add($match, $foulerSide, $foulerPlayer, [
        'reds' => 1,
        'secondYellowReds' => $secondYellow ? 1 : 0,
        'directReds' => $directRed ? 1 : 0,
      ]);
      sync_match_player_cards($match, $foulerSide, $foulerPlayer);
      send_off_match_player($match, $foulerSide, $foulerPlayer);
    }
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $foulerSide) . ($secondYellow ? ' RED CARD (2Y)' : ' RED CARD')];
  } elseif ($foulerPlayer && $yellow) {
    sync_match_player_cards($match, $foulerSide, $foulerPlayer);
  }
  if ($foulerPlayer) telemetry_maybe_complain($match, $foulerSide, $foulerPlayer, $yellow ? 0.24 : 0.12);
  if ($fouledPlayer) telemetry_maybe_complain($match, $fouledSide, $fouledPlayer, 0.20);
  if (is_penalty_foul_spot($spot, $fouledSide)) {
    begin_penalty($match, $fouledSide);
    $ball = $match['ball'];
    return;
  }
  $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $fouledSide) . ' FREE KICK'];
  clear_owner($match);
  $match['engine']['passTarget'] = null;
  $match['engine']['shot'] = null;
  $match['engine']['freeKickSide'] = $fouledSide;
  $match['engine']['freeKickSpot'] = $spot;
  $kickIndex = nearest_role_index($match['units'][$fouledSide] ?? [], $spot, $fouledSide);
  $match['engine']['freeKickKicker'] = $kickIndex !== null ? ($match['units'][$fouledSide][$kickIndex]['id'] ?? null) : null;
  $match['engine']['freeKickUntil'] = (float)$match['elapsed'] + 0.95;
  $match['engine']['freeKickSignalUntil'] = (float)$match['elapsed'] + 1.45;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 1.65;
  $ball = stopped_ball_at($spot['x'], $spot['y']);
  $match['ball'] = $ball;
}

function free_kick_kicker_index(array $match, string $side, array $spot): ?int {
  $kickerId = $match['engine']['freeKickKicker'] ?? null;
  $index = player_index_by_id($match['units'][$side] ?? [], is_string($kickerId) ? $kickerId : null);
  return $index ?? nearest_role_index($match['units'][$side] ?? [], $spot, $side);
}

function move_player_away_from_spot(array &$player, array $spot, float $minDistance, float $speed, float $step): void {
  $dx = (float)$player['x'] - (float)$spot['x'];
  $dy = (float)$player['y'] - (float)$spot['y'];
  $dist = sqrt($dx * $dx + $dy * $dy);
  if ($dist >= $minDistance) return;
  if ($dist < 0.001) {
    $dx = ((float)($player['homeX'] ?? 0.5) < 0.5) ? -1.0 : 1.0;
    $dy = ((float)($player['homeY'] ?? 0.5) < 0.5) ? -0.35 : 0.35;
    $dist = sqrt($dx * $dx + $dy * $dy);
  }
  $targetX = clamp_float((float)$spot['x'] + $dx / $dist * $minDistance, 0.04, 0.96);
  $targetY = clamp_float((float)$spot['y'] + $dy / $dist * $minDistance, 0.04, 0.96);
  move_towards($player, $targetX, $targetY, $speed, $step);
}

function arrange_free_kick_players(array &$match, string $freeKickSide, array $spot, float $step): void {
  $opponent = opponent_side($freeKickSide);
  if (!empty($match['units'][$opponent]) && is_array($match['units'][$opponent])) {
    foreach ($match['units'][$opponent] as &$player) {
      $speed = 0.065 + player_stat($player, 'speed') / 900;
      move_player_away_from_spot($player, $spot, 0.175, $speed, $step);
    }
    unset($player);
  }

  $kickIndex = free_kick_kicker_index($match, $freeKickSide, $spot);
  if (!empty($match['units'][$freeKickSide]) && is_array($match['units'][$freeKickSide])) {
    foreach ($match['units'][$freeKickSide] as $index => &$player) {
      if ($index === $kickIndex) {
        $speed = 0.030 + player_stat($player, 'speed') / 1600;
        move_towards($player, (float)$spot['x'], (float)$spot['y'], $speed, $step);
        continue;
      }
      move_player_away_from_spot($player, $spot, 0.070, 0.035 + player_stat($player, 'speed') / 1800, $step);
      $role = (string)($player['role'] ?? 'CM');
      if ($role === 'GK') continue;
      $dir = attack_dir($freeKickSide);
      $supportX = clamp_float((float)$player['homeX'] + ((float)$spot['x'] - 0.5) * 0.10, 0.06, 0.94);
      $supportY = clamp_role_y($freeKickSide, $role, (float)$spot['y'] + $dir * (0.12 + ($index % 3) * 0.035), 0.16);
      $speed = 0.025 + player_stat($player, 'speed') / 2000;
      move_towards($player, $supportX, $supportY, $speed, $step);
    }
    unset($player);
  }
}

function take_free_kick(array &$match, string $side, int $kickIndex): void {
  $player = $match['units'][$side][$kickIndex];
  $shotProbability = shot_probability($match, $side, $player);
  $receiver = choose_receiver($match, $side, $player);
  $goalY = $side === 'home' ? 0.0 : 1.0;
  $goalDistance = abs((float)$player['y'] - $goalY);
  $central = 1.0 - clamp_float(abs((float)$player['x'] - 0.5) / 0.42, 0.0, 1.0);
  $shootNow = $goalDistance < 0.22 || ($goalDistance < 0.34 && $central > 0.34 && $shotProbability > 0.20);
  $match['engine']['freeKickSide'] = null;
  $match['engine']['freeKickSpot'] = null;
  $match['engine']['freeKickKicker'] = null;
  $match['engine']['freeKickUntil'] = 0.0;
  $match['engine']['freeKickSignalUntil'] = 0.0;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.18;
  if ($shootNow) {
    release_shot($match, $side, $kickIndex);
    return;
  }
  if ($receiver) {
    release_pass($match, $side, $kickIndex, $receiver);
    return;
  }
  set_owner($match, $side, $kickIndex);
}

function penalty_spot_for_side(string $side): array {
  $spotY = $side === 'home' ? (11.0 / 105.0) : (1.0 - 11.0 / 105.0);
  return ['x' => 0.5, 'y' => $spotY];
}

function is_penalty_foul_spot(array $spot, string $attackingSide): bool {
  $x = (float)($spot['x'] ?? 0.5);
  $y = (float)($spot['y'] ?? 0.5);
  $halfWidth = ((7.32 + 16.5 * 2.0) / 68.0) / 2.0;
  $minX = 0.5 - $halfWidth;
  $maxX = 0.5 + $halfWidth;
  $depth = 16.5 / 105.0;
  if ($x < $minX || $x > $maxX) return false;
  return $attackingSide === 'home' ? $y <= $depth : $y >= (1.0 - $depth);
}

function penalty_kicker_index(array $match, string $side): ?int {
  $best = null;
  $bestScore = -1;
  foreach ($match['units'][$side] ?? [] as $index => $player) {
    if (($player['role'] ?? '') === 'GK') continue;
    $score = player_stat($player, 'shooting') * 1.15 + player_stat($player, 'shotcontrol') * 0.85 + player_stat($player, 'composure') * 0.70 + player_stat($player, 'crit') * 0.15;
    if (in_array($player['role'] ?? '', ['ST', 'CM'], true)) $score += 8;
    if ($score > $bestScore) {
      $bestScore = $score;
      $best = (int)$index;
    }
  }
  return $best;
}

function prepare_penalty_units(array &$match, string $side): void {
  reset_units($match);
  $spot = penalty_spot_for_side($side);
  $kickerIndex = penalty_kicker_index($match, $side);
  if ($kickerIndex !== null && isset($match['units'][$side][$kickerIndex])) {
    $dir = attack_dir($side);
    $match['engine']['penaltyKicker'] = $match['units'][$side][$kickerIndex]['id'] ?? null;
    $match['units'][$side][$kickerIndex]['x'] = 0.5;
    $match['units'][$side][$kickerIndex]['y'] = clamp_float((float)$spot['y'] - $dir * 0.045, 0.03, 0.97);
    $match['units'][$side][$kickerIndex]['vx'] = 0.0;
    $match['units'][$side][$kickerIndex]['vy'] = 0.0;
  }
  $keeperSide = opponent_side($side);
  foreach ($match['units'][$keeperSide] ?? [] as &$player) {
    if (($player['role'] ?? '') !== 'GK') continue;
    $player['x'] = 0.5;
    $player['y'] = $side === 'home' ? 0.012 : 0.988;
    $player['vx'] = 0.0;
    $player['vy'] = 0.0;
    break;
  }
  unset($player);
}

function begin_penalty(array &$match, string $side): void {
  ensure_match_engine($match);
  clear_owner($match);
  $spot = penalty_spot_for_side($side);
  $match['engine']['passTarget'] = null;
  $match['engine']['shot'] = null;
  $match['engine']['freeKickSide'] = null;
  $match['engine']['freeKickSpot'] = null;
  $match['engine']['freeKickKicker'] = null;
  $match['engine']['freeKickUntil'] = 0.0;
  $match['engine']['freeKickSignalUntil'] = 0.0;
  $match['engine']['penaltySide'] = $side;
  $match['engine']['penaltyUntil'] = (float)$match['elapsed'] + 0.95;
  $match['engine']['penaltySignalUntil'] = 0.0;
  $match['engine']['penaltyWhistled'] = false;
  prepare_penalty_units($match, $side);
  $match['ball'] = stopped_ball_at($spot['x'], $spot['y']);
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 1.75;
  $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $side) . ' PENALTY'];
}

function take_penalty(array &$match, string $side): void {
  $spot = penalty_spot_for_side($side);
  $kickIndex = player_index_by_id($match['units'][$side] ?? [], is_string($match['engine']['penaltyKicker'] ?? null) ? $match['engine']['penaltyKicker'] : null);
  $kickIndex = $kickIndex ?? penalty_kicker_index($match, $side);
  if ($kickIndex === null || empty($match['units'][$side][$kickIndex])) return;
  $player = $match['units'][$side][$kickIndex];
  $shooting = player_stat($player, 'shooting');
  $shotControl = player_stat($player, 'shotcontrol');
  $composure = player_stat($player, 'composure');
  $accuracy = ($shooting * 0.45 + $shotControl * 0.30 + $composure * 0.25) / 100;
  $targetX = clamp_float(0.5 + (rand_float() * 2 - 1) * (0.22 + (1.0 - $accuracy) * 0.10), goal_min_x() + 0.01, goal_max_x() - 0.01);
  if (rand_float() > $accuracy + 0.18) $targetX += rand_float() < 0.5 ? -0.20 : 0.20;
  if (!manager_high_score_goal_allowed(match_side_goals($match, $side))) $targetX += rand_float() < 0.5 ? -0.24 : 0.24;
  $goalY = $side === 'home' ? -0.04 : 1.04;
  $dx = $targetX - (float)$spot['x'];
  $dy = $goalY - (float)$spot['y'];
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $speed = clamp_float(0.36 + $shooting / 520 + $shotControl / 900, 0.40, 0.64);
  clear_owner($match);
  $match['engine']['shots'][$side]++;
  telemetry_add($match, $side, $player, ['shots' => 1, 'touches' => 1]);
  $match['engine']['lastShotSeq'] = (int)($match['engine']['lastShotSeq'] ?? 0) + 1;
  $match['engine']['lastShotAt'] = (float)$match['elapsed'];
  $match['engine']['lastShotBy'] = [
    'side' => $side,
    'id' => $player['id'] ?? null,
    'name' => $player['name'] ?? null,
  ];
  $match['engine']['shot'] = ['side' => $side, 'id' => $player['id'] ?? null, 'power' => $speed, 'crit' => false, 'penalty' => true];
  $match['engine']['penaltySide'] = null;
  $match['engine']['penaltyKicker'] = null;
  $match['engine']['penaltyUntil'] = 0.0;
  $match['engine']['penaltySignalUntil'] = 0.0;
  $match['engine']['penaltyWhistled'] = false;
  $match['engine']['lastTouch'] = $side . ':' . ($player['id'] ?? $kickIndex);
  $match['engine']['lastTouchAt'] = (float)$match['elapsed'];
  $match['engine']['lastPass'] = null;
  $match['ball'] = [
    'x' => (float)$spot['x'],
    'y' => (float)$spot['y'],
    'z' => rand_float() < 0.24 ? 0.035 : 0.0,
    'vx' => $dx / $dist * $speed,
    'vy' => $dy / $dist * $speed,
    'vz' => rand_float() < 0.24 ? 0.11 : 0.0,
  ];
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.44;
}

function make_substitution(array &$match, string $side): array {
  ensure_match_engine($match);
  if (!in_array($side, ['home', 'away'], true)) return ['ok' => false, 'error' => 'bad_side'];
  $used = (int)($match['engine']['subsUsed'][$side] ?? 0);
  if ($used >= MANAGER_MAX_MATCH_SUBS) return ['ok' => false, 'error' => 'subs_used'];
  if (empty($match['units'][$side]) || empty($match['bench'][$side])) return ['ok' => false, 'error' => 'no_bench'];

  $owner = owner_ref($match);
  $ownerIndex = $owner && $owner['side'] === $side ? $owner['index'] : null;
  $outIndex = null;
  $outScore = PHP_FLOAT_MAX;
  foreach ($match['units'][$side] as $index => $player) {
    if (($player['role'] ?? '') === 'GK') continue;
    if ($ownerIndex !== null && $index === $ownerIndex) continue;
    $score = (int)($player['rating'] ?? 50) + mt_rand(0, 9);
    if ($score < $outScore) {
      $outScore = $score;
      $outIndex = $index;
    }
  }
  if ($outIndex === null) {
    foreach ($match['units'][$side] as $index => $player) {
      if (($player['role'] ?? '') === 'GK') continue;
      $outIndex = $index;
      break;
    }
  }
  if ($outIndex === null) $outIndex = 0;

  $outgoing = $match['units'][$side][$outIndex];
  $outRole = (string)($outgoing['role'] ?? 'CM');
  $benchIndex = null;
  $benchScore = -1;
  foreach ($match['bench'][$side] as $index => $player) {
    if (($player['role'] ?? '') === $outRole) {
      $benchIndex = $index;
      break;
    }
    if (($player['role'] ?? '') !== 'GK' && (int)($player['rating'] ?? 0) > $benchScore) {
      $benchScore = (int)($player['rating'] ?? 0);
      $benchIndex = $index;
    }
  }
  if ($benchIndex === null) $benchIndex = 0;

  $incoming = array_splice($match['bench'][$side], $benchIndex, 1)[0];
  $incoming['role'] = $outRole;
  $incoming['homeX'] = $outgoing['homeX'];
  $incoming['homeY'] = $outgoing['homeY'];
  $incoming['x'] = $outgoing['x'];
  $incoming['y'] = $outgoing['y'];
  $match['units'][$side][$outIndex] = $incoming;
  $match['engine']['subsUsed'][$side] = $used + 1;
  if ($ownerIndex !== null && $ownerIndex === $outIndex) set_owner($match, $side, $outIndex);
  $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => side_team_name($match, $side) . ' SUB ' . ($used + 1) . '/' . MANAGER_MAX_MATCH_SUBS];
  return ['ok' => true, 'in' => $incoming['id'] ?? null, 'out' => $outgoing['id'] ?? null];
}

function nearest_opponent_index(array $match, string $side, float $x, float $y): ?int {
  $opponent = opponent_side($side);
  return nearest_index($match['units'][$opponent] ?? [], ['x' => $x, 'y' => $y]);
}

function pressure_on(array $match, string $side, float $x, float $y): float {
  $opponent = opponent_side($side);
  $best = PHP_FLOAT_MAX;
  foreach ($match['units'][$opponent] ?? [] as $player) {
    if (($player['role'] ?? '') === 'GK') continue;
    $dist = sqrt(distance2($player, ['x' => $x, 'y' => $y]));
    $best = min($best, $dist);
  }
  if ($best === PHP_FLOAT_MAX) return 0.0;
  return clamp_float(1.0 - $best / 0.18, 0.0, 1.0);
}

function pass_line_clearance(array $match, string $side, array $passer, array $receiver): float {
  $opponent = opponent_side($side);
  $ax = (float)$passer['x'];
  $ay = (float)$passer['y'];
  $bx = (float)$receiver['x'];
  $by = (float)$receiver['y'];
  $dx = $bx - $ax;
  $dy = $by - $ay;
  $len2 = max(0.0001, $dx * $dx + $dy * $dy);
  $clearance = 1.0;
  foreach ($match['units'][$opponent] ?? [] as $defender) {
    $px = (float)$defender['x'];
    $py = (float)$defender['y'];
    $t = clamp_float((($px - $ax) * $dx + ($py - $ay) * $dy) / $len2, 0.0, 1.0);
    $cx = $ax + $dx * $t;
    $cy = $ay + $dy * $t;
    $dist = sqrt(($px - $cx) * ($px - $cx) + ($py - $cy) * ($py - $cy));
    if ($dist < 0.035) $clearance *= 0.25;
    elseif ($dist < 0.075) $clearance *= 0.62;
  }
  return clamp_float($clearance, 0.0, 1.0);
}

function receiver_score(array $match, string $side, array $passer, array $receiver): float {
  if (($receiver['id'] ?? '') === ($passer['id'] ?? '')) return 0.0;
  $dx = (float)$receiver['x'] - (float)$passer['x'];
  $dy = (float)$receiver['y'] - (float)$passer['y'];
  $dist = sqrt($dx * $dx + $dy * $dy);
  if ($dist < 0.08 || $dist > 0.72) return 0.0;
  $forward = attack_dir($side) * $dy;
  $forwardScore = clamp_float(($forward + 0.12) / 0.50, 0.0, 1.0);
  $distanceScore = 1.0 - clamp_float(abs($dist - 0.26) / 0.48, 0.0, 1.0);
  $space = 1.0 - pressure_on($match, $side, (float)$receiver['x'], (float)$receiver['y']);
  $line = pass_line_clearance($match, $side, $passer, $receiver);
  $role = in_array($receiver['role'] ?? '', ['ST', 'RM', 'LM', 'CM'], true) ? 1.0 : 0.75;
  $pass = player_stat($passer, 'passing') / 100;
  $vision = player_stat($passer, 'vision') / 100;
  $crossing = player_stat($passer, 'crossing') / 100;
  $control = player_stat($receiver, 'passcontrol') / 100;
  $positioning = player_stat($receiver, 'positioning') / 100;
  $wideLane = clamp_float(abs((float)$receiver['x'] - 0.5) / 0.44, 0.0, 1.0);
  $wideRole = in_array($receiver['role'] ?? '', ['RM', 'LM', 'RB', 'LB'], true) ? 1.0 : 0.0;
  $passerCentral = 1.0 - clamp_float(abs((float)$passer['x'] - 0.5) / 0.42, 0.0, 1.0);
  $wideSwitch = clamp_float($wideLane * 0.60 + $wideRole * 0.40, 0.0, 1.0) * (0.35 + $passerCentral * 0.45 + $crossing * 0.20);
  return clamp_float(0.16 * $forwardScore + 0.13 * $distanceScore + 0.20 * $space + 0.18 * $line + 0.06 * $role + 0.05 * $pass + 0.06 * $vision + 0.04 * $control + 0.04 * $positioning + 0.08 * $wideSwitch, 0.0, 1.0);
}

function choose_receiver(array $match, string $side, array $passer): ?array {
  $best = null;
  $bestScore = 0.0;
  $wide = null;
  $wideScore = 0.0;
  $long = null;
  $longScore = 0.0;
  $passerPressure = pressure_on($match, $side, (float)$passer['x'], (float)$passer['y']);
  $passerCentral = 1.0 - clamp_float(abs((float)$passer['x'] - 0.5) / 0.42, 0.0, 1.0);
  $passing = player_stat($passer, 'passing');
  $vision = player_stat($passer, 'vision');
  $crossing = player_stat($passer, 'crossing');
  foreach ($match['units'][$side] ?? [] as $index => $receiver) {
    $score = receiver_score($match, $side, $passer, $receiver);
    if ($score > $bestScore) {
      $bestScore = $score;
      $best = ['index' => $index, 'score' => $score];
    }
    $dx = (float)$receiver['x'] - (float)$passer['x'];
    $dy = (float)$receiver['y'] - (float)$passer['y'];
    $dist = sqrt($dx * $dx + $dy * $dy);
    $forward = attack_dir($side) * $dy;
    $wideLane = abs((float)$receiver['x'] - 0.5);
    $isWideOption = $wideLane > 0.26 || in_array($receiver['role'] ?? '', ['RM', 'LM', 'RB', 'LB'], true);
    if ($isWideOption && abs($dx) > 0.10 && $forward > -0.06 && $score > 0.30) {
      $candidateScore = $score + $wideLane * 0.22 + $passerCentral * 0.08 + $crossing / 550;
      if ($candidateScore > $wideScore) {
        $wideScore = $candidateScore;
        $wide = ['index' => $index, 'score' => clamp_float($score + 0.05, 0.0, 1.0), 'type' => 'wide'];
      }
    }
    $isForwardTarget = ($receiver['role'] ?? '') !== 'GK';
    if ($isForwardTarget && $dist > 0.22 && $forward > 0.05 && $score > 0.12) {
      $roleBoost = in_array($receiver['role'] ?? '', ['ST', 'RM', 'LM'], true) ? 0.12 : 0.0;
      $candidateScore = $score + $forward * 0.42 + $dist * 0.20 + ($passing + $vision) / 680 + $roleBoost;
      if ($candidateScore > $longScore) {
        $longScore = $candidateScore;
        $long = ['index' => $index, 'score' => clamp_float($score + 0.04, 0.0, 1.0), 'type' => 'long'];
      }
    }
  }
  if ($long !== null) {
    $longChance = clamp_float(0.14 + ($passing + $vision) / 920 + $passerPressure * 0.16, 0.14, 0.44);
    if (in_array($passer['role'] ?? '', ['GK', 'CB', 'RB', 'LB', 'CM', 'RM', 'LM'], true) && rand_float() < $longChance) return $long;
  }
  if ($wide !== null) {
    $wideChance = clamp_float(0.10 + $passerCentral * 0.13 + $passerPressure * 0.10 + $crossing / 700, 0.10, 0.38);
    if (rand_float() < $wideChance) return $wide;
  }
  return $best !== null && $bestScore > 0.37 ? $best : null;
}

function shot_probability(array $match, string $side, array $player): float {
  $dir = attack_dir($side);
  $goalY = $side === 'home' ? 0.0 : 1.0;
  $goalDistance = abs((float)$player['y'] - $goalY);
  if ($goalDistance > 0.62) return 0.0;
  $central = 1.0 - clamp_float(abs((float)$player['x'] - 0.5) / 0.42, 0.0, 1.0);
  $distanceScore = 1.0 - clamp_float($goalDistance / 0.62, 0.0, 1.0);
  $skill = (player_stat($player, 'shooting') * 0.46 + player_stat($player, 'shotcontrol') * 0.28 + player_stat($player, 'composure') * 0.16 + player_stat($player, 'positioning') * 0.10) / 100;
  $pressure = pressure_on($match, $side, (float)$player['x'], (float)$player['y']);
  $rolePenalty = in_array($player['role'] ?? '', ['CB', 'RB', 'LB'], true) && $goalDistance > 0.22 ? 0.12 : 0.0;
  $roleBoost = match ((string)($player['role'] ?? '')) {
    'ST' => 0.26,
    'RM', 'LM' => 0.10,
    'CM' => 0.05,
    default => 0.0,
  };
  $pressurePenalty = $pressure * clamp_float(0.22 - player_stat($player, 'composure') / 800, 0.08, 0.22);
  $chance = clamp_float(-0.04 + 0.42 * $distanceScore + 0.20 * $central + 0.48 * $skill + $roleBoost - $pressurePenalty - $rolePenalty, 0.0, 0.96);
  return clamp_float($chance * manager_high_score_goal_factor(match_side_goals($match, $side)), 0.0, 0.96);
}

function release_pass(array &$match, string $side, int $ownerIndex, array $target): void {
  $passer = $match['units'][$side][$ownerIndex];
  $receiver = $match['units'][$side][$target['index']];
  $passing = player_stat($passer, 'passing');
  $passControl = player_stat($passer, 'passcontrol');
  $vision = player_stat($passer, 'vision');
  $composure = player_stat($passer, 'composure');
  $crossing = player_stat($passer, 'crossing');
  $dx = (float)$receiver['x'] - (float)$passer['x'];
  $dy = (float)$receiver['y'] - (float)$passer['y'];
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $type = (string)($target['type'] ?? 'ground');
  $isLong = $type === 'long';
  $weightedPass = $passing * 0.50 + $passControl * 0.22 + $vision * 0.18 + $composure * 0.10 + ($type === 'wide' || $isLong ? $crossing * 0.10 : 0);
  $error = (1.0 - clamp_float($weightedPass / ($type === 'wide' || $isLong ? 110 : 100), 0.0, 1.0)) * 0.16;
  $angleX = $dx / $dist + (rand_float() * 2 - 1) * $error;
  $angleY = $dy / $dist + (rand_float() * 2 - 1) * $error;
  $len = max(0.001, sqrt($angleX * $angleX + $angleY * $angleY));
  $targetTime = $isLong
    ? clamp_float(0.82 + $dist * 0.52 - ($passing + $vision) / 1700, 0.78, 1.22)
    : clamp_float(0.36 + $dist * 1.08 - ($passing + $passControl + $vision * 0.45) / 900, 0.34, 1.12);
  $speed = $isLong ? clamp_float($dist / $targetTime, 0.24, 0.52) : clamp_float($dist / $targetTime, 0.14, 0.46);
  clear_owner($match);
  $match['engine']['passTarget'] = [
    'side' => $side,
    'id' => $receiver['id'],
    'quality' => clamp_float(($target['score'] + $passing / 100 + $passControl / 100 + $vision / 100 + $composure / 100) / 5, 0.0, 1.0),
    'type' => $isLong ? 'long' : ((string)($target['type'] ?? '') === 'wide' ? 'wide' : 'ground'),
  ];
  $match['engine']['lastPass'] = [
    'side' => $side,
    'id' => $passer['id'] ?? null,
    'name' => $passer['name'] ?? null,
    'to' => $receiver['id'] ?? null,
    'at' => (float)$match['elapsed'],
  ];
  telemetry_add($match, $side, $passer, [
    'passes' => 1,
    'longPasses' => $isLong ? 1 : 0,
    'widePasses' => ((string)($target['type'] ?? '') === 'wide') ? 1 : 0,
  ]);
  $release = player_ball_release_point($passer, $side, 0.92);
  $match['ball'] = [
    'x' => $release['x'],
    'y' => $release['y'],
    'z' => $isLong ? 0.045 : 0.0,
    'vx' => $angleX / $len * $speed,
    'vy' => $angleY / $len * $speed,
    'vz' => $isLong ? 0.25 + ($passing + $crossing) / 200 * 0.09 + min(0.05, $dist * 0.06) : 0.0,
  ];
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + ($isLong ? 0.32 : 0.18);
}

function release_shot(array &$match, string $side, int $ownerIndex): void {
  $player = $match['units'][$side][$ownerIndex];
  $shooting = player_stat($player, 'shooting');
  $shotControl = player_stat($player, 'shotcontrol');
  $composure = player_stat($player, 'composure');
  $strength = player_stat($player, 'strength');
  $crit = player_stat($player, 'crit');
  $isCrit = rand_float() < ($crit / 100) * 0.12;
  if ($isCrit) $match['engine']['crits'][$side]++;
  $goalX = 0.5;
  $goalY = $side === 'home' ? -0.04 : 1.04;
  $accuracy = ($shooting * 0.45 + $shotControl * 0.34 + $composure * 0.21) / 100;
  $error = (1 - $accuracy) * ($isCrit ? 0.12 : 0.28);
  $targetX = clamp_float($goalX + (rand_float() * 2 - 1) * $error, 0.22, 0.78);
  if (!$isCrit && rand_float() > $accuracy + 0.10) {
    $targetX += rand_float() < 0.5 ? -0.18 : 0.18;
  }
  $dx = $targetX - (float)$player['x'];
  $dy = $goalY - (float)$player['y'];
  $dist = max(0.001, sqrt($dx * $dx + $dy * $dy));
  $speed = clamp_float(0.25 + $dist * 0.24 + $shooting / 100 * 0.10 + $strength / 100 * 0.05 + ($isCrit ? 0.08 : 0.0), 0.30, 0.62);
  $lifted = rand_float() < clamp_float(0.10 + $shotControl / 600 + $shooting / 900, 0.10, 0.34);
  clear_owner($match);
  $match['engine']['shots'][$side]++;
  telemetry_add($match, $side, $player, ['shots' => 1, 'crits' => $isCrit ? 1 : 0, 'touches' => 1]);
  $match['engine']['lastShotSeq'] = (int)($match['engine']['lastShotSeq'] ?? 0) + 1;
  $match['engine']['lastShotAt'] = (float)$match['elapsed'];
  $match['engine']['lastShotBy'] = [
    'side' => $side,
    'id' => $player['id'] ?? null,
    'name' => $player['name'] ?? null,
  ];
  $match['engine']['shot'] = ['side' => $side, 'id' => $player['id'], 'power' => $speed, 'crit' => $isCrit];
  $release = player_ball_release_point($player, $side, 0.96);
  $match['ball'] = [
    'x' => $release['x'],
    'y' => $release['y'],
    'z' => $lifted ? 0.05 : 0.0,
    'vx' => $dx / $dist * $speed,
    'vy' => $dy / $dist * $speed,
    'vz' => $lifted ? 0.11 + $shooting / 100 * 0.07 : 0.0,
  ];
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.48;
  if ($isCrit) {
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => ($match['teams'][$side]['name'] ?? strtoupper($side)) . ' CRIT SHOT'];
  }
}

function apply_keeper_save(array &$match, array &$ball): bool {
  $nearTop = (float)$ball['y'] < 0.035 && (float)($ball['vy'] ?? 0) < 0;
  $nearBottom = (float)$ball['y'] > 0.965 && (float)($ball['vy'] ?? 0) > 0;
  if (!$nearTop && !$nearBottom) return false;
  if ((float)$ball['x'] < goal_min_x() - 0.035 || (float)$ball['x'] > goal_max_x() + 0.035) return false;
  $defendingSide = $nearTop ? 'away' : 'home';
  if (empty($match['units'][$defendingSide][0])) return false;
  $keeper = $match['units'][$defendingSide][0];
  $dist = sqrt(distance2($keeper, $ball));
  $reach = 0.052 + player_stat($keeper, 'keeping') / 1100 + player_stat($keeper, 'speed') / 1800 + player_stat($keeper, 'positioning') / 2200;
  if ($dist > $reach) return false;

  $shot = $match['engine']['shot'] ?? null;
  $shotPower = is_array($shot) ? (float)($shot['power'] ?? 0.25) : 0.22;
  $critPenalty = is_array($shot) && !empty($shot['crit']) ? 0.16 : 0.0;
  $saveSkill = player_stat($keeper, 'keeping') * 0.58 + player_stat($keeper, 'speed') * 0.14 + player_stat($keeper, 'positioning') * 0.14 + player_stat($keeper, 'composure') * 0.08 + player_stat($keeper, 'shotcontrol') * 0.06;
  $saveChance = clamp_float(0.16 + $saveSkill / 120 - $shotPower * 1.08 - $critPenalty - $dist * 1.4, 0.05, 0.92);
  if (rand_float() > $saveChance) return false;

  $match['engine']['saves'][$defendingSide]++;
  telemetry_add($match, $defendingSide, $keeper, ['saves' => 1, 'touches' => 1]);
  $catchChance = clamp_float((player_stat($keeper, 'keeping') * 0.72 + player_stat($keeper, 'composure') * 0.18 + player_stat($keeper, 'strength') * 0.10) / 125 - $shotPower * 0.75, 0.05, 0.72);
  if (rand_float() < $catchChance) {
    set_owner($match, $defendingSide, 0);
    place_ball_on_player($ball, $keeper, $defendingSide);
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => ($match['teams'][$defendingSide]['name'] ?? strtoupper($defendingSide)) . ' KEEPER HOLD'];
  } else {
    clear_owner($match);
    $awayFromGoal = $nearTop ? 1 : -1;
    $ball['vx'] = ((float)$ball['x'] - 0.5) * 0.18 + (rand_float() * 2 - 1) * 0.08;
    $ball['vy'] = $awayFromGoal * (0.13 + (player_stat($keeper, 'keeping') + player_stat($keeper, 'strength') * 0.35) / 1050);
    $ball['vz'] = 0.06 + rand_float() * 0.10;
    $match['engine']['shot'] = null;
    $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.30;
    $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => ($match['teams'][$defendingSide]['name'] ?? strtoupper($defendingSide)) . ' KEEPER SAVE'];
  }
  return true;
}

function move_players(array &$match, array $ball, float $step): void {
  $owner = owner_ref($match);
  $clock = (float)($match['elapsed'] ?? 0);
  $nearest = [
    'home' => nearest_role_index($match['units']['home'] ?? [], $ball, 'home'),
    'away' => nearest_role_index($match['units']['away'] ?? [], $ball, 'away'),
  ];
  foreach (['home', 'away'] as $side) {
    if (empty($match['units'][$side])) continue;
    $mode = $match['tactics'][$side] ?? 'balanced';
    $lineShift = match ($mode) {
      'attack' => $side === 'home' ? -0.055 : 0.055,
      'defend' => $side === 'home' ? 0.055 : -0.055,
      default => 0.0,
    };
    foreach ($match['units'][$side] as $index => &$player) {
      $role = (string)($player['role'] ?? 'CM');
      $oldX = (float)($player['x'] ?? ($player['homeX'] ?? 0.5));
      $oldY = (float)($player['y'] ?? ($player['homeY'] ?? 0.5));
      $speedStat = player_stat($player, 'speed');
      $stamina = player_stat($player, 'stamina');
      $workrate = player_stat($player, 'workrate');
      $positioning = player_stat($player, 'positioning');
      $gameIq = ($speedStat * 0.22 + player_stat($player, 'passcontrol') * 0.16 + player_stat($player, 'tackling') * 0.17 + $positioning * 0.25 + $workrate * 0.20);
      $speed = 0.044 + ($speedStat / 100) * 0.096 + ($stamina / 100) * 0.018;
      $isOwner = $owner && $owner['side'] === $side && $owner['index'] === $index;
      $isChaser = $nearest[$side] === $index;
      $distToBall = sqrt(distance2($player, $ball));
      $zoneMargin = match ($role) {
        'GK' => 0.04,
        'CB', 'RB', 'LB' => 0.055 + ($speedStat + $positioning) / 3000,
        'ST' => 0.075 + ($speedStat + $positioning) / 2800,
        default => 0.085 + ($speedStat + $positioning) / 2600,
      };
      $canJoinPlay = role_zone_contains($side, $role, (float)$ball['y'], $zoneMargin + (player_stat($player, 'tackling') + $workrate) / 3300);
      $nearRadius = 0.10 + $gameIq / 800;
      $isNearPlay = $canJoinPlay && $distToBall < $nearRadius;
      $urgent = $isOwner || $isChaser;
      if (($player['role'] ?? '') === 'GK') {
        $isThreat = $side === 'home' ? (float)$ball['y'] > 0.50 : (float)$ball['y'] < 0.50;
        $targetX = $isThreat ? clamp_float((float)$ball['x'], goal_min_x(), goal_max_x()) : 0.5;
        $targetY = (float)$player['homeY'];
        $speed *= $isThreat ? 0.70 : 0.22;
      } elseif ($isOwner) {
        $dir = attack_dir($side);
        if (in_array($role, ['RM', 'LM', 'RB', 'LB'], true)) {
          $wideBias = ((float)$player['homeX'] < 0.5) ? -0.020 : 0.020;
          $targetX = clamp_float((float)$player['x'] + ((float)$player['homeX'] - (float)$player['x']) * 0.18 + $wideBias, 0.05, 0.95);
        } else {
          $targetX = clamp_float((float)$player['x'] + (0.5 - (float)$player['x']) * 0.10, 0.06, 0.94);
        }
        $targetY = clamp_role_y($side, $role, (float)$player['y'] + $dir * (0.11 + (player_stat($player, 'shotcontrol') + player_stat($player, 'dribbling')) / 200 * 0.05), $zoneMargin + 0.14);
        $speed *= 0.88 + (player_stat($player, 'shotcontrol') + player_stat($player, 'dribbling')) / 560;
      } elseif ($owner && $owner['side'] !== $side) {
        $ownerPlayer = $match['units'][$owner['side']][$owner['index']];
        $support = AiDefensiveLane($player, $ownerPlayer, $side);
        $targetX = $support['x'];
        $targetY = $support['y'];
        if ($isChaser) {
          $targetX = (float)$ownerPlayer['x'];
          $targetY = clamp_role_y($side, $role, (float)$ownerPlayer['y'], $zoneMargin + 0.12);
          $speed *= 1.28;
          $urgent = true;
        } elseif ($isNearPlay) {
          $targetX = clamp_float($support['x'], 0.06, 0.94);
          $targetY = clamp_role_y($side, $role, $support['y'], $zoneMargin);
          $speed *= 0.26 + $gameIq / 420;
        } else {
          $targetX = clamp_float((float)$player['homeX'] + ((float)$ball['x'] - 0.5) * 0.04, 0.06, 0.94);
          $targetY = clamp_role_y($side, $role, (float)$player['homeY'] + $lineShift * 0.35, $zoneMargin * 0.6);
          $speed *= 0.10 + $gameIq / 900;
        }
      } elseif ($isChaser) {
        $lead = 0.20 + player_stat($player, 'speed') / 450;
        $targetX = clamp_float((float)$ball['x'] + (float)($ball['vx'] ?? 0) * $lead, 0.03, 0.97);
        $targetY = clamp_role_y($side, $role, (float)$ball['y'] + (float)($ball['vy'] ?? 0) * $lead, $zoneMargin + 0.10);
        $speed *= 1.35;
        $urgent = true;
      } elseif ($isNearPlay) {
        $targetX = clamp_float((float)$player['homeX'] + ((float)$ball['x'] - 0.5) * 0.10, 0.06, 0.94);
        $targetY = clamp_role_y($side, $role, (float)$player['homeY'] + ((float)$ball['y'] - (float)$player['homeY']) * 0.14 + $lineShift * 0.55, $zoneMargin);
        $speed *= 0.18 + $gameIq / 500;
      } else {
        $targetX = clamp_float((float)$player['homeX'] + ((float)$ball['x'] - 0.5) * 0.035, 0.06, 0.94);
        $targetY = clamp_role_y($side, $role, (float)$player['homeY'] + $lineShift * 0.30, $zoneMargin * 0.5);
        $speed *= 0.08 + $gameIq / 1050;
      }

      if (!$urgent && $role !== 'GK') {
        $phaseBase = $clock * 0.75 + $index * 2.4 + ($side === 'away' ? 1.1 : 0.0);
        $targetX = clamp_float($targetX + sin($phaseBase) * 0.006, 0.05, 0.95);
        $targetY = clamp_role_y($side, $role, $targetY + sin($clock * 0.62 + $index * 1.7) * 0.004, $zoneMargin * 0.55);
      }

      if (!$urgent) {
        $dx = $targetX - (float)$player['x'];
        $dy = $targetY - (float)$player['y'];
        $targetDist = sqrt($dx * $dx + $dy * $dy);
        if ($targetDist < ($isNearPlay ? 0.018 : 0.034)) {
          telemetry_record_movement($match, $side, $player, $oldX, $oldY, $step, $isNearPlay, $urgent);
          continue;
        }
        $reactionWindow = 0.20 + $gameIq / 420;
        $phase = fmod($clock + $index * 0.17 + ($side === 'away' ? 0.09 : 0.0), 0.90);
        if (!$isNearPlay && $phase > $reactionWindow) {
          telemetry_record_movement($match, $side, $player, $oldX, $oldY, $step, $isNearPlay, $urgent);
          continue;
        }
      }
      move_towards($player, $targetX, $targetY, $speed, $step);
      telemetry_record_movement($match, $side, $player, $oldX, $oldY, $step, $isNearPlay, $urgent);
    }
    unset($player);
  }
}

function AiDefensiveLane(array $player, array $threat, string $defendingSide): array {
  $ownGoalY = $defendingSide === 'home' ? 1.0 : 0.0;
  $coverX = (float)$threat['x'] + (0.5 - (float)$threat['x']) * 0.45;
  $coverY = (float)$threat['y'] + ($ownGoalY - (float)$threat['y']) * 0.22;
  $lane = ((float)$player['homeX'] - 0.5) * 0.12;
  return [
    'x' => clamp_float($coverX + $lane, 0.06, 0.94),
    'y' => clamp_float($coverY, 0.06, 0.94),
  ];
}

function apply_player_touch(array &$match, array &$ball, float $step): void {
  $owner = owner_ref($match);
  if ($owner) {
    $side = $owner['side'];
    $index = $owner['index'];
    $player = &$match['units'][$side][$index];
    $match['engine']['possession'][$side] += $step;
    telemetry_add($match, $side, $player, ['possessionSeconds' => $step]);
    $match['engine']['lastTouch'] = $side . ':' . ($player['id'] ?? $index);
    place_ball_on_player($ball, $player, $side);
    $canUseBall = ball_touches_player_circle($ball, $player, 0.003);

    $opponent = opponent_side($side);
    $defIndex = nearest_index($match['units'][$opponent] ?? [], $ball);
    if ($defIndex !== null) {
      $defender = $match['units'][$opponent][$defIndex];
      if (ball_touches_player_circle($ball, $defender, 0.006) && (float)$match['elapsed'] >= (float)($match['engine']['ballFreeUntil'] ?? 0)) {
        $defAggression = player_stat($defender, 'aggression');
        $defDiscipline = player_stat($defender, 'discipline');
        $tackle = player_stat($defender, 'tackling') * 0.46 + player_stat($defender, 'speed') * 0.16 + player_stat($defender, 'strength') * 0.16 + player_stat($defender, 'positioning') * 0.12 + $defAggression * 0.10 + mt_rand(0, 20);
        $control = player_stat($player, 'passcontrol') * 0.25 + player_stat($player, 'shotcontrol') * 0.25 + player_stat($player, 'dribbling') * 0.22 + player_stat($player, 'composure') * 0.16 + player_stat($player, 'strength') * 0.07 + player_stat($player, 'speed') * 0.05 + mt_rand(0, 20);
        if ($tackle > $control) {
          $match['engine']['tackles'][$opponent]++;
          telemetry_add($match, $opponent, $defender, ['tackles' => 1, 'touches' => 1]);
          telemetry_add($match, $side, $player, ['lostBalls' => 1]);
          telemetry_maybe_complain($match, $side, $player, 0.16);
          set_owner($match, $opponent, $defIndex);
          place_ball_on_player($ball, $match['units'][$opponent][$defIndex], $opponent);
          $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => ($match['teams'][$opponent]['name'] ?? strtoupper($opponent)) . ' TACKLE'];
          unset($player);
          return;
        } else {
          $foulChance = clamp_float(0.08 + ($control - $tackle) / 125 + (100 - player_stat($defender, 'tackling')) / 430 + $defAggression / 420 - $defDiscipline / 360, 0.03, 0.62);
          if (rand_float() < $foulChance) {
            $yellowChance = clamp_float(0.03 + ($control - $tackle) / 200 + $defAggression / 360 + (100 - $defDiscipline) / 420, 0.015, 0.32);
            award_foul($match, $side, $opponent, $ball, rand_float() < $yellowChance, $defender, $player);
            unset($player);
            return;
          }
          $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.24;
        }
      }
    }

    if ($canUseBall && (float)$match['elapsed'] >= (float)($match['engine']['nextDecisionAt'] ?? 0)) {
      $shotProbability = shot_probability($match, $side, $player);
      $receiver = choose_receiver($match, $side, $player);
      $passProbability = $receiver ? clamp_float(0.03 + $receiver['score'] * 0.84 + (player_stat($player, 'passing') + player_stat($player, 'vision')) / 420 + player_stat($player, 'composure') / 520 + pressure_on($match, $side, (float)$player['x'], (float)$player['y']) * 0.22, 0.0, 0.96) : 0.0;
      $match['engine']['nextDecisionAt'] = (float)$match['elapsed'] + 0.18 + rand_float() * 0.24;
      $isStriker = ($player['role'] ?? '') === 'ST';
      if ($isStriker && $shotProbability > 0.12 && rand_float() < clamp_float($shotProbability + 0.24, 0.0, 0.94)) {
        release_shot($match, $side, $index);
        $ball = $match['ball'];
      } elseif ($receiver && rand_float() < $passProbability && ($shotProbability < 0.68 || rand_float() < 0.40)) {
        release_pass($match, $side, $index, $receiver);
        $ball = $match['ball'];
      } elseif ($shotProbability > 0 && rand_float() < $shotProbability) {
        release_shot($match, $side, $index);
        $ball = $match['ball'];
      } elseif ($receiver && rand_float() < $passProbability * 0.45) {
        release_pass($match, $side, $index, $receiver);
        $ball = $match['ball'];
      }
    }
    unset($player);
    return;
  }

  if ((float)$match['elapsed'] < (float)($match['engine']['ballFreeUntil'] ?? 0)) return;
  $isHigh = (float)($ball['z'] ?? 0) > 0.08;
  $incomingSpeed = sqrt((float)($ball['vx'] ?? 0) ** 2 + (float)($ball['vy'] ?? 0) ** 2);
  $interceptExtra = clamp_float($incomingSpeed * 0.018, 0.0, 0.010);
  $target = $match['engine']['passTarget'] ?? null;
  $bestSide = null;
  $bestIndex = null;
  $bestScore = PHP_FLOAT_MAX;
  foreach (['home', 'away'] as $side) {
    foreach ($match['units'][$side] ?? [] as $candidate => $candidatePlayer) {
      $touchRadius = player_touch_radius($candidatePlayer, $isHigh) + $interceptExtra;
      $dist = distance2($candidatePlayer, $ball);
      if ($dist > $touchRadius * $touchRadius) continue;
      $targeted = is_array($target) && ($target['side'] ?? '') === $side && ($target['id'] ?? '') === ($candidatePlayer['id'] ?? null);
      $score = $dist / max(0.00001, $touchRadius * $touchRadius)
        - player_touch_control($candidatePlayer, $isHigh) / 700
        - ($targeted ? 0.18 : 0.0);
      if ($score < $bestScore) {
        $bestScore = $score;
        $bestSide = $side;
        $bestIndex = (int)$candidate;
      }
    }
  }
  if ($bestSide === null || $bestIndex === null) return;
  $player = $match['units'][$bestSide][$bestIndex];
  $controlStat = player_touch_control($player, $isHigh);

  $match['engine']['possession'][$bestSide] += $step;
  telemetry_add($match, $bestSide, $player, ['possessionSeconds' => $step, 'touches' => 1]);
  $touchKey = $bestSide . ':' . ($player['id'] ?? $bestIndex);
  if (($match['engine']['lastTouch'] ?? null) !== $touchKey || ((float)$match['elapsed'] - (float)($match['engine']['lastTouchAt'] ?? -10)) > 0.6) {
    $match['engine']['touches'][$bestSide]++;
    $match['engine']['lastTouch'] = $touchKey;
    $match['engine']['lastTouchAt'] = (float)$match['elapsed'];
  }

  $targeted = is_array($target) && ($target['side'] ?? '') === $bestSide && ($target['id'] ?? '') === ($player['id'] ?? null);
  $quality = is_array($target) ? (float)($target['quality'] ?? 0.5) : 0.45;
  $catchChance = clamp_float(0.18 + $controlStat / 130 + ($targeted ? $quality * 0.34 : 0.0) - $incomingSpeed * 0.70, 0.05, 0.96);
  if (($player['role'] ?? '') === 'GK') $catchChance = clamp_float($catchChance + player_stat($player, 'keeping') / 220, 0.10, 0.98);
  if (rand_float() < $catchChance) {
    set_owner($match, $bestSide, $bestIndex);
    place_ball_on_player($ball, $player, $bestSide);
    $match['ball'] = $ball;
    $match['engine']['passTarget'] = null;
    return;
  }

  $attackDir = attack_dir($bestSide);
  $skill = $isHigh ? player_stat($player, 'heading') : (player_stat($player, 'passcontrol') * 0.7 + player_stat($player, 'composure') * 0.3);
  $looseTouch = player_ball_release_point($player, $bestSide, 0.82);
  $ball['x'] = $looseTouch['x'];
  $ball['y'] = $looseTouch['y'];
  $ball['vx'] = (rand_float() * 2 - 1) * (0.05 + (100 - $skill) / 900);
  $ball['vy'] = $attackDir * (0.09 + $skill / 900);
  $ball['vz'] = $isHigh ? 0.05 : 0.0;
  $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.26;
}

function tick_match_fixed_dt(array &$match, float $dt, bool $updateWallClock = false): void {
  if (($match['status'] ?? '') !== 'active') return;
  ensure_match_engine($match);
  $dt = max(0.0, min(2.0, $dt));
  if ($dt <= 0) return;
  if ($updateWallClock) {
    $match['lastTick'] = microtime(true);
    $match['updated'] = time();
  }
  $steps = max(1, (int)ceil($dt / 0.05));
  $step = $dt / $steps;
  $clockStep = $step * (MATCH_GAME_MINUTES / MATCH_REAL_SECONDS);
  $goalMin = goal_min_x();
  $goalMax = goal_max_x();
  for ($i = 0; $i < $steps; $i++) {
    $ball = $match['ball'];
    $kickoffUntil = (float)($match['engine']['kickoffUntil'] ?? 0.0);
    if ($kickoffUntil > (float)$match['elapsed']) {
      $match['ball'] = stopped_kickoff_ball();
      $match['elapsed'] += $clockStep;
      continue;
    }
    if (!empty($match['engine']['kickoffDirection'])) {
      $kickoffSide = in_array($match['engine']['kickoffSide'] ?? '', ['home', 'away'], true)
        ? (string)$match['engine']['kickoffSide']
        : (((int)$match['engine']['kickoffDirection']) > 0 ? 'away' : 'home');
      if (empty($match['engine']['kickoffWhistled'])) {
        $match['engine']['kickoffWhistled'] = true;
        $match['engine']['kickoffWhistleUntil'] = (float)$match['elapsed'] + 0.42;
        $match['engine']['kickoffWhistleSeq'] = (int)($match['engine']['kickoffWhistleSeq'] ?? 0) + 1;
        $match['ball'] = stopped_kickoff_ball();
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['kickoffWhistleUntil'] ?? 0.0) > (float)$match['elapsed']) {
        arrange_kickoff_players($match, $kickoffSide, $step);
        $match['ball'] = stopped_kickoff_ball();
        $match['elapsed'] += $clockStep;
        continue;
      }
      $ball = kickoff_ball_for_side($match, $kickoffSide);
      $match['ball'] = $ball;
      $match['engine']['kickoffDirection'] = null;
      $match['engine']['kickoffUntil'] = 0.0;
      $match['engine']['ballFreeUntil'] = (float)$match['elapsed'] + 0.18;
    }
    if (!empty($match['engine']['restartType'])) {
      $restartType = (string)$match['engine']['restartType'];
      $restartSide = in_array($match['engine']['restartSide'] ?? '', ['home', 'away'], true) ? (string)$match['engine']['restartSide'] : 'home';
      $restartSpot = is_array($match['engine']['restartSpot'] ?? null) ? $match['engine']['restartSpot'] : ['x' => 0.5, 'y' => 0.5];
      $restartBall = stopped_ball_at((float)($restartSpot['x'] ?? 0.5), (float)($restartSpot['y'] ?? 0.5));
      $restartStarted = (float)($match['engine']['restartStartedAt'] ?? $match['elapsed']);
      $restartLine = is_array($match['engine']['restartLineBall'] ?? null) ? $match['engine']['restartLineBall'] : $restartBall;
      $restartOut = is_array($match['engine']['restartOutBall'] ?? null) ? $match['engine']['restartOutBall'] : $restartBall;
      $outBall = [
        'x' => (float)($restartOut['x'] ?? $restartBall['x']),
        'y' => (float)($restartOut['y'] ?? $restartBall['y']),
        'z' => 0.0,
        'vx' => 0.0,
        'vy' => 0.0,
        'vz' => 0.0,
      ];
      if ((float)($match['engine']['restartRollUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $rollSpan = max(0.001, (float)($match['engine']['restartRollUntil'] ?? $match['elapsed']) - $restartStarted);
        $rollT = clamp_float(((float)$match['elapsed'] - $restartStarted) / $rollSpan, 0.0, 1.0);
        $rollT = $rollT * $rollT * (3.0 - 2.0 * $rollT);
        $match['ball'] = [
          'x' => (float)($restartLine['x'] ?? $outBall['x']) + ($outBall['x'] - (float)($restartLine['x'] ?? $outBall['x'])) * $rollT,
          'y' => (float)($restartLine['y'] ?? $outBall['y']) + ($outBall['y'] - (float)($restartLine['y'] ?? $outBall['y'])) * $rollT,
          'z' => 0.0,
          'vx' => 0.0,
          'vy' => 0.0,
          'vz' => 0.0,
        ];
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['restartDeliveryUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $match['ball'] = $outBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['restartUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $match['ball'] = $restartBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if (empty($match['engine']['restartWhistled'])) {
        $match['engine']['restartWhistled'] = true;
        $match['engine']['restartSignalUntil'] = (float)$match['elapsed'] + 0.42;
        $match['engine']['restartSeq'] = (int)($match['engine']['restartSeq'] ?? 0) + 1;
        $match['ball'] = $restartBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['restartSignalUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $match['ball'] = $restartBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      take_restart($match, $restartSide, $restartType, $restartBall);
      $ball = $match['ball'];
    }
    $penaltySide = in_array($match['engine']['penaltySide'] ?? '', ['home', 'away'], true) ? (string)$match['engine']['penaltySide'] : '';
    if ($penaltySide !== '') {
      $spot = penalty_spot_for_side($penaltySide);
      $penaltyBall = stopped_ball_at($spot['x'], $spot['y']);
      if ((float)($match['engine']['penaltyUntil'] ?? 0.0) > (float)$match['elapsed']) {
        prepare_penalty_units($match, $penaltySide);
        $match['ball'] = $penaltyBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if (empty($match['engine']['penaltyWhistled'])) {
        $match['engine']['penaltyWhistled'] = true;
        $match['engine']['penaltySignalUntil'] = (float)$match['elapsed'] + 0.42;
        $match['engine']['restartSeq'] = (int)($match['engine']['restartSeq'] ?? 0) + 1;
        $match['ball'] = $penaltyBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      if ((float)($match['engine']['penaltySignalUntil'] ?? 0.0) > (float)$match['elapsed']) {
        $match['ball'] = $penaltyBall;
        $match['elapsed'] += $clockStep;
        continue;
      }
      take_penalty($match, $penaltySide);
      $ball = $match['ball'];
    }
    $freeKickUntil = (float)($match['engine']['freeKickUntil'] ?? 0.0);
    if ($freeKickUntil > (float)$match['elapsed']) {
      $spot = is_array($match['engine']['freeKickSpot'] ?? null) ? $match['engine']['freeKickSpot'] : ['x' => 0.5, 'y' => 0.5];
      $fkSide = in_array($match['engine']['freeKickSide'] ?? '', ['home', 'away'], true) ? $match['engine']['freeKickSide'] : 'home';
      arrange_free_kick_players($match, $fkSide, $spot, $step);
      $match['ball'] = stopped_ball_at((float)($spot['x'] ?? 0.5), (float)($spot['y'] ?? 0.5));
      $match['elapsed'] += $clockStep;
      continue;
    }
    $freeKickSignalUntil = (float)($match['engine']['freeKickSignalUntil'] ?? 0.0);
    if ($freeKickSignalUntil > (float)$match['elapsed']) {
      $spot = is_array($match['engine']['freeKickSpot'] ?? null) ? $match['engine']['freeKickSpot'] : ['x' => 0.5, 'y' => 0.5];
      $fkSide = in_array($match['engine']['freeKickSide'] ?? '', ['home', 'away'], true) ? $match['engine']['freeKickSide'] : 'home';
      arrange_free_kick_players($match, $fkSide, $spot, $step);
      $match['ball'] = stopped_ball_at((float)($spot['x'] ?? 0.5), (float)($spot['y'] ?? 0.5));
      $match['elapsed'] += $clockStep;
      continue;
    }
    if (!empty($match['engine']['freeKickSide'])) {
      $fkSide = in_array($match['engine']['freeKickSide'], ['home', 'away'], true) ? $match['engine']['freeKickSide'] : 'home';
      $spot = is_array($match['engine']['freeKickSpot'] ?? null) ? $match['engine']['freeKickSpot'] : ['x' => 0.5, 'y' => 0.5];
      $ball = stopped_ball_at((float)($spot['x'] ?? 0.5), (float)($spot['y'] ?? 0.5));
      $kickIndex = free_kick_kicker_index($match, $fkSide, $ball);
      if ($kickIndex !== null && isset($match['units'][$fkSide][$kickIndex])) {
        $dist = sqrt(distance2($match['units'][$fkSide][$kickIndex], $ball));
        if ($dist > 0.035 && (float)$match['elapsed'] < max($freeKickUntil, $freeKickSignalUntil) + 0.90) {
          arrange_free_kick_players($match, $fkSide, $ball, $step);
          $match['ball'] = $ball;
          $match['elapsed'] += $clockStep;
          continue;
        }
        $match['units'][$fkSide][$kickIndex]['x'] = $ball['x'];
        $match['units'][$fkSide][$kickIndex]['y'] = clamp_float((float)$ball['y'], 0.03, 0.97);
        take_free_kick($match, $fkSide, $kickIndex);
        $ball = $match['ball'];
      } else {
        $match['engine']['freeKickSide'] = null;
        $match['engine']['freeKickSpot'] = null;
        $match['engine']['freeKickKicker'] = null;
        $match['engine']['freeKickUntil'] = 0.0;
        $match['engine']['freeKickSignalUntil'] = 0.0;
      }
      $match['ball'] = $ball;
    }
    $shortLineRunoff = ball_short_line_crossed($ball) && !ball_short_line_visible_out($ball);
    if ($shortLineRunoff) clear_owner($match);
    if (!$shortLineRunoff) {
      move_players($match, $ball, $step);
      apply_player_touch($match, $ball, $step);
      if (owner_ref($match)) {
        $match['ball'] = $ball;
        $match['elapsed'] += $clockStep;
        continue;
      }
    }
    $hasShot = !empty($match['engine']['shot']);
    $airPass = is_array($match['engine']['passTarget'] ?? null)
      && (($match['engine']['passTarget']['type'] ?? '') === 'long')
      && ((float)($ball['z'] ?? 0.0) > 0.0 || (float)($ball['vz'] ?? 0.0) > 0.0);
    $airborne = $hasShot || $airPass;
    if (!$airborne) {
      $ball['z'] = 0.0;
      $ball['vz'] = 0.0;
    }
    $ball['vx'] = (float)($ball['vx'] ?? 0.0);
    $ball['vy'] = (float)($ball['vy'] ?? 0.0);
    $ball['vz'] = (float)($ball['vz'] ?? 0.0);
    $speed = sqrt($ball['vx'] * $ball['vx'] + $ball['vy'] * $ball['vy']);
    if ($speed > BALL_MAX_SPEED) {
      $ball['vx'] *= BALL_MAX_SPEED / $speed;
      $ball['vy'] *= BALL_MAX_SPEED / $speed;
      $speed = BALL_MAX_SPEED;
    }
    $ball['x'] += $ball['vx'] * $step;
    $ball['y'] += $ball['vy'] * $step;

    if ($airborne) {
      $ball['z'] = max(0.0, (float)($ball['z'] ?? 0.0) + $ball['vz'] * $step);
      if ($ball['z'] > 0.0 || $ball['vz'] > 0.0) {
        $ball['vz'] -= BALL_GRAVITY * $step;
        $drag = exp(-BALL_AIR_DRAG * $step);
        $ball['vx'] *= $drag;
        $ball['vy'] *= $drag;
      }
      if ($ball['z'] <= 0.0 && $ball['vz'] < 0.0) {
        $ball['z'] = 0.0;
        if ($airPass) {
          $ball['vz'] = 0.0;
          $ball['vx'] *= 0.82;
          $ball['vy'] *= 0.82;
        } else {
          $ball['vz'] *= -BALL_BOUNCE;
          $ball['vx'] *= 0.92;
          $ball['vy'] *= 0.92;
          if (abs($ball['vz']) < 0.022) $ball['vz'] = 0.0;
        }
      }
    }

    if (!$airborne || (float)($ball['z'] ?? 0.0) <= 0.0) {
      $ball['z'] = 0.0;
      if (!$hasShot || $ball['vz'] <= 0.0) $ball['vz'] = 0.0;
      $speed = sqrt($ball['vx'] * $ball['vx'] + $ball['vy'] * $ball['vy']);
      if ($speed > 0.0) {
        $newSpeed = max(0.0, $speed - ($hasShot ? BALL_SHOT_ROLL_DECEL : BALL_ROLL_DECEL) * $step);
        if ($newSpeed < BALL_STOP_SPEED) {
          $ball['vx'] = 0.0;
          $ball['vy'] = 0.0;
        } else {
          $ball['vx'] *= $newSpeed / $speed;
          $ball['vy'] *= $newSpeed / $speed;
        }
      }
      if ($hasShot && sqrt($ball['vx'] * $ball['vx'] + $ball['vy'] * $ball['vy']) < 0.035) {
        $match['engine']['shot'] = null;
      }
    }
    if ($ball['x'] < 0.02 || $ball['x'] > 0.98) {
      $ball['x'] = max(0.02, min(0.98, $ball['x']));
      $ball['vx'] = 0.0;
      $ball['vy'] *= 0.82;
    }
    if (!ball_short_line_crossed($ball) && apply_keeper_save($match, $ball)) {
      $match['ball'] = $ball;
      $match['elapsed'] += $clockStep;
      continue;
    }
    if (ball_short_line_crossed($ball) && (ball_short_line_visible_out($ball) || ball_short_line_stalled_out($ball))) {
      $topLine = $ball['y'] < 0;
      $inGoal = $ball['x'] >= $goalMin && $ball['x'] <= $goalMax;
      $scoringSide = $topLine ? 'home' : 'away';
      $kickoffSide = $topLine ? 'away' : 'home';
      $scorer = null;
      if ($inGoal && $topLine) {
        $match['score'][0]++;
        $scorer = goal_scorer_name($match, 'home');
      } elseif ($inGoal) {
        $match['score'][1]++;
        $scorer = goal_scorer_name($match, 'away');
      }
      if ($inGoal) {
        $scorer = $scorer ?? goal_scorer_name($match, $scoringSide);
        $shot = $match['engine']['shot'] ?? null;
        $isPenaltyGoal = is_array($shot) && !empty($shot['penalty']);
        $assist = $isPenaltyGoal ? null : goal_assist_name($match, $scoringSide, $scorer);
        $scorerId = is_array($shot) ? (string)($shot['id'] ?? '') : '';
        $goalText = side_team_name($match, $scoringSide) . ' GOAL' . ($scorer ? ' - ' . $scorer : '') . ($assist ? ' | ASSIST: ' . $assist : '');
        $match['events'][] = ['t' => round((float)$match['elapsed']), 'text' => $goalText];
        $match['engine']['goalSeq'] = (int)($match['engine']['goalSeq'] ?? 0) + 1;
        $match['engine']['lastGoal'] = [
          'seq' => $match['engine']['goalSeq'],
          'at' => (float)$match['elapsed'],
          'side' => $scoringSide,
          'team' => side_team_name($match, $scoringSide),
          'scorer' => $scorer,
          'scorerId' => $scorerId,
          'assist' => $assist,
          'penalty' => $isPenaltyGoal,
        ];
        $match['engine']['goalScorers'][] = $match['engine']['lastGoal'];
      }
      clear_owner($match);
      $match['engine']['passTarget'] = null;
      $match['engine']['shot'] = null;
      if ($inGoal) {
        begin_kickoff($match, $kickoffSide, KICKOFF_PAUSE_GAME_MINUTES);
        $ball = $match['ball'];
      } else {
        $attackingSide = $topLine ? 'home' : 'away';
        $defendingSide = opponent_side($attackingSide);
        $lastSide = last_touch_side($match) ?? $attackingSide;
        if ($lastSide === $attackingSide) {
          $spot = ['x' => 0.5, 'y' => $topLine ? 0.085 : 0.915];
          $outBall = ['x' => clamp_float((float)$ball['x'], 0.02, 0.98), 'y' => $topLine ? -0.26 : 1.26];
          begin_restart($match, 'goal_kick', $defendingSide, $spot, $outBall, $ball);
        } else {
          $spot = [
            'x' => (float)$ball['x'] < 0.5 ? 0.018 : 0.982,
            'y' => $topLine ? 0.018 : 0.982,
          ];
          $outBall = ['x' => clamp_float((float)$ball['x'], 0.02, 0.98), 'y' => $topLine ? -0.26 : 1.26];
          begin_restart($match, 'corner', $attackingSide, $spot, $outBall, $ball);
        }
        $ball = $match['ball'];
      }
    }
    $match['ball'] = $ball;
    $match['elapsed'] += $clockStep;
  }
  if ($match['elapsed'] >= MATCH_GAME_MINUTES) {
    $match['elapsed'] = MATCH_GAME_MINUTES;
    $match['status'] = 'finished';
  }
  if (count($match['events']) > 8) $match['events'] = array_slice($match['events'], -8);
}

function tick_match(array &$match): void {
  if (($match['status'] ?? '') !== 'active') return;
  $now = microtime(true);
  $dt = max(0.0, min(2.0, $now - (float)($match['lastTick'] ?? $now)));
  tick_match_fixed_dt($match, $dt, true);
}

function public_referee(array $match): array {
  $engine = $match['engine'] ?? default_engine();
  $ball = $match['ball'] ?? ['x' => 0.5, 'y' => 0.5];
  $freeKickSpot = is_array($engine['freeKickSpot'] ?? null) ? $engine['freeKickSpot'] : null;
  $restartSpot = is_array($engine['restartSpot'] ?? null) ? $engine['restartSpot'] : null;
  $penaltySpot = in_array($engine['penaltySide'] ?? '', ['home', 'away'], true) ? penalty_spot_for_side((string)$engine['penaltySide']) : null;
  $spot = $freeKickSpot ?: ($penaltySpot ?: $restartSpot);
  $x = 0.975;
  $kickoffWhistle = (float)($engine['kickoffWhistleUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0);
  $restartWhistle = (float)($engine['restartSignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0);
  $penaltyWhistle = (float)($engine['penaltySignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0);
  $y = $spot ? (float)($spot['y'] ?? 0.5) : ($kickoffWhistle ? 0.5 : (float)($ball['y'] ?? 0.5) + 0.03);
  $card = null;
  $whistle = $kickoffWhistle || $restartWhistle || $penaltyWhistle;
  $elapsed = (float)($match['elapsed'] ?? 0);
  foreach (array_reverse($match['events'] ?? []) as $event) {
    $age = $elapsed - (float)($event['t'] ?? $elapsed);
    $text = (string)($event['text'] ?? '');
    if ($age > 1.6) break;
    if (str_contains($text, 'YELLOW CARD')) $card = 'yellow';
    if (str_contains($text, 'RED CARD')) $card = 'red';
    if (str_contains($text, 'FREE KICK') || str_contains($text, 'PENALTY') || str_contains($text, 'GOAL')) $whistle = true;
  }
  return [
    'x' => clamp_float($x, 0.02, 0.98),
    'y' => clamp_float($y, 0.08, 0.92),
    'card' => $card,
    'whistle' => $whistle,
  ];
}

function public_match(array $match, string $user): array {
  ensure_match_engine($match);
  $side = (($match['players']['home'] ?? null) === $user) ? 'home' : 'away';
  $engine = $match['engine'] ?? default_engine();
  return [
    'id' => $match['id'],
    'status' => $match['status'],
    'side' => $side,
    'elapsed' => round((float)($match['elapsed'] ?? 0), 2),
    'score' => $match['score'],
    'players' => $match['players'],
    'teams' => $match['teams'],
    'units' => $match['units'] ?? ['home' => [], 'away' => []],
    'bench' => $match['bench'] ?? ['home' => [], 'away' => []],
    'ratings' => [
      'home' => average_rating($match['units']['home'] ?? []),
      'away' => average_rating($match['units']['away'] ?? []),
    ],
    'managerFixture' => !empty($match['managerFixture']) ? [
      'seasonId' => (string)($match['managerFixture']['seasonId'] ?? ''),
      'roundIndex' => (int)($match['managerFixture']['roundIndex'] ?? 0),
      'fixtureIndex' => (int)($match['managerFixture']['fixtureIndex'] ?? 0),
      'round' => (string)($match['managerFixture']['round'] ?? ''),
      'phase' => (string)($match['managerFixture']['phase'] ?? ''),
      'playoff' => !empty($match['managerFixture']['playoff']),
      'cupName' => (string)($match['managerFixture']['cupName'] ?? ''),
    ] : null,
    'engine' => [
      'touches' => $engine['touches'] ?? ['home' => 0, 'away' => 0],
      'possession' => $engine['possession'] ?? ['home' => 0, 'away' => 0],
      'shots' => $engine['shots'] ?? ['home' => 0, 'away' => 0],
      'tackles' => $engine['tackles'] ?? ['home' => 0, 'away' => 0],
      'fouls' => $engine['fouls'] ?? ['home' => 0, 'away' => 0],
      'yellows' => $engine['yellows'] ?? ['home' => 0, 'away' => 0],
      'reds' => $engine['reds'] ?? ['home' => 0, 'away' => 0],
      'subsUsed' => $engine['subsUsed'] ?? ['home' => 0, 'away' => 0],
      'saves' => $engine['saves'] ?? ['home' => 0, 'away' => 0],
      'crits' => $engine['crits'] ?? ['home' => 0, 'away' => 0],
      'owner' => $engine['owner'] ?? null,
      'lastTouch' => $engine['lastTouch'] ?? null,
      'lastTouchAt' => $engine['lastTouchAt'] ?? null,
      'lastShotSeq' => $engine['lastShotSeq'] ?? 0,
      'lastShotAt' => $engine['lastShotAt'] ?? null,
      'lastShotBy' => $engine['lastShotBy'] ?? null,
      'lastGoal' => $engine['lastGoal'] ?? null,
      'goalScorers' => $engine['goalScorers'] ?? [],
      'passTarget' => $engine['passTarget'] ?? null,
      'shot' => $engine['shot'] ?? null,
      'kickoff' => [
        'side' => $engine['kickoffSide'] ?? null,
        'taker' => $engine['kickoffTaker'] ?? null,
        'receiver' => $engine['kickoffReceiver'] ?? null,
        'waiting' => !empty($engine['kickoffDirection']) && (float)($engine['kickoffUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
        'signal' => (float)($engine['kickoffWhistleUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
        'seq' => (int)($engine['kickoffWhistleSeq'] ?? 0),
      ],
      'restart' => [
        'type' => $engine['restartType'] ?? null,
        'side' => $engine['restartSide'] ?? null,
        'spot' => $engine['restartSpot'] ?? null,
        'line' => $engine['restartLineBall'] ?? null,
        'out' => $engine['restartOutBall'] ?? null,
        'kicker' => $engine['restartKicker'] ?? null,
        'startedAt' => isset($engine['restartStartedAt']) ? round((float)$engine['restartStartedAt'], 2) : null,
        'rollUntil' => isset($engine['restartRollUntil']) ? round((float)$engine['restartRollUntil'], 2) : null,
        'deliveryUntil' => isset($engine['restartDeliveryUntil']) ? round((float)$engine['restartDeliveryUntil'], 2) : null,
        'waiting' => !empty($engine['restartType']) && (float)($engine['restartUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
        'signal' => (float)($engine['restartSignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
        'seq' => (int)($engine['restartSeq'] ?? 0),
      ],
      'freeKick' => [
        'side' => $engine['freeKickSide'] ?? null,
        'spot' => $engine['freeKickSpot'] ?? null,
        'kicker' => $engine['freeKickKicker'] ?? null,
        'signal' => (float)($match['elapsed'] ?? 0.0) >= (float)($engine['freeKickUntil'] ?? 0.0)
          && (float)($engine['freeKickSignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
      ],
      'penalty' => [
        'side' => $engine['penaltySide'] ?? null,
        'kicker' => $engine['penaltyKicker'] ?? null,
        'spot' => in_array($engine['penaltySide'] ?? '', ['home', 'away'], true) ? penalty_spot_for_side((string)$engine['penaltySide']) : null,
        'signal' => (float)($engine['penaltySignalUntil'] ?? 0.0) > (float)($match['elapsed'] ?? 0.0),
      ],
    ],
    'referee' => public_referee($match),
    'tactics' => $match['tactics'],
    'ball' => $match['ball'],
    'events' => $match['events'] ?? [],
  ];
}
