feat: initial commit with M1-M4 implementation
This commit is contained in:
258
internal/game/world.go
Normal file
258
internal/game/world.go
Normal file
@@ -0,0 +1,258 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user