259 lines
6.5 KiB
Go
259 lines
6.5 KiB
Go
package game
|
|
|
|
import (
|
|
"math"
|
|
|
|
"github.com/soer/football/internal/entities"
|
|
)
|
|
|
|
const (
|
|
Friction = 0.99
|
|
Restitution = 0.8
|
|
StopThreshold = 0.1
|
|
WorldWidth = 1280.0
|
|
WorldHeight = 720.0
|
|
WorldCenterX = 640.0
|
|
WorldCenterY = 360.0
|
|
ZoneWidth = WorldWidth / 3
|
|
GoalieMaxDistance = 80.0
|
|
GoalieDeadzone = 1.0
|
|
)
|
|
|
|
var (
|
|
GoalLeft = entities.Vector2{X: 50, Y: WorldCenterY}
|
|
GoalRight = entities.Vector2{X: 1230, Y: WorldCenterY}
|
|
)
|
|
|
|
type World struct {
|
|
Puck *entities.Puck
|
|
Players []*entities.Player
|
|
Width float64
|
|
Height float64
|
|
}
|
|
|
|
func NewWorld() *World {
|
|
return &World{
|
|
Puck: entities.NewPuck(WorldCenterX, WorldCenterY),
|
|
Width: WorldWidth,
|
|
Height: WorldHeight,
|
|
Players: []*entities.Player{
|
|
entities.NewPlayer(200, WorldCenterY, entities.TeamRed, entities.RoleDefender),
|
|
entities.NewPlayer(400, WorldCenterY, entities.TeamRed, entities.RoleStriker),
|
|
entities.NewPlayer(GoalLeft.X, GoalLeft.Y, entities.TeamRed, entities.RoleGoalie),
|
|
entities.NewPlayer(1080, WorldCenterY, entities.TeamBlue, entities.RoleDefender),
|
|
entities.NewPlayer(880, WorldCenterY, entities.TeamBlue, entities.RoleStriker),
|
|
entities.NewPlayer(GoalRight.X, GoalRight.Y, entities.TeamBlue, entities.RoleGoalie),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (w *World) CheckGoal() (entities.Team, bool) {
|
|
if w.Puck.Position.X < 0 {
|
|
return entities.TeamBlue, true
|
|
}
|
|
if w.Puck.Position.X > w.Width {
|
|
return entities.TeamRed, true
|
|
}
|
|
return entities.TeamRed, false
|
|
}
|
|
|
|
func (w *World) ResetPositions() {
|
|
w.Puck.Position = entities.Vector2{X: WorldCenterX, Y: WorldCenterY}
|
|
w.Puck.Velocity = entities.Vector2{X: 0, Y: 0}
|
|
for _, p := range w.Players {
|
|
p.Position = p.HomePosition
|
|
p.Velocity = entities.Vector2{X: 0, Y: 0}
|
|
}
|
|
}
|
|
|
|
func (w *World) Update() {
|
|
// 1. Update Puck
|
|
w.Puck.Velocity.X *= Friction
|
|
w.Puck.Velocity.Y *= Friction
|
|
|
|
if (w.Puck.Velocity.X*w.Puck.Velocity.X + w.Puck.Velocity.Y*w.Puck.Velocity.Y) < StopThreshold*StopThreshold {
|
|
w.Puck.Velocity.X = 0
|
|
w.Puck.Velocity.Y = 0
|
|
}
|
|
|
|
w.Puck.Position.X += w.Puck.Velocity.X
|
|
w.Puck.Position.Y += w.Puck.Velocity.Y
|
|
|
|
if w.Puck.Position.X < w.Puck.Radius {
|
|
w.Puck.Position.X = w.Puck.Radius
|
|
w.Puck.Velocity.X *= -Restitution
|
|
} else if w.Puck.Position.X > w.Width-w.Puck.Radius {
|
|
w.Puck.Position.X = w.Width - w.Puck.Radius
|
|
w.Puck.Velocity.X *= -Restitution
|
|
}
|
|
|
|
if w.Puck.Position.Y < w.Puck.Radius {
|
|
w.Puck.Position.Y = w.Puck.Radius
|
|
w.Puck.Velocity.Y *= -Restitution
|
|
} else if w.Puck.Position.Y > w.Height-w.Puck.Radius {
|
|
w.Puck.Position.Y = w.Height - w.Puck.Radius
|
|
w.Puck.Velocity.Y *= -Restitution
|
|
}
|
|
|
|
// 2. Update Players
|
|
for _, p := range w.Players {
|
|
// AI Target
|
|
target := w.calculateTarget(p, w.Puck.Position)
|
|
w.applySteering(p, target)
|
|
|
|
// Anti-clumping (repulsion from teammates)
|
|
for _, other := range w.Players {
|
|
if p == other || p.Team != other.Team {
|
|
continue
|
|
}
|
|
dx := p.Position.X - other.Position.X
|
|
dy := p.Position.Y - other.Position.Y
|
|
distSq := dx*dx + dy*dy
|
|
if distSq < 40*40 && distSq > 0.01 {
|
|
dist := math.Sqrt(distSq)
|
|
p.Velocity.X += (dx / dist) * 0.1
|
|
p.Velocity.Y += (dy / dist) * 0.1
|
|
}
|
|
}
|
|
|
|
// Apply movement
|
|
p.Position.X += p.Velocity.X
|
|
p.Position.Y += p.Velocity.Y
|
|
|
|
// Boundary clamping
|
|
if p.Position.X < 0 {
|
|
p.Position.X = 0
|
|
} else if p.Position.X > w.Width {
|
|
p.Position.X = w.Width
|
|
}
|
|
if p.Position.Y < 0 {
|
|
p.Position.Y = 0
|
|
} else if p.Position.Y > w.Height {
|
|
p.Position.Y = w.Height
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *World) applySteering(p *entities.Player, target entities.Vector2) {
|
|
dx := target.X - p.Position.X
|
|
dy := target.Y - p.Position.Y
|
|
dist := math.Sqrt(dx*dx + dy*dy)
|
|
|
|
// Dead zone
|
|
if dist < 1.0 {
|
|
p.Velocity.X = 0
|
|
p.Velocity.Y = 0
|
|
return
|
|
}
|
|
|
|
// Desired velocity
|
|
desiredX := (dx / dist) * p.MaxSpeed
|
|
desiredY := (dy / dist) * p.MaxSpeed
|
|
|
|
// Steering force
|
|
steeringX := desiredX - p.Velocity.X
|
|
steeringY := desiredY - p.Velocity.Y
|
|
steeringDist := math.Sqrt(steeringX*steeringX + steeringY*steeringY)
|
|
|
|
if steeringDist > p.Acceleration {
|
|
steeringX = (steeringX / steeringDist) * p.Acceleration
|
|
steeringY = (steeringY / steeringDist) * p.Acceleration
|
|
}
|
|
|
|
p.Velocity.X += steeringX
|
|
p.Velocity.Y += steeringY
|
|
|
|
// Cap final velocity
|
|
speed := math.Sqrt(p.Velocity.X*p.Velocity.X + p.Velocity.Y*p.Velocity.Y)
|
|
if speed > p.MaxSpeed {
|
|
p.Velocity.X = (p.Velocity.X / speed) * p.MaxSpeed
|
|
p.Velocity.Y = (p.Velocity.Y / speed) * p.MaxSpeed
|
|
}
|
|
}
|
|
|
|
func (w *World) getPuckZone(team entities.Team, puckPos entities.Vector2) int {
|
|
// 0: Defensive, 1: Middle, 2: Offensive
|
|
if team == entities.TeamRed {
|
|
if puckPos.X < ZoneWidth {
|
|
return 0
|
|
}
|
|
if puckPos.X < 2*ZoneWidth {
|
|
return 1
|
|
}
|
|
return 2
|
|
} else {
|
|
if puckPos.X > WorldWidth-ZoneWidth {
|
|
return 0
|
|
}
|
|
if puckPos.X > WorldWidth-2*ZoneWidth {
|
|
return 1
|
|
}
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func (w *World) calculateTarget(p *entities.Player, puckPos entities.Vector2) entities.Vector2 {
|
|
zone := w.getPuckZone(p.Team, puckPos)
|
|
|
|
goalPos := GoalLeft
|
|
if p.Team == entities.TeamBlue {
|
|
goalPos = GoalRight
|
|
}
|
|
|
|
switch p.Role {
|
|
case entities.RoleDefender:
|
|
switch zone {
|
|
case 0: // Defensive: Intercept puck
|
|
return entities.Vector2{
|
|
X: (puckPos.X + goalPos.X) / 2,
|
|
Y: (puckPos.Y + goalPos.Y) / 2,
|
|
}
|
|
case 1: // Middle: Move towards puck but stay slightly behind it
|
|
return entities.Vector2{
|
|
X: puckPos.X + (goalPos.X-puckPos.X)*0.2,
|
|
Y: puckPos.Y + (goalPos.Y-puckPos.Y)*0.2,
|
|
}
|
|
case 2: // Offensive: Return to Middle Zone
|
|
return p.HomePosition
|
|
}
|
|
case entities.RoleStriker:
|
|
switch zone {
|
|
case 2: // Offensive: Aggressively pursue puck
|
|
return puckPos
|
|
case 1: // Middle: Pursue puck
|
|
return puckPos
|
|
case 0: // Defensive: Return to Middle Zone
|
|
return entities.Vector2{X: WorldCenterX, Y: WorldCenterY}
|
|
}
|
|
case entities.RoleGoalie:
|
|
return w.updateGoalieAI(p, puckPos)
|
|
}
|
|
return p.HomePosition
|
|
}
|
|
|
|
func (w *World) updateGoalieAI(p *entities.Player, puckPos entities.Vector2) entities.Vector2 {
|
|
goalCenter := GoalLeft
|
|
if p.Team == entities.TeamBlue {
|
|
goalCenter = GoalRight
|
|
}
|
|
|
|
// Check if puck is behind the goal
|
|
if (p.Team == entities.TeamRed && puckPos.X < goalCenter.X) || (p.Team == entities.TeamBlue && puckPos.X > goalCenter.X) {
|
|
return goalCenter
|
|
}
|
|
|
|
dx := puckPos.X - goalCenter.X
|
|
dy := puckPos.Y - goalCenter.Y
|
|
dist := math.Sqrt(dx*dx + dy*dy)
|
|
|
|
if dist < GoalieDeadzone {
|
|
return goalCenter
|
|
}
|
|
|
|
moveDist := math.Min(dist, GoalieMaxDistance)
|
|
return entities.Vector2{
|
|
X: goalCenter.X + (dx/dist)*moveDist,
|
|
Y: goalCenter.Y + (dy/dist)*moveDist,
|
|
}
|
|
}
|