Created
January 15, 2026 17:44
-
-
Save ninjadynamics/61e02d1b35763921dd1d00dce5d969bf to your computer and use it in GitHub Desktop.
Vibe Soccer (by Claude Opus 4.5)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Soccer Game - CPU vs CPU with Strategic AI | |
| // Compile: gcc soccer.c -o soccer -lraylib -lm | |
| #include "raylib.h" | |
| #include <math.h> | |
| #include <stdlib.h> | |
| #include <stdio.h> | |
| #include <string.h> | |
| #define SCREEN_WIDTH 1360 | |
| #define SCREEN_HEIGHT 720 | |
| #define FIELD_MARGIN_X 80 | |
| #define FIELD_MARGIN_Y 60 | |
| #define FIELD_WIDTH (SCREEN_WIDTH - 2 * FIELD_MARGIN_X) | |
| #define FIELD_HEIGHT (SCREEN_HEIGHT - 2 * FIELD_MARGIN_Y) | |
| #define FIELD_LEFT FIELD_MARGIN_X | |
| #define FIELD_RIGHT (SCREEN_WIDTH - FIELD_MARGIN_X) | |
| #define FIELD_TOP FIELD_MARGIN_Y | |
| #define FIELD_BOTTOM (SCREEN_HEIGHT - FIELD_MARGIN_Y) | |
| #define FIELD_CENTER_X (SCREEN_WIDTH / 2) | |
| #define FIELD_CENTER_Y (SCREEN_HEIGHT / 2) | |
| #define GOAL_WIDTH 40 | |
| #define GOAL_HEIGHT 140 | |
| #define GOAL_TOP ((SCREEN_HEIGHT - GOAL_HEIGHT) / 2) | |
| #define GOAL_BOTTOM ((SCREEN_HEIGHT + GOAL_HEIGHT) / 2) | |
| #define PENALTY_AREA_WIDTH 130 | |
| #define PENALTY_AREA_HEIGHT 320 | |
| #define GOAL_AREA_WIDTH 50 | |
| #define GOAL_AREA_HEIGHT 160 | |
| #define PLAYER_RADIUS 18 | |
| #define BALL_RADIUS 10 | |
| #define PLAYERS_PER_TEAM 11 | |
| #define MAX_SPEED 4.0f | |
| #define GK_MAX_SPEED 3.2f | |
| #define BALL_FRICTION 0.982f | |
| #define KICK_POWER 8.0f | |
| #define PASS_POWER 7.5f | |
| #define SHOT_POWER 12.0f | |
| #define CONTROL_DISTANCE 25.0f | |
| #define MIN_TEAMMATE_DISTANCE 80.0f | |
| #define LANE_WIDTH (FIELD_HEIGHT / 5) | |
| #define SETPIECE_CLEAR_RADIUS 80.0f | |
| // Reaction time settings (in seconds) | |
| #define REACTION_TIME_MIN 0.20f | |
| #define REACTION_TIME_MAX 0.35f | |
| #define SEEK_WEIGHT 1.0f | |
| #define ARRIVE_WEIGHT 1.0f | |
| #define SEPARATION_WEIGHT 3.5f | |
| typedef enum { | |
| STATE_KICKOFF, STATE_PLAYING, STATE_GOAL_SCORED, | |
| STATE_THROW_IN, STATE_CORNER, STATE_GOAL_KICK | |
| } GameState; | |
| typedef enum { | |
| ROLE_GOALKEEPER, ROLE_DEFENDER, ROLE_MIDFIELDER, ROLE_ATTACKER | |
| } PlayerRole; | |
| typedef enum { TEAM_A = 0, TEAM_B = 1 } Team; | |
| typedef struct { | |
| int defenders, midfielders, attackers; | |
| const char* name; | |
| } Formation; | |
| Formation formations[] = { | |
| {4, 4, 2, "4-4-2"}, {4, 3, 3, "4-3-3"}, {3, 5, 2, "3-5-2"}, | |
| {4, 5, 1, "4-5-1"}, {3, 4, 3, "3-4-3"}, {5, 3, 2, "5-3-2"}, {5, 4, 1, "5-4-1"} | |
| }; | |
| #define NUM_FORMATIONS 7 | |
| Vector2 Vec2Add(Vector2 a, Vector2 b) { return (Vector2){a.x + b.x, a.y + b.y}; } | |
| Vector2 Vec2Sub(Vector2 a, Vector2 b) { return (Vector2){a.x - b.x, a.y - b.y}; } | |
| Vector2 Vec2Scale(Vector2 v, float s) { return (Vector2){v.x * s, v.y * s}; } | |
| float Vec2Length(Vector2 v) { return sqrtf(v.x * v.x + v.y * v.y); } | |
| float Vec2Distance(Vector2 a, Vector2 b) { return Vec2Length(Vec2Sub(a, b)); } | |
| Vector2 Vec2Normalize(Vector2 v) { | |
| float len = Vec2Length(v); | |
| return (len > 0.0001f) ? Vec2Scale(v, 1.0f / len) : (Vector2){0, 0}; | |
| } | |
| Vector2 Vec2Truncate(Vector2 v, float max) { | |
| float len = Vec2Length(v); | |
| return (len > max) ? Vec2Scale(Vec2Normalize(v), max) : v; | |
| } | |
| float Vec2Dot(Vector2 a, Vector2 b) { return a.x * b.x + a.y * b.y; } | |
| float ClampF(float val, float min, float max) { | |
| return (val < min) ? min : ((val > max) ? max : val); | |
| } | |
| float RandomFloat(float min, float max) { | |
| return min + (float)GetRandomValue(0, 10000) / 10000.0f * (max - min); | |
| } | |
| typedef struct { | |
| Vector2 pos, vel, homePos, targetPos, facing; | |
| Team team; | |
| PlayerRole role; | |
| int number, laneIndex, roleIndex; | |
| bool hasBall; | |
| float kickCooldown, supportTimer; | |
| float reactionTimer; // Time until player can react to ball | |
| } Player; | |
| typedef struct { | |
| Vector2 pos, vel; | |
| Player* owner; | |
| Team lastTouchedBy; | |
| } Ball; | |
| typedef struct { | |
| Player players[2][PLAYERS_PER_TEAM]; | |
| Ball ball; | |
| GameState state; | |
| Team kickoffTeam, lastGoalTeam, setpieceTeam; | |
| int scoreA, scoreB; | |
| float stateTimer; | |
| Vector2 setpiecePos; | |
| Formation teamAFormation, teamBFormation; | |
| bool offsideEnabled; | |
| } Game; | |
| Game game; | |
| void ResetPositions(Team kickoffTeam); | |
| Vector2 GetFormationPosition(Team team, int playerIndex, Formation formation); | |
| float GetOffsideLine(Team attackingTeam); | |
| bool IsOffside(Player* player); | |
| Vector2 GetStrategicPosition(Player* player); | |
| float GetLaneY(int laneIndex) { | |
| return FIELD_TOP + (FIELD_HEIGHT / 5.0f) * (laneIndex + 0.5f); | |
| } | |
| void AssignLanes(Team team, Formation formation) { | |
| int defCount = formation.defenders; | |
| int midCount = formation.midfielders; | |
| int atkCount = formation.attackers; | |
| game.players[team][0].laneIndex = 2; | |
| game.players[team][0].roleIndex = 0; | |
| for (int i = 0; i < defCount; i++) { | |
| Player* p = &game.players[team][1 + i]; | |
| p->roleIndex = i; | |
| if (defCount == 3) { int lanes[] = {1, 2, 3}; p->laneIndex = lanes[i]; } | |
| else if (defCount == 4) { int lanes[] = {0, 1, 3, 4}; p->laneIndex = lanes[i]; } | |
| else if (defCount == 5) { p->laneIndex = i; } | |
| } | |
| for (int i = 0; i < midCount; i++) { | |
| Player* p = &game.players[team][1 + defCount + i]; | |
| p->roleIndex = i; | |
| if (midCount == 3) { int lanes[] = {1, 2, 3}; p->laneIndex = lanes[i]; } | |
| else if (midCount == 4) { int lanes[] = {0, 1, 3, 4}; p->laneIndex = lanes[i]; } | |
| else if (midCount == 5) { p->laneIndex = i; } | |
| } | |
| for (int i = 0; i < atkCount; i++) { | |
| Player* p = &game.players[team][1 + defCount + midCount + i]; | |
| p->roleIndex = i; | |
| if (atkCount == 1) { p->laneIndex = 2; } | |
| else if (atkCount == 2) { int lanes[] = {1, 3}; p->laneIndex = lanes[i]; } | |
| else if (atkCount == 3) { int lanes[] = {1, 2, 3}; p->laneIndex = lanes[i]; } | |
| } | |
| } | |
| // Trigger reaction time for all players (called when ball changes state) | |
| void TriggerReactionTime(Player* excludePlayer) { | |
| for (int t = 0; t < 2; t++) { | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* p = &game.players[t][i]; | |
| if (p != excludePlayer && p->reactionTimer <= 0) { | |
| p->reactionTimer = RandomFloat(REACTION_TIME_MIN, REACTION_TIME_MAX); | |
| } | |
| } | |
| } | |
| } | |
| void InitGame(void) { | |
| game.scoreA = 0; | |
| game.scoreB = 0; | |
| game.state = STATE_KICKOFF; | |
| game.kickoffTeam = GetRandomValue(0, 1) == 0 ? TEAM_A : TEAM_B; | |
| game.offsideEnabled = true; | |
| game.teamAFormation = formations[GetRandomValue(0, NUM_FORMATIONS - 1)]; | |
| game.teamBFormation = formations[GetRandomValue(0, NUM_FORMATIONS - 1)]; | |
| game.ball.pos = (Vector2){FIELD_CENTER_X, FIELD_CENTER_Y}; | |
| game.ball.vel = (Vector2){0, 0}; | |
| game.ball.owner = NULL; | |
| game.ball.lastTouchedBy = game.kickoffTeam; | |
| for (int t = 0; t < 2; t++) { | |
| Team team = (Team)t; | |
| Formation form = (team == TEAM_A) ? game.teamAFormation : game.teamBFormation; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* p = &game.players[t][i]; | |
| p->team = team; | |
| p->number = i + 1; | |
| p->hasBall = false; | |
| p->kickCooldown = 0; | |
| p->supportTimer = RandomFloat(0, 3.0f); | |
| p->reactionTimer = 0; | |
| if (i == 0) p->role = ROLE_GOALKEEPER; | |
| else if (i <= form.defenders) p->role = ROLE_DEFENDER; | |
| else if (i <= form.defenders + form.midfielders) p->role = ROLE_MIDFIELDER; | |
| else p->role = ROLE_ATTACKER; | |
| p->homePos = GetFormationPosition(team, i, form); | |
| p->pos = p->homePos; | |
| p->targetPos = p->homePos; | |
| p->vel = (Vector2){0, 0}; | |
| p->facing = (team == TEAM_A) ? (Vector2){1, 0} : (Vector2){-1, 0}; | |
| } | |
| AssignLanes(team, form); | |
| } | |
| ResetPositions(game.kickoffTeam); | |
| } | |
| Vector2 GetFormationPosition(Team team, int playerIndex, Formation formation) { | |
| float x, y; | |
| int defCount = formation.defenders; | |
| int midCount = formation.midfielders; | |
| int atkCount = formation.attackers; | |
| float gkZone = 0.05f, defZone = 0.18f, midZone = 0.40f, atkZone = 0.72f; | |
| if (playerIndex == 0) { x = gkZone; y = 0.5f; } | |
| else if (playerIndex <= defCount) { | |
| int idx = playerIndex - 1; | |
| x = defZone; | |
| y = 0.08f + (0.84f / (defCount + 1)) * (idx + 1); | |
| } else if (playerIndex <= defCount + midCount) { | |
| int idx = playerIndex - defCount - 1; | |
| x = midZone; | |
| y = 0.08f + (0.84f / (midCount + 1)) * (idx + 1); | |
| } else { | |
| int idx = playerIndex - defCount - midCount - 1; | |
| x = atkZone; | |
| y = 0.15f + (0.7f / (atkCount + 1)) * (idx + 1); | |
| } | |
| if (team == TEAM_A) return (Vector2){FIELD_LEFT + x * FIELD_WIDTH, FIELD_TOP + y * FIELD_HEIGHT}; | |
| else return (Vector2){FIELD_RIGHT - x * FIELD_WIDTH, FIELD_TOP + y * FIELD_HEIGHT}; | |
| } | |
| void ResetPositions(Team kickoffTeam) { | |
| game.ball.pos = (Vector2){FIELD_CENTER_X, FIELD_CENTER_Y}; | |
| game.ball.vel = (Vector2){0, 0}; | |
| game.ball.owner = NULL; | |
| for (int t = 0; t < 2; t++) { | |
| Team team = (Team)t; | |
| Formation form = (team == TEAM_A) ? game.teamAFormation : game.teamBFormation; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* p = &game.players[t][i]; | |
| p->homePos = GetFormationPosition(team, i, form); | |
| p->pos = p->homePos; | |
| p->targetPos = p->homePos; | |
| p->vel = (Vector2){0, 0}; | |
| p->hasBall = false; | |
| p->kickCooldown = 0; | |
| p->reactionTimer = 0; | |
| if (team == kickoffTeam && p->role == ROLE_ATTACKER) { | |
| p->pos.x = (team == TEAM_A) ? FIELD_CENTER_X - 30 : FIELD_CENTER_X + 30; | |
| } | |
| if (team != kickoffTeam) { | |
| if (team == TEAM_A && p->pos.x > FIELD_CENTER_X - PLAYER_RADIUS) | |
| p->pos.x = FIELD_CENTER_X - PLAYER_RADIUS - 10; | |
| if (team == TEAM_B && p->pos.x < FIELD_CENTER_X + PLAYER_RADIUS) | |
| p->pos.x = FIELD_CENTER_X + PLAYER_RADIUS + 10; | |
| } | |
| } | |
| } | |
| } | |
| Vector2 Seek(Player* player, Vector2 target) { | |
| Vector2 desired = Vec2Normalize(Vec2Sub(target, player->pos)); | |
| float maxSpeed = (player->role == ROLE_GOALKEEPER) ? GK_MAX_SPEED : MAX_SPEED; | |
| return Vec2Sub(Vec2Scale(desired, maxSpeed), player->vel); | |
| } | |
| Vector2 Arrive(Player* player, Vector2 target, float slowRadius) { | |
| Vector2 toTarget = Vec2Sub(target, player->pos); | |
| float dist = Vec2Length(toTarget); | |
| if (dist < 1.0f) return (Vector2){0, 0}; | |
| float maxSpeed = (player->role == ROLE_GOALKEEPER) ? GK_MAX_SPEED : MAX_SPEED; | |
| float speed = (dist < slowRadius) ? maxSpeed * (dist / slowRadius) : maxSpeed; | |
| return Vec2Sub(Vec2Scale(Vec2Normalize(toTarget), speed), player->vel); | |
| } | |
| Vector2 SeparationFromTeammates(Player* player) { | |
| Vector2 steering = {0, 0}; | |
| Team team = player->team; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* other = &game.players[team][i]; | |
| if (other == player) continue; | |
| float dist = Vec2Distance(player->pos, other->pos); | |
| if (dist < MIN_TEAMMATE_DISTANCE && dist > 0.1f) { | |
| Vector2 diff = Vec2Normalize(Vec2Sub(player->pos, other->pos)); | |
| float strength = (MIN_TEAMMATE_DISTANCE - dist) / MIN_TEAMMATE_DISTANCE; | |
| steering = Vec2Add(steering, Vec2Scale(diff, strength * 3.0f)); | |
| } | |
| } | |
| return steering; | |
| } | |
| float GetSpaceValue(Vector2 pos, Team myTeam) { | |
| float minOppDist = 9999; | |
| Team oppTeam = (myTeam == TEAM_A) ? TEAM_B : TEAM_A; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| float dist = Vec2Distance(pos, game.players[oppTeam][i].pos); | |
| if (dist < minOppDist) minOppDist = dist; | |
| } | |
| return minOppDist; | |
| } | |
| float GetOffsideLine(Team attackingTeam) { | |
| Team defendingTeam = (attackingTeam == TEAM_A) ? TEAM_B : TEAM_A; | |
| float defenders[PLAYERS_PER_TEAM]; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) | |
| defenders[i] = game.players[defendingTeam][i].pos.x; | |
| for (int i = 0; i < PLAYERS_PER_TEAM - 1; i++) { | |
| for (int j = i + 1; j < PLAYERS_PER_TEAM; j++) { | |
| bool swap = (attackingTeam == TEAM_A) ? (defenders[i] < defenders[j]) : (defenders[i] > defenders[j]); | |
| if (swap) { float t = defenders[i]; defenders[i] = defenders[j]; defenders[j] = t; } | |
| } | |
| } | |
| float offsideLine = defenders[1]; | |
| if (attackingTeam == TEAM_A && game.ball.pos.x > offsideLine) offsideLine = game.ball.pos.x; | |
| else if (attackingTeam == TEAM_B && game.ball.pos.x < offsideLine) offsideLine = game.ball.pos.x; | |
| if (attackingTeam == TEAM_A && offsideLine < FIELD_CENTER_X) offsideLine = FIELD_CENTER_X; | |
| else if (attackingTeam == TEAM_B && offsideLine > FIELD_CENTER_X) offsideLine = FIELD_CENTER_X; | |
| return offsideLine; | |
| } | |
| bool IsOffside(Player* player) { | |
| if (player->role == ROLE_GOALKEEPER) return false; | |
| float offsideLine = GetOffsideLine(player->team); | |
| if (player->team == TEAM_A) return player->pos.x > offsideLine + 5; | |
| else return player->pos.x < offsideLine - 5; | |
| } | |
| Vector2 GetStrategicPosition(Player* player) { | |
| Team team = player->team; | |
| Team oppTeam = (team == TEAM_A) ? TEAM_B : TEAM_A; | |
| Vector2 ballPos = game.ball.pos; | |
| bool teamHasBall = (game.ball.owner != NULL && game.ball.owner->team == team); | |
| bool iHaveBall = (game.ball.owner == player); | |
| float attackDir = (team == TEAM_A) ? 1.0f : -1.0f; | |
| float laneY = GetLaneY(player->laneIndex); | |
| Vector2 basePos = player->homePos; | |
| Vector2 targetPos = basePos; | |
| if (player->role == ROLE_GOALKEEPER) { | |
| float gkLineX = (team == TEAM_A) ? FIELD_LEFT + 30 : FIELD_RIGHT - 30; | |
| float targetY = ClampF(ballPos.y, GOAL_TOP + 20, GOAL_BOTTOM - 20); | |
| targetPos = (Vector2){gkLineX, targetY}; | |
| float distToBall = Vec2Distance(player->pos, ballPos); | |
| if (distToBall < 180) { | |
| targetPos.x += attackDir * (180 - distToBall) / 180.0f * 40.0f; | |
| } | |
| } | |
| else if (teamHasBall && !iHaveBall) { | |
| if (player->role == ROLE_ATTACKER) { | |
| float runX = ballPos.x + attackDir * 180; | |
| runX = ClampF(runX, FIELD_LEFT + 100, FIELD_RIGHT - 100); | |
| float runY = laneY; | |
| player->supportTimer += GetFrameTime(); | |
| if (player->supportTimer > 2.0f) { | |
| if (player->roleIndex % 2 == 0) { | |
| runY = (player->laneIndex < 2) ? FIELD_TOP + 90 : FIELD_BOTTOM - 90; | |
| } else { | |
| float wave = sinf(player->supportTimer * 1.5f) * 80.0f; | |
| runY = FIELD_CENTER_Y + wave; | |
| } | |
| if (player->supportTimer > 5.0f) player->supportTimer = 0; | |
| } | |
| float offsideLine = GetOffsideLine(team); | |
| if (team == TEAM_A && runX > offsideLine - 25) runX = offsideLine - 25; | |
| else if (team == TEAM_B && runX < offsideLine + 25) runX = offsideLine + 25; | |
| targetPos = (Vector2){runX, ClampF(runY, FIELD_TOP + 70, FIELD_BOTTOM - 70)}; | |
| } | |
| else if (player->role == ROLE_MIDFIELDER) { | |
| float supportX; | |
| if (player->roleIndex % 3 == 0) supportX = ballPos.x + attackDir * 120; | |
| else if (player->roleIndex % 3 == 1) supportX = ballPos.x - attackDir * 80; | |
| else supportX = ballPos.x; | |
| supportX = ClampF(supportX, FIELD_LEFT + 150, FIELD_RIGHT - 150); | |
| float supportY = laneY * 0.7f + ballPos.y * 0.3f; | |
| if (player->laneIndex == 0 || player->laneIndex == 4) supportY = laneY; | |
| targetPos = (Vector2){supportX, ClampF(supportY, FIELD_TOP + 70, FIELD_BOTTOM - 70)}; | |
| } | |
| else if (player->role == ROLE_DEFENDER) { | |
| float defX = basePos.x + attackDir * 30; | |
| if (team == TEAM_A) defX = ClampF(defX, FIELD_LEFT + 80, FIELD_LEFT + FIELD_WIDTH * 0.35f); | |
| else defX = ClampF(defX, FIELD_RIGHT - FIELD_WIDTH * 0.35f, FIELD_RIGHT - 80); | |
| targetPos = (Vector2){defX, laneY}; | |
| } | |
| } | |
| else if (!teamHasBall) { | |
| if (player->role == ROLE_DEFENDER) { | |
| float defLineX; | |
| if (team == TEAM_A) { | |
| defLineX = fminf(ballPos.x - 150, FIELD_LEFT + FIELD_WIDTH * 0.30f); | |
| defLineX = ClampF(defLineX, FIELD_LEFT + 80, FIELD_LEFT + FIELD_WIDTH * 0.32f); | |
| } else { | |
| defLineX = fmaxf(ballPos.x + 150, FIELD_RIGHT - FIELD_WIDTH * 0.30f); | |
| defLineX = ClampF(defLineX, FIELD_RIGHT - FIELD_WIDTH * 0.32f, FIELD_RIGHT - 80); | |
| } | |
| float markY = laneY; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* opp = &game.players[oppTeam][i]; | |
| if (opp->role == ROLE_ATTACKER) { | |
| if (fabsf(opp->pos.y - laneY) < LANE_WIDTH * 0.8f) { | |
| markY = opp->pos.y * 0.6f + laneY * 0.4f; | |
| break; | |
| } | |
| } | |
| } | |
| targetPos = (Vector2){defLineX, ClampF(markY, FIELD_TOP + 70, FIELD_BOTTOM - 70)}; | |
| } | |
| else if (player->role == ROLE_MIDFIELDER) { | |
| float midLineX = (team == TEAM_A) ? ballPos.x - 130 : ballPos.x + 130; | |
| if (team == TEAM_A) midLineX = ClampF(midLineX, FIELD_LEFT + 200, FIELD_CENTER_X + 80); | |
| else midLineX = ClampF(midLineX, FIELD_CENTER_X - 80, FIELD_RIGHT - 200); | |
| float midY = laneY * 0.6f + ballPos.y * 0.4f; | |
| targetPos = (Vector2){midLineX, ClampF(midY, FIELD_TOP + 70, FIELD_BOTTOM - 70)}; | |
| } | |
| else if (player->role == ROLE_ATTACKER) { | |
| float atkX = (team == TEAM_A) ? | |
| ClampF(ballPos.x + 80, FIELD_CENTER_X - 50, FIELD_RIGHT - 150) : | |
| ClampF(ballPos.x - 80, FIELD_LEFT + 150, FIELD_CENTER_X + 50); | |
| targetPos = (Vector2){atkX, laneY}; | |
| } | |
| } | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* mate = &game.players[team][i]; | |
| if (mate == player || mate->role == ROLE_GOALKEEPER) continue; | |
| float dist = Vec2Distance(targetPos, mate->targetPos); | |
| if (dist < MIN_TEAMMATE_DISTANCE && dist > 0.1f) { | |
| Vector2 away = Vec2Normalize(Vec2Sub(targetPos, mate->targetPos)); | |
| away.x *= 1.5f; | |
| away = Vec2Normalize(away); | |
| targetPos = Vec2Add(targetPos, Vec2Scale(away, (MIN_TEAMMATE_DISTANCE - dist) * 0.7f)); | |
| } | |
| float horizDist = fabsf(targetPos.x - mate->targetPos.x); | |
| if (horizDist < 50 && player->role == mate->role) { | |
| float pushDir = (targetPos.x > mate->targetPos.x) ? 1.0f : -1.0f; | |
| targetPos.x += pushDir * (50 - horizDist) * 0.5f; | |
| } | |
| } | |
| targetPos.x = ClampF(targetPos.x, FIELD_LEFT + 40, FIELD_RIGHT - 40); | |
| targetPos.y = ClampF(targetPos.y, FIELD_TOP + 40, FIELD_BOTTOM - 40); | |
| return targetPos; | |
| } | |
| Player* GetBestPassTarget(Player* passer) { | |
| Player* best = NULL; | |
| float bestScore = -1000; | |
| Team team = passer->team; | |
| Team oppTeam = (team == TEAM_A) ? TEAM_B : TEAM_A; | |
| float attackDir = (team == TEAM_A) ? 1.0f : -1.0f; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* target = &game.players[team][i]; | |
| if (target == passer || target->role == ROLE_GOALKEEPER) continue; | |
| float dist = Vec2Distance(passer->pos, target->pos); | |
| if (dist < 70 || dist > 400) continue; | |
| bool clear = true; | |
| Vector2 passDir = Vec2Normalize(Vec2Sub(target->pos, passer->pos)); | |
| for (int j = 0; j < PLAYERS_PER_TEAM && clear; j++) { | |
| Player* opp = &game.players[oppTeam][j]; | |
| Vector2 toOpp = Vec2Sub(opp->pos, passer->pos); | |
| float proj = Vec2Dot(toOpp, passDir); | |
| if (proj > 30 && proj < dist - 30) { | |
| Vector2 closest = Vec2Add(passer->pos, Vec2Scale(passDir, proj)); | |
| if (Vec2Distance(closest, opp->pos) < 40) clear = false; | |
| } | |
| } | |
| if (!clear) continue; | |
| if (game.offsideEnabled && IsOffside(target)) continue; | |
| float score = 0; | |
| float forwardProgress = (target->pos.x - passer->pos.x) * attackDir; | |
| score += forwardProgress * 0.5f; | |
| score += GetSpaceValue(target->pos, team) * 0.5f; | |
| if (target->role == ROLE_ATTACKER) score += 60; | |
| else if (target->role == ROLE_MIDFIELDER) score += 30; | |
| if (forwardProgress > 50) score += 40; | |
| if (fabsf(target->pos.y - passer->pos.y) > 200) score += 50; | |
| if (dist > 280) score -= 30; | |
| if (score > bestScore) { bestScore = score; best = target; } | |
| } | |
| return best; | |
| } | |
| bool CanShoot(Player* player) { | |
| Team oppTeam = (player->team == TEAM_A) ? TEAM_B : TEAM_A; | |
| float goalX = (player->team == TEAM_A) ? FIELD_RIGHT : FIELD_LEFT; | |
| float distToGoal = Vec2Distance(player->pos, (Vector2){goalX, FIELD_CENTER_Y}); | |
| // Increased shooting range | |
| if (distToGoal > 350) return false; | |
| for (int angle = -1; angle <= 1; angle++) { | |
| Vector2 target = {goalX, FIELD_CENTER_Y + angle * 50}; | |
| Vector2 shotDir = Vec2Normalize(Vec2Sub(target, player->pos)); | |
| float shotDist = Vec2Distance(player->pos, target); | |
| bool clear = true; | |
| for (int i = 0; i < PLAYERS_PER_TEAM && clear; i++) { | |
| Player* opp = &game.players[oppTeam][i]; | |
| Vector2 toOpp = Vec2Sub(opp->pos, player->pos); | |
| float proj = Vec2Dot(toOpp, shotDir); | |
| if (proj > 0 && proj < shotDist) { | |
| Vector2 closest = Vec2Add(player->pos, Vec2Scale(shotDir, proj)); | |
| if (Vec2Distance(closest, opp->pos) < 30) clear = false; | |
| } | |
| } | |
| if (clear) return true; | |
| } | |
| return false; | |
| } | |
| // Ensure goalkeeper facing is always away from own goal | |
| void UpdateGoalkeeperFacing(Player* player) { | |
| if (player->role != ROLE_GOALKEEPER) return; | |
| float attackDir = (player->team == TEAM_A) ? 1.0f : -1.0f; | |
| // Calculate facing direction - always pointing away from own goal | |
| // but can vary within 180 degree arc in front of goal | |
| Vector2 toBall = Vec2Sub(game.ball.pos, player->pos); | |
| float ballAngle = atan2f(toBall.y, toBall.x); | |
| // Clamp to forward-facing hemisphere | |
| if (player->team == TEAM_A) { | |
| // Team A faces right, angle should be between -PI/2 and PI/2 | |
| if (ballAngle > 3.14159f / 2.0f) ballAngle = 3.14159f / 2.0f; | |
| if (ballAngle < -3.14159f / 2.0f) ballAngle = -3.14159f / 2.0f; | |
| } else { | |
| // Team B faces left, angle should be between PI/2 and 3PI/2 | |
| if (ballAngle > 0 && ballAngle < 3.14159f / 2.0f) ballAngle = 3.14159f / 2.0f; | |
| if (ballAngle < 0 && ballAngle > -3.14159f / 2.0f) ballAngle = -3.14159f / 2.0f; | |
| } | |
| player->facing = (Vector2){cosf(ballAngle), sinf(ballAngle)}; | |
| player->facing = Vec2Normalize(player->facing); | |
| // Ensure primary direction is correct | |
| if ((player->team == TEAM_A && player->facing.x < 0) || | |
| (player->team == TEAM_B && player->facing.x > 0)) { | |
| player->facing.x = attackDir; | |
| } | |
| } | |
| void UpdatePlayerAI(Player* player) { | |
| Vector2 steering = {0, 0}; | |
| float maxSpeed = (player->role == ROLE_GOALKEEPER) ? GK_MAX_SPEED : MAX_SPEED; | |
| Team team = player->team; | |
| Team oppTeam = (team == TEAM_A) ? TEAM_B : TEAM_A; | |
| float attackDir = (team == TEAM_A) ? 1.0f : -1.0f; | |
| float oppGoalX = (team == TEAM_A) ? FIELD_RIGHT : FIELD_LEFT; | |
| Vector2 ballPos = game.ball.pos; | |
| float distToBall = Vec2Distance(player->pos, ballPos); | |
| bool iHaveBall = (game.ball.owner == player); | |
| // Update timers | |
| if (player->kickCooldown > 0) player->kickCooldown -= GetFrameTime(); | |
| if (player->reactionTimer > 0) player->reactionTimer -= GetFrameTime(); | |
| player->targetPos = GetStrategicPosition(player); | |
| // Update goalkeeper facing | |
| if (player->role == ROLE_GOALKEEPER) { | |
| UpdateGoalkeeperFacing(player); | |
| } | |
| if (player->role == ROLE_GOALKEEPER) { | |
| steering = Vec2Add(steering, Vec2Scale(Arrive(player, player->targetPos, 15), ARRIVE_WEIGHT)); | |
| // Goalkeeper can react faster | |
| if (distToBall < 60 && game.ball.owner != player && player->kickCooldown <= 0) { | |
| steering = Vec2Add(steering, Vec2Scale(Seek(player, ballPos), SEEK_WEIGHT * 2.5f)); | |
| } | |
| } | |
| else if (iHaveBall) { | |
| float distToGoal = Vec2Distance(player->pos, (Vector2){oppGoalX, FIELD_CENTER_Y}); | |
| int pressure = 0; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| if (Vec2Distance(player->pos, game.players[oppTeam][i].pos) < 65) pressure++; | |
| } | |
| // MORE AGGRESSIVE SHOOTING - shoot more often and from further | |
| bool shouldShoot = CanShoot(player) && ( | |
| distToGoal < 280 || // Always try from close range | |
| (distToGoal < 350 && player->role == ROLE_ATTACKER) || // Attackers shoot from further | |
| (distToGoal < 320 && pressure >= 2) // Under pressure, take a shot | |
| ); | |
| if (shouldShoot) { | |
| float targetY = FIELD_CENTER_Y + GetRandomValue(-55, 55); | |
| Vector2 shotDir = Vec2Normalize(Vec2Sub((Vector2){oppGoalX, targetY}, player->pos)); | |
| game.ball.vel = Vec2Scale(shotDir, SHOT_POWER + (player->role == ROLE_ATTACKER ? 1.0f : 0)); | |
| game.ball.owner = NULL; | |
| player->hasBall = false; | |
| player->kickCooldown = 0.5f; | |
| TriggerReactionTime(player); | |
| } | |
| else if (pressure >= 2 || (pressure >= 1 && player->role == ROLE_DEFENDER)) { | |
| Player* passTarget = GetBestPassTarget(player); | |
| if (passTarget != NULL) { | |
| Vector2 leadPos = Vec2Add(passTarget->pos, Vec2Scale(passTarget->vel, 10)); | |
| Vector2 passDir = Vec2Normalize(Vec2Sub(leadPos, player->pos)); | |
| float power = fminf(PASS_POWER * (1.0f + Vec2Distance(player->pos, passTarget->pos) / 350.0f), PASS_POWER * 1.5f); | |
| game.ball.vel = Vec2Scale(passDir, power); | |
| game.ball.owner = NULL; | |
| player->hasBall = false; | |
| player->kickCooldown = 0.3f; | |
| TriggerReactionTime(player); | |
| } else { | |
| steering = Vec2Add(steering, Vec2Scale(Seek(player, Vec2Add(player->pos, (Vector2){attackDir * 80, 0})), SEEK_WEIGHT)); | |
| } | |
| } | |
| else { | |
| Vector2 dribbleTarget = {player->pos.x + attackDir * 120, player->pos.y}; | |
| float bestY = player->pos.y, bestSpace = 0; | |
| for (int yOff = -100; yOff <= 100; yOff += 50) { | |
| Vector2 testPos = {dribbleTarget.x, ClampF(player->pos.y + yOff, FIELD_TOP + 80, FIELD_BOTTOM - 80)}; | |
| float space = GetSpaceValue(testPos, team); | |
| if (space > bestSpace) { bestSpace = space; bestY = testPos.y; } | |
| } | |
| dribbleTarget.y = bestY; | |
| dribbleTarget.x = ClampF(dribbleTarget.x, FIELD_LEFT + 70, FIELD_RIGHT - 70); | |
| steering = Vec2Add(steering, Vec2Scale(Seek(player, dribbleTarget), SEEK_WEIGHT * 0.9f)); | |
| // Random pass/shot decision - attackers more likely to shoot | |
| int shootChance = (player->role == ROLE_ATTACKER) ? 8 : 3; | |
| if (GetRandomValue(0, 100) < shootChance && CanShoot(player)) { | |
| float targetY = FIELD_CENTER_Y + GetRandomValue(-55, 55); | |
| Vector2 shotDir = Vec2Normalize(Vec2Sub((Vector2){oppGoalX, targetY}, player->pos)); | |
| game.ball.vel = Vec2Scale(shotDir, SHOT_POWER); | |
| game.ball.owner = NULL; | |
| player->hasBall = false; | |
| player->kickCooldown = 0.5f; | |
| TriggerReactionTime(player); | |
| } | |
| else if (GetRandomValue(0, 100) < 3) { | |
| Player* passTarget = GetBestPassTarget(player); | |
| if (passTarget != NULL && GetRandomValue(0, 100) < 40) { | |
| Vector2 passDir = Vec2Normalize(Vec2Sub(passTarget->pos, player->pos)); | |
| game.ball.vel = Vec2Scale(passDir, PASS_POWER); | |
| game.ball.owner = NULL; | |
| player->hasBall = false; | |
| player->kickCooldown = 0.3f; | |
| TriggerReactionTime(player); | |
| } | |
| } | |
| } | |
| } | |
| else { | |
| // REACTION TIME CHECK - player can't chase ball if still reacting | |
| bool canReact = (player->reactionTimer <= 0); | |
| int closerCount = 0; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* mate = &game.players[team][i]; | |
| if (mate == player || mate->role == ROLE_GOALKEEPER) continue; | |
| if (Vec2Distance(mate->pos, ballPos) < distToBall - 25) closerCount++; | |
| } | |
| bool shouldChase = canReact && (closerCount < 2) && (player->role != ROLE_GOALKEEPER); | |
| if (player->role == ROLE_DEFENDER) { | |
| bool ballInDefThird = (team == TEAM_A) ? | |
| (ballPos.x < FIELD_LEFT + FIELD_WIDTH * 0.4f) : | |
| (ballPos.x > FIELD_RIGHT - FIELD_WIDTH * 0.4f); | |
| shouldChase = shouldChase && ballInDefThird; | |
| } | |
| if (shouldChase) { | |
| Vector2 interceptPos = (game.ball.owner == NULL) ? | |
| Vec2Add(ballPos, Vec2Scale(game.ball.vel, 6)) : ballPos; | |
| steering = Vec2Add(steering, Vec2Scale(Seek(player, interceptPos), SEEK_WEIGHT * 1.4f)); | |
| } else { | |
| steering = Vec2Add(steering, Vec2Scale(Arrive(player, player->targetPos, 40), ARRIVE_WEIGHT)); | |
| } | |
| } | |
| steering = Vec2Add(steering, Vec2Scale(SeparationFromTeammates(player), SEPARATION_WEIGHT)); | |
| player->vel = Vec2Add(player->vel, Vec2Scale(steering, 0.12f)); | |
| player->vel = Vec2Truncate(player->vel, maxSpeed); | |
| // Update facing for non-goalkeepers | |
| if (player->role != ROLE_GOALKEEPER && Vec2Length(player->vel) > 0.5f) { | |
| Vector2 newFacing = Vec2Normalize(player->vel); | |
| player->facing.x = player->facing.x * 0.85f + newFacing.x * 0.15f; | |
| player->facing.y = player->facing.y * 0.85f + newFacing.y * 0.15f; | |
| player->facing = Vec2Normalize(player->facing); | |
| } | |
| player->pos = Vec2Add(player->pos, player->vel); | |
| // Allow players to go outside the field (for throw-ins, etc.) but not too far | |
| player->pos.x = ClampF(player->pos.x, FIELD_LEFT - 40, FIELD_RIGHT + 40); | |
| player->pos.y = ClampF(player->pos.y, FIELD_TOP - 40, FIELD_BOTTOM + 40); | |
| if (player->role == ROLE_GOALKEEPER) { | |
| float gkMinX = (team == TEAM_A) ? FIELD_LEFT - 10 : FIELD_RIGHT - PENALTY_AREA_WIDTH; | |
| float gkMaxX = (team == TEAM_A) ? FIELD_LEFT + PENALTY_AREA_WIDTH : FIELD_RIGHT + 10; | |
| player->pos.x = ClampF(player->pos.x, gkMinX, gkMaxX); | |
| } | |
| // Ball control - REACTION TIME CHECK | |
| bool canControlBall = (player->reactionTimer <= 0) || (player->role == ROLE_GOALKEEPER); | |
| if (game.ball.owner == NULL && distToBall < CONTROL_DISTANCE && player->kickCooldown <= 0 && canControlBall) { | |
| if (game.offsideEnabled && game.ball.lastTouchedBy != team && IsOffside(player)) { | |
| game.state = STATE_GOAL_KICK; | |
| game.setpieceTeam = (team == TEAM_A) ? TEAM_B : TEAM_A; | |
| game.setpiecePos = (game.setpieceTeam == TEAM_A) ? | |
| (Vector2){FIELD_LEFT + GOAL_AREA_WIDTH, FIELD_CENTER_Y} : | |
| (Vector2){FIELD_RIGHT - GOAL_AREA_WIDTH, FIELD_CENTER_Y}; | |
| game.stateTimer = 1.0f; | |
| return; | |
| } | |
| game.ball.owner = player; | |
| player->hasBall = true; | |
| game.ball.lastTouchedBy = team; | |
| if (player->role != ROLE_GOALKEEPER) { | |
| player->facing = (team == TEAM_A) ? (Vector2){1, 0} : (Vector2){-1, 0}; | |
| } | |
| TriggerReactionTime(player); | |
| } | |
| // Goalkeeper grab | |
| if (player->role == ROLE_GOALKEEPER && game.ball.owner != NULL && game.ball.owner->team != team) { | |
| if (Vec2Distance(player->pos, game.ball.owner->pos) < PLAYER_RADIUS * 2 + BALL_RADIUS) { | |
| game.ball.owner->hasBall = false; | |
| game.ball.owner = player; | |
| player->hasBall = true; | |
| game.ball.lastTouchedBy = team; | |
| UpdateGoalkeeperFacing(player); | |
| Player* clearTarget = NULL; | |
| float bestScore = -9999; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* mate = &game.players[team][i]; | |
| if (mate == player) continue; | |
| float dist = Vec2Distance(player->pos, mate->pos); | |
| if (dist < 100 || dist > 400) continue; | |
| // Only pass to players in front of goal (away from own goal) | |
| float mateDir = (mate->pos.x - player->pos.x) * attackDir; | |
| if (mateDir < 0) continue; // Don't pass backwards toward own goal | |
| float score = (mate->role == ROLE_DEFENDER) ? 100 : ((mate->role == ROLE_MIDFIELDER) ? 60 : 20); | |
| score -= dist * 0.08f; | |
| score += GetSpaceValue(mate->pos, team) * 0.3f; | |
| if (score > bestScore) { bestScore = score; clearTarget = mate; } | |
| } | |
| if (clearTarget != NULL) { | |
| game.ball.vel = Vec2Scale(Vec2Normalize(Vec2Sub(clearTarget->pos, player->pos)), PASS_POWER * 1.3f); | |
| } else { | |
| // Boot it upfield - never toward own goal | |
| game.ball.vel = (Vector2){attackDir * KICK_POWER * 1.6f, GetRandomValue(-40, 40) * 0.06f}; | |
| } | |
| game.ball.owner = NULL; | |
| player->hasBall = false; | |
| player->kickCooldown = 0.5f; | |
| TriggerReactionTime(player); | |
| } | |
| } | |
| } | |
| void UpdateBall(void) { | |
| if (game.ball.owner != NULL) { | |
| Player* owner = game.ball.owner; | |
| // For goalkeeper, ensure ball is always in front (away from own goal) | |
| if (owner->role == ROLE_GOALKEEPER) { | |
| UpdateGoalkeeperFacing(owner); | |
| } | |
| game.ball.pos = Vec2Add(owner->pos, Vec2Scale(owner->facing, PLAYER_RADIUS + BALL_RADIUS + 2)); | |
| game.ball.vel = (Vector2){0, 0}; | |
| } else { | |
| game.ball.vel = Vec2Scale(game.ball.vel, BALL_FRICTION); | |
| game.ball.pos = Vec2Add(game.ball.pos, game.ball.vel); | |
| } | |
| // Check goals first | |
| if (game.ball.pos.x < FIELD_LEFT && game.ball.pos.y > GOAL_TOP && game.ball.pos.y < GOAL_BOTTOM) { | |
| game.scoreB++; game.lastGoalTeam = TEAM_B; game.state = STATE_GOAL_SCORED; | |
| game.kickoffTeam = TEAM_A; game.stateTimer = 2.0f; | |
| game.ball.vel = (Vector2){0, 0}; game.ball.owner = NULL; return; | |
| } | |
| if (game.ball.pos.x > FIELD_RIGHT && game.ball.pos.y > GOAL_TOP && game.ball.pos.y < GOAL_BOTTOM) { | |
| game.scoreA++; game.lastGoalTeam = TEAM_A; game.state = STATE_GOAL_SCORED; | |
| game.kickoffTeam = TEAM_B; game.stateTimer = 2.0f; | |
| game.ball.vel = (Vector2){0, 0}; game.ball.owner = NULL; return; | |
| } | |
| // Sideline - throw in | |
| if (game.ball.pos.y < FIELD_TOP || game.ball.pos.y > FIELD_BOTTOM) { | |
| game.state = STATE_THROW_IN; | |
| game.setpieceTeam = (game.ball.lastTouchedBy == TEAM_A) ? TEAM_B : TEAM_A; | |
| game.setpiecePos.x = ClampF(game.ball.pos.x, FIELD_LEFT + 5, FIELD_RIGHT - 5); | |
| game.setpiecePos.y = (game.ball.pos.y < FIELD_CENTER_Y) ? FIELD_TOP : FIELD_BOTTOM; | |
| game.stateTimer = 1.0f; | |
| game.ball.owner = NULL; | |
| game.ball.vel = (Vector2){0, 0}; | |
| return; | |
| } | |
| // Goal line | |
| if (game.ball.pos.x < FIELD_LEFT) { | |
| if (game.ball.lastTouchedBy == TEAM_A) { | |
| game.state = STATE_GOAL_KICK; game.setpieceTeam = TEAM_A; | |
| game.setpiecePos = (Vector2){FIELD_LEFT + GOAL_AREA_WIDTH, FIELD_CENTER_Y}; | |
| } else { | |
| game.state = STATE_CORNER; game.setpieceTeam = TEAM_B; | |
| game.setpiecePos = (Vector2){FIELD_LEFT + 5, (game.ball.pos.y < FIELD_CENTER_Y) ? FIELD_TOP + 5 : FIELD_BOTTOM - 5}; | |
| } | |
| game.stateTimer = 1.0f; game.ball.owner = NULL; | |
| game.ball.vel = (Vector2){0, 0}; | |
| return; | |
| } | |
| if (game.ball.pos.x > FIELD_RIGHT) { | |
| if (game.ball.lastTouchedBy == TEAM_B) { | |
| game.state = STATE_GOAL_KICK; game.setpieceTeam = TEAM_B; | |
| game.setpiecePos = (Vector2){FIELD_RIGHT - GOAL_AREA_WIDTH, FIELD_CENTER_Y}; | |
| } else { | |
| game.state = STATE_CORNER; game.setpieceTeam = TEAM_A; | |
| game.setpiecePos = (Vector2){FIELD_RIGHT - 5, (game.ball.pos.y < FIELD_CENTER_Y) ? FIELD_TOP + 5 : FIELD_BOTTOM - 5}; | |
| } | |
| game.stateTimer = 1.0f; game.ball.owner = NULL; | |
| game.ball.vel = (Vector2){0, 0}; | |
| return; | |
| } | |
| } | |
| void HandleSetPiece(void) { | |
| game.stateTimer -= GetFrameTime(); | |
| // Find the taker first | |
| float minDist = 9999; | |
| Player* taker = NULL; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* p = &game.players[game.setpieceTeam][i]; | |
| if (p->role == ROLE_GOALKEEPER && game.state != STATE_GOAL_KICK) continue; | |
| float dist = Vec2Distance(p->pos, game.setpiecePos); | |
| if (dist < minDist) { minDist = dist; taker = p; } | |
| } | |
| // Calculate taker position based on set piece type | |
| Vector2 takerOffset = {0, 0}; | |
| Vector2 takerFacing = {1, 0}; | |
| if (game.state == STATE_THROW_IN) { | |
| // Throw-in: taker stands OUTSIDE the sideline, facing INTO the field | |
| float outsideOffset = PLAYER_RADIUS + BALL_RADIUS + 8; | |
| if (game.setpiecePos.y <= FIELD_TOP) { | |
| // Top sideline - taker above the line, facing down (into field) | |
| takerOffset = (Vector2){0, -outsideOffset}; | |
| takerFacing = (Vector2){0, 1}; | |
| } else { | |
| // Bottom sideline - taker below the line, facing up (into field) | |
| takerOffset = (Vector2){0, outsideOffset}; | |
| takerFacing = (Vector2){0, -1}; | |
| } | |
| } else if (game.state == STATE_CORNER) { | |
| // Corner: taker outside corner, angled toward goal | |
| float attackDir = (game.setpieceTeam == TEAM_A) ? 1.0f : -1.0f; | |
| takerOffset = (Vector2){-attackDir * (PLAYER_RADIUS + BALL_RADIUS + 5), 0}; | |
| takerFacing = (Vector2){attackDir, 0}; | |
| } else { | |
| // Goal kick and others: behind the ball, facing forward | |
| float attackDir = (game.setpieceTeam == TEAM_A) ? 1.0f : -1.0f; | |
| takerOffset = (Vector2){-attackDir * (PLAYER_RADIUS + BALL_RADIUS + 5), 0}; | |
| takerFacing = (Vector2){attackDir, 0}; | |
| } | |
| Vector2 takerPos = Vec2Add(game.setpiecePos, takerOffset); | |
| // Move ONLY OPPONENTS away from the TAKER | |
| if (taker != NULL) { | |
| Team oppTeam = (game.setpieceTeam == TEAM_A) ? TEAM_B : TEAM_A; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* p = &game.players[oppTeam][i]; | |
| float dist = Vec2Distance(p->pos, takerPos); | |
| if (dist < SETPIECE_CLEAR_RADIUS) { | |
| Vector2 awayDir = Vec2Normalize(Vec2Sub(p->pos, takerPos)); | |
| if (Vec2Length(awayDir) < 0.1f) { | |
| // Default push direction - into the field | |
| awayDir = (Vector2){0, (game.setpiecePos.y < FIELD_CENTER_Y) ? 1.0f : -1.0f}; | |
| } | |
| p->pos = Vec2Add(takerPos, Vec2Scale(awayDir, SETPIECE_CLEAR_RADIUS + 5)); | |
| // Keep opponents inside the field | |
| p->pos.x = ClampF(p->pos.x, FIELD_LEFT + 20, FIELD_RIGHT - 20); | |
| p->pos.y = ClampF(p->pos.y, FIELD_TOP + 20, FIELD_BOTTOM - 20); | |
| } | |
| } | |
| } | |
| if (game.stateTimer <= 0) { | |
| if (taker != NULL) { | |
| taker->pos = takerPos; | |
| taker->facing = takerFacing; | |
| taker->kickCooldown = 0.5f; | |
| game.ball.lastTouchedBy = game.setpieceTeam; | |
| if (game.state == STATE_THROW_IN) { | |
| // THROW-IN: Immediately throw ball into the field - don't take possession! | |
| // Find best teammate to throw to | |
| Player* throwTarget = NULL; | |
| float bestScore = -9999; | |
| Team team = game.setpieceTeam; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* mate = &game.players[team][i]; | |
| if (mate == taker || mate->role == ROLE_GOALKEEPER) continue; | |
| float dist = Vec2Distance(taker->pos, mate->pos); | |
| if (dist < 60 || dist > 300) continue; | |
| // Must be inside the field | |
| if (mate->pos.x < FIELD_LEFT + 30 || mate->pos.x > FIELD_RIGHT - 30) continue; | |
| if (mate->pos.y < FIELD_TOP + 30 || mate->pos.y > FIELD_BOTTOM - 30) continue; | |
| float score = 100 - dist * 0.3f; | |
| score += GetSpaceValue(mate->pos, team) * 0.5f; | |
| if (score > bestScore) { | |
| bestScore = score; | |
| throwTarget = mate; | |
| } | |
| } | |
| Vector2 throwDir; | |
| float throwPower = PASS_POWER * 1.1f; | |
| if (throwTarget != NULL) { | |
| throwDir = Vec2Normalize(Vec2Sub(throwTarget->pos, taker->pos)); | |
| } else { | |
| // No target found - just throw into the field | |
| throwDir = takerFacing; | |
| // Add some horizontal component toward opponent goal | |
| float attackDir = (game.setpieceTeam == TEAM_A) ? 1.0f : -1.0f; | |
| throwDir.x += attackDir * 0.5f; | |
| throwDir = Vec2Normalize(throwDir); | |
| } | |
| // Ball starts at the sideline and is thrown in | |
| game.ball.pos = game.setpiecePos; | |
| game.ball.vel = Vec2Scale(throwDir, throwPower); | |
| game.ball.owner = NULL; // Ball is in the air, no one owns it | |
| taker->hasBall = false; | |
| } | |
| else { | |
| // CORNER, GOAL KICK, etc: Take possession normally (ball is inside field) | |
| game.ball.pos = game.setpiecePos; | |
| game.ball.vel = (Vector2){0, 0}; | |
| game.ball.owner = taker; | |
| taker->hasBall = true; | |
| taker->reactionTimer = 0; | |
| } | |
| } | |
| game.state = STATE_PLAYING; | |
| TriggerReactionTime(taker); | |
| } | |
| } | |
| void UpdateGame(void) { | |
| switch (game.state) { | |
| case STATE_KICKOFF: | |
| game.stateTimer -= GetFrameTime(); | |
| if (game.stateTimer <= 0) { | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* p = &game.players[game.kickoffTeam][i]; | |
| if (p->role == ROLE_ATTACKER && Vec2Distance(p->pos, game.ball.pos) < 60) { | |
| game.ball.owner = p; p->hasBall = true; | |
| game.ball.lastTouchedBy = game.kickoffTeam; | |
| game.state = STATE_PLAYING; | |
| TriggerReactionTime(p); | |
| break; | |
| } | |
| } | |
| if (game.state == STATE_KICKOFF) game.stateTimer = 0.5f; | |
| } | |
| for (int t = 0; t < 2; t++) | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) | |
| UpdatePlayerAI(&game.players[t][i]); | |
| UpdateBall(); | |
| break; | |
| case STATE_PLAYING: | |
| for (int t = 0; t < 2; t++) | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) | |
| UpdatePlayerAI(&game.players[t][i]); | |
| UpdateBall(); | |
| break; | |
| case STATE_GOAL_SCORED: | |
| game.stateTimer -= GetFrameTime(); | |
| if (game.stateTimer <= 0) { | |
| ResetPositions(game.kickoffTeam); | |
| game.state = STATE_KICKOFF; | |
| game.stateTimer = 1.0f; | |
| } | |
| break; | |
| case STATE_THROW_IN: | |
| case STATE_CORNER: | |
| case STATE_GOAL_KICK: | |
| HandleSetPiece(); | |
| break; | |
| } | |
| } | |
| void DrawField(void) { | |
| DrawRectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, DARKGREEN); | |
| DrawRectangle(FIELD_LEFT, FIELD_TOP, FIELD_WIDTH, FIELD_HEIGHT, (Color){34, 139, 34, 255}); | |
| for (int i = 0; i < 12; i++) | |
| if (i % 2 == 0) | |
| DrawRectangle(FIELD_LEFT + i * (FIELD_WIDTH / 12), FIELD_TOP, FIELD_WIDTH / 12, FIELD_HEIGHT, (Color){30, 130, 30, 255}); | |
| DrawRectangleLinesEx((Rectangle){FIELD_LEFT, FIELD_TOP, FIELD_WIDTH, FIELD_HEIGHT}, 3, WHITE); | |
| DrawLine(FIELD_CENTER_X, FIELD_TOP, FIELD_CENTER_X, FIELD_BOTTOM, WHITE); | |
| DrawCircleLines(FIELD_CENTER_X, FIELD_CENTER_Y, 60, WHITE); | |
| DrawCircle(FIELD_CENTER_X, FIELD_CENTER_Y, 5, WHITE); | |
| DrawRectangleLinesEx((Rectangle){FIELD_LEFT, FIELD_CENTER_Y - PENALTY_AREA_HEIGHT/2, PENALTY_AREA_WIDTH, PENALTY_AREA_HEIGHT}, 2, WHITE); | |
| DrawRectangleLinesEx((Rectangle){FIELD_RIGHT - PENALTY_AREA_WIDTH, FIELD_CENTER_Y - PENALTY_AREA_HEIGHT/2, PENALTY_AREA_WIDTH, PENALTY_AREA_HEIGHT}, 2, WHITE); | |
| DrawRectangleLinesEx((Rectangle){FIELD_LEFT, FIELD_CENTER_Y - GOAL_AREA_HEIGHT/2, GOAL_AREA_WIDTH, GOAL_AREA_HEIGHT}, 2, WHITE); | |
| DrawRectangleLinesEx((Rectangle){FIELD_RIGHT - GOAL_AREA_WIDTH, FIELD_CENTER_Y - GOAL_AREA_HEIGHT/2, GOAL_AREA_WIDTH, GOAL_AREA_HEIGHT}, 2, WHITE); | |
| DrawCircle(FIELD_LEFT + 90, FIELD_CENTER_Y, 4, WHITE); | |
| DrawCircle(FIELD_RIGHT - 90, FIELD_CENTER_Y, 4, WHITE); | |
| DrawCircleSector((Vector2){FIELD_LEFT, FIELD_TOP}, 15, 0, 90, 20, WHITE); | |
| DrawCircleSector((Vector2){FIELD_RIGHT, FIELD_TOP}, 15, 90, 180, 20, WHITE); | |
| DrawCircleSector((Vector2){FIELD_LEFT, FIELD_BOTTOM}, 15, 270, 360, 20, WHITE); | |
| DrawCircleSector((Vector2){FIELD_RIGHT, FIELD_BOTTOM}, 15, 180, 270, 20, WHITE); | |
| DrawRectangle(FIELD_LEFT - GOAL_WIDTH, GOAL_TOP, GOAL_WIDTH, GOAL_HEIGHT, GRAY); | |
| DrawRectangleLinesEx((Rectangle){FIELD_LEFT - GOAL_WIDTH, GOAL_TOP, GOAL_WIDTH, GOAL_HEIGHT}, 2, DARKGRAY); | |
| DrawRectangle(FIELD_RIGHT, GOAL_TOP, GOAL_WIDTH, GOAL_HEIGHT, GRAY); | |
| DrawRectangleLinesEx((Rectangle){FIELD_RIGHT, GOAL_TOP, GOAL_WIDTH, GOAL_HEIGHT}, 2, DARKGRAY); | |
| } | |
| void DrawPlayers(void) { | |
| for (int t = 0; t < 2; t++) { | |
| Color teamColor = (t == TEAM_A) ? BLUE : RED; | |
| for (int i = 0; i < PLAYERS_PER_TEAM; i++) { | |
| Player* p = &game.players[t][i]; | |
| DrawCircle(p->pos.x, p->pos.y, PLAYER_RADIUS, teamColor); | |
| DrawCircleLines(p->pos.x, p->pos.y, PLAYER_RADIUS, BLACK); | |
| const char* numStr = TextFormat("%d", p->number); | |
| int fontSize = 14; | |
| DrawText(numStr, p->pos.x - MeasureText(numStr, fontSize)/2, p->pos.y - fontSize/2, fontSize, WHITE); | |
| if (p->hasBall) DrawCircleLines(p->pos.x, p->pos.y, PLAYER_RADIUS + 4, YELLOW); | |
| } | |
| } | |
| } | |
| void DrawBall(void) { | |
| DrawCircle(game.ball.pos.x, game.ball.pos.y, BALL_RADIUS, WHITE); | |
| DrawCircleLines(game.ball.pos.x, game.ball.pos.y, BALL_RADIUS, BLACK); | |
| } | |
| void DrawHUD(void) { | |
| DrawRectangle(SCREEN_WIDTH/2 - 100, 5, 200, 40, (Color){0, 0, 0, 180}); | |
| const char* scoreText = TextFormat("%d - %d", game.scoreA, game.scoreB); | |
| DrawText(scoreText, SCREEN_WIDTH/2 - MeasureText(scoreText, 30)/2, 10, 30, WHITE); | |
| DrawCircle(SCREEN_WIDTH/2 - 70, 25, 10, BLUE); | |
| DrawCircle(SCREEN_WIDTH/2 + 70, 25, 10, RED); | |
| DrawText(TextFormat("A: %s", game.teamAFormation.name), 10, 10, 16, BLUE); | |
| const char* formB = TextFormat("B: %s", game.teamBFormation.name); | |
| DrawText(formB, SCREEN_WIDTH - MeasureText(formB, 16) - 10, 10, 16, RED); | |
| const char* stateText = ""; | |
| switch (game.state) { | |
| case STATE_KICKOFF: stateText = "KICK OFF"; break; | |
| case STATE_GOAL_SCORED: stateText = (game.lastGoalTeam == TEAM_A) ? "GOAL! BLUE SCORES!" : "GOAL! RED SCORES!"; break; | |
| case STATE_THROW_IN: stateText = "THROW IN"; break; | |
| case STATE_CORNER: stateText = "CORNER KICK"; break; | |
| case STATE_GOAL_KICK: stateText = "GOAL KICK"; break; | |
| default: break; | |
| } | |
| if (strlen(stateText) > 0) { | |
| int stateWidth = MeasureText(stateText, 24); | |
| DrawRectangle(SCREEN_WIDTH/2 - stateWidth/2 - 10, SCREEN_HEIGHT - 45, stateWidth + 20, 35, (Color){0, 0, 0, 180}); | |
| DrawText(stateText, SCREEN_WIDTH/2 - stateWidth/2, SCREEN_HEIGHT - 40, 24, (game.state == STATE_GOAL_SCORED) ? YELLOW : WHITE); | |
| } | |
| DrawText("R: Restart | ESC: Quit", 10, SCREEN_HEIGHT - 25, 16, (Color){255, 255, 255, 150}); | |
| } | |
| int main(void) { | |
| InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Soccer - CPU vs CPU"); | |
| SetTargetFPS(60); | |
| InitGame(); | |
| game.stateTimer = 1.5f; | |
| while (!WindowShouldClose()) { | |
| if (IsKeyPressed(KEY_R)) { InitGame(); game.stateTimer = 1.5f; } | |
| UpdateGame(); | |
| BeginDrawing(); | |
| ClearBackground(DARKGREEN); | |
| DrawField(); | |
| DrawPlayers(); | |
| DrawBall(); | |
| DrawHUD(); | |
| EndDrawing(); | |
| } | |
| CloseWindow(); | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment