GOingTunneling/player.go

676 lines
18 KiB
Go

package main
import (
"github.com/veandco/go-sdl2/sdl"
"image/color"
"math/rand"
"net"
"sync"
)
var playerUpdateMutex sync.Mutex
var lastPlayerID = uint32(0)
var playersMutex sync.RWMutex
type Player struct {
local bool
connection *net.TCPConn
playerColors PlayerColors
playerColorID uint32
keyMap KeyMap
joyMap JoyMap
joyStick *sdl.Joystick
camera *sdl.Rect
energy uint32
ammunition uint32
shields uint32
digCooldown uint32
shootCooldown uint32
repairCooldown uint32
rechargeCooldown uint32
movementCooldown uint32
reloadCooldown uint32
reloadWait uint32
gameObject *GameObject
playerID uint32
knownGameMap *GameMap
knownBullets map[uint32]*Bullet
knownBulletParticles map[uint32]*BulletParticle
knownBases map[uint32]*Base
knownPlayers map[uint32]*Player
previousEnergy uint32
previousAmmunition uint32
previousShields uint32
window *sdl.Window
logicalSurface *sdl.Surface
playSurface *sdl.Surface
HUDSurface *sdl.Surface
playSurfaceRect *sdl.Rect
HUDSurfaceRect *sdl.Rect
playSurfaceTargetRect *sdl.Rect
HUDSurfaceTargetRect *sdl.Rect
totalHandleEventsTime uint64
totalRenderTime uint64
totalFrameTime uint64
totalScalingTime uint64
frameCount uint64
initialized bool
}
func (player *Player) track(camera *sdl.Rect) {
camera.X = player.gameObject.baseRect.X - 37
camera.Y = player.gameObject.baseRect.Y - 38
}
func (player *Player) render(camera *sdl.Rect, surface *sdl.Surface, players map[uint32]*Player) {
if player.shields > 0 &&
player.shields <= serverConfig.MaxShields &&
player.gameObject.baseRect.X >= 0 &&
player.gameObject.baseRect.Y >= 0 {
player.gameObject.render(camera, surface)
if !player.gameObject.inView && !config.Server {
delete(players, player.playerID)
}
}
}
func (player *Player) tick(bullets map[uint32]*Bullet) {
if player.digCooldown > 0 {
player.digCooldown--
}
if player.shootCooldown > 0 {
player.shootCooldown--
}
if player.repairCooldown > 0 {
player.repairCooldown--
}
if player.rechargeCooldown > 0 {
player.rechargeCooldown--
}
if player.movementCooldown > 0 {
player.movementCooldown--
}
if player.reloadCooldown > 0 {
player.reloadCooldown--
} else if player.ammunition < serverConfig.MaxAmmunition && player.shootCooldown == 0 && player.reloadWait == 0 && player.energy > serverConfig.ReloadCost {
player.ammunition++
player.reloadCooldown = serverConfig.ReloadCooldown
player.energy -= serverConfig.ReloadCost
}
if player.shields <= 0 {
player.shields = serverConfig.MaxShields + 1
player.explode(bullets)
}
if player.reloadWait > 0 {
player.reloadWait--
}
}
func (player *Player) digBlock(posX, posY uint32, gameMap *GameMap, isShooting bool) uint8 {
collisionAtDigSite := gameMap.checkCollision(int32(posX), int32(posY))
if collisionAtDigSite == 0 {
return 0
} else if collisionAtDigSite == 1 && (player.digCooldown == 0 || isShooting) {
if player.energy > serverConfig.DiggingCost {
if isShooting && player.energy > serverConfig.ShootDiggingCostBonus {
player.energy -= serverConfig.ShootDiggingCostBonus
}
player.digCooldown = serverConfig.DiggingCooldown
} else {
player.digCooldown = serverConfig.DiggingCooldownNoEnergy
}
if isShooting && player.ammunition < serverConfig.MaxAmmunition && rand.Intn(2) == 0 {
player.ammunition++
}
if config.Client {
player.sendDigToServer(posX, posY, isShooting)
}
gameMap.tiles[posX][posY] = 0
return 1
}
return 2
}
func (player *Player) tryMove(gameMap *GameMap, isShooting bool, players map[uint32]*Player) (moved bool) {
if config.Client {
defer player.sendPositionToServer()
}
player.gameObject.adjustBaseRect()
if player.movementCooldown > 0 {
return false
}
ranOutOfEnergy := (isShooting && player.energy <= serverConfig.DiggingCost+serverConfig.ShootDiggingCostBonus) || player.energy <= serverConfig.DiggingCost
if ranOutOfEnergy {
isShooting = false
}
// Define movement deltas based on orientation
movementDeltas := []struct {
dx, dy int32
}{
{0, -1}, {1, 0}, {0, 1}, {-1, 0}, // Up, Right, Down, Left
{1, -1}, {-1, -1}, {1, 1}, {-1, 1}, // Up-Right, Up-Left, Down-Right, Down-Left
}
dx, dy := movementDeltas[player.gameObject.orientation].dx, movementDeltas[player.gameObject.orientation].dy
// Set collision rectangle
player.gameObject.collisionRect = &sdl.Rect{
X: player.gameObject.baseRect.X + oNeg(dx),
Y: player.gameObject.baseRect.Y + oNeg(dy),
W: player.gameObject.baseRect.W + abs(dx),
H: player.gameObject.baseRect.H + abs(dy),
}
// Check for player collision
playersMutex.RLock()
for _, opponent := range players {
if opponent != player && opponent.gameObject.baseRect.HasIntersection(player.gameObject.collisionRect) {
playersMutex.RUnlock()
return false
}
}
playersMutex.RUnlock()
// Initialize flags for movement and digging status
stopped := false
// Check collision and dig at the target position
for x := player.gameObject.baseRect.X + oNeg(dx); x < player.gameObject.baseRect.X+dx+player.gameObject.baseRect.W; x++ {
for y := player.gameObject.baseRect.Y + oNeg(dy); y < player.gameObject.baseRect.Y+dy+player.gameObject.baseRect.H; y++ {
digResult := player.digBlock(uint32(x), uint32(y), gameMap, isShooting)
if digResult == 1 { // Successfully dug a block
if !isShooting {
stopped = true // Stop movement if not shooting
}
} else if digResult == 2 { // Block could not be dug (e.g., solid block)
stopped = true
}
if stopped || !(digResult == 0 || (digResult == 1 && isShooting)) {
stopped = true
break
}
}
if stopped {
break
}
}
// Move player if there was no obstruction
if !stopped {
var dxT, dyT int32
if dx > 1 {
dxT = 1
} else if dx < -1 {
dxT = -1
} else {
dxT = dx
}
if dy > 1 {
dyT = 1
} else if dy < -1 {
dyT = -1
} else {
dyT = dy
}
player.gameObject.baseRect.X += dxT
player.gameObject.baseRect.Y += dyT
moved = true
}
// Send the updated position to the server
// Apply movement cooldown
if moved {
if ranOutOfEnergy {
player.movementCooldown = serverConfig.MovementCooldownNoEnergy
} else {
player.movementCooldown = serverConfig.MovementCooldown
}
}
return
}
func abs(dx int32) int32 {
if dx < 0 {
return -dx
} else {
return dx
}
}
func oNeg(dx int32) int32 {
if dx < 0 {
return dx
} else {
return 0
}
}
func (player *Player) getRGBAColor(colorIndex uint8, format *sdl.PixelFormat) uint32 {
var selectedColor color.Color
switch colorIndex {
case 0:
selectedColor = player.playerColors.tracks
case 1:
selectedColor = player.playerColors.body
case 2:
selectedColor = player.playerColors.cannon
}
var r, g, b, a uint8
if selectedColor != nil {
rt, gt, bt, at := selectedColor.RGBA()
r, g, b, a = uint8(rt), uint8(gt), uint8(bt), uint8(at)
}
return sdl.MapRGBA(format, r, g, b, a)
}
type Point struct {
X int32
Y int32
}
func (player *Player) shoot(super bool, bullets map[uint32]*Bullet) {
if (super && (player.energy <= serverConfig.SuperShotCost || player.ammunition < serverConfig.MaxAmmunition)) || (!super && (player.energy <= serverConfig.NormalShotCost || player.ammunition < 1)) {
return
}
if player.shootCooldown == 0 {
var shootX, shootY int32
offsets := []Point{
{X: 2, Y: 0}, // 0: Up
{X: 6, Y: 2}, // 1: Right
{X: 2, Y: 6}, // 2: Down
{X: 0, Y: 2}, // 3: Left
{X: 6, Y: 0}, // 4: Up-Right
{X: 0, Y: 0}, // 5: Up-Left
{X: 5, Y: 5}, // 6: Down-Right
{X: 0, Y: 6}, // 7: Down-Left
}
// Access the offset based on the player's orientation
shootX = player.gameObject.baseRect.X + offsets[player.gameObject.orientation].X
shootY = player.gameObject.baseRect.Y + offsets[player.gameObject.orientation].Y
player.shootCooldown = serverConfig.ShootCooldown
// Set cooldown and decrease energy
if super {
player.energy -= serverConfig.SuperShotCost
player.ammunition = 0
} else {
player.energy -= serverConfig.NormalShotCost
player.ammunition--
}
player.reloadWait = serverConfig.ReloadWait
if config.Client {
player.sendShootToServer(super)
} else {
// Set bullet color
bulletColor := player.playerColors.body
bulletMutex.Lock()
// Create and add the bullet
bullets[bulletLastID] = &Bullet{
posX: shootX,
posY: shootY,
direction: player.gameObject.orientation,
color: bulletColor,
super: super,
id: bulletLastID,
ownerID: player.playerID,
}
bulletMutex.Unlock()
bulletLastID++
}
}
}
func (player *Player) explode(bullets map[uint32]*Bullet) {
// Set bullet color
bulletColor := player.playerColors.body
for x := player.gameObject.baseRect.X - int32(serverConfig.BlastRadius); x < int32(serverConfig.BlastRadius)*2+1; x++ {
for y := player.gameObject.baseRect.Y - int32(serverConfig.BlastRadius); y < int32(serverConfig.BlastRadius)*2+1; y++ {
// Create and add the bullet
bulletMutex.Lock()
bullets[bulletLastID] = &Bullet{
posX: x,
posY: y,
direction: uint8(rand.Intn(8)),
color: bulletColor,
super: true,
id: bulletLastID,
ownerID: player.playerID,
}
bulletMutex.Unlock()
bulletLastID++
}
}
}
func (player *Player) updateRemotePlayerMap(wgX *sync.WaitGroup) {
defer wgX.Done()
fromX := player.gameObject.baseRect.X - (config.CameraW / 2) - 1
fromY := player.gameObject.baseRect.Y - (config.CameraH / 2) - 1
toX := player.gameObject.baseRect.X + (config.CameraW / 2) + 2
toY := player.gameObject.baseRect.Y + (config.CameraH / 2) + 1
if uint32(toX) > serverConfig.MapWidth {
toX = int32(serverConfig.MapWidth)
}
if uint32(toY) > serverConfig.MapHeight {
toY = int32(serverConfig.MapHeight)
}
if fromX < 0 {
fromX = 0
}
if fromY < 0 {
fromY = 0
}
// Create a WaitGroup to wait for all goroutines to finish
var wg sync.WaitGroup
var lck sync.Mutex
// Process columns instead of individual pixels
for x := fromX; x < toX; x++ {
// Increment the WaitGroup counter
wg.Add(1)
// Launch a goroutine for each column
go func(x int32) {
defer wg.Done() // Decrement the counter when the goroutine completes
for y := fromY; y < toY; y++ {
if player.knownGameMap.tiles[x][y] != gameMap.tiles[x][y] {
lck.Lock()
kind := gameMap.tiles[x][y]
err := sendTileUpdate(
uint32(x),
uint32(y),
kind,
player.connection)
if err == nil {
player.knownGameMap.tiles[x][y] = gameMap.tiles[x][y]
}
lck.Unlock()
}
}
}(x)
}
// Wait for all goroutines to finish
wg.Wait()
}
func (player *Player) updateRemotePlayerPlayers(players map[uint32]*Player, wgX *sync.WaitGroup) {
defer wgX.Done()
player.knownPlayers = make(map[uint32]*Player)
playersMutex.RLock()
for _, playerLoop := range players {
player.camera = &sdl.Rect{
X: player.gameObject.baseRect.X - (config.CameraW / 2),
Y: player.gameObject.baseRect.Y - (config.CameraH / 2),
W: config.CameraW,
H: config.CameraH,
}
for _, playerLoopRect := range playerLoop.gameObject.getCurrentRects() {
if player.camera.HasIntersection(playerLoop.gameObject.adjustRectWorld(playerLoopRect.rect)) && playerLoop.shields <= serverConfig.MaxShields {
if player.knownPlayers[playerLoop.playerID] == nil {
_ = sendOtherPlayer(
playerLoop.playerID,
uint32(player.gameObject.baseRect.X),
uint32(player.gameObject.baseRect.Y),
player.gameObject.orientation,
player.connection)
player.knownPlayers[playerLoop.playerID] = playerLoop
}
break
}
}
}
playersMutex.RUnlock()
}
func (player *Player) updateRemotePlayerBullets(bullets map[uint32]*Bullet, wgX *sync.WaitGroup) {
defer wgX.Done()
player.knownBullets = make(map[uint32]*Bullet)
bulletMutex.RLock()
for _, bullet := range bullets {
bulletRect := &sdl.Rect{
X: bullet.posX,
Y: bullet.posY,
W: 1,
H: 1,
}
player.camera = &sdl.Rect{
X: player.gameObject.baseRect.X - (config.CameraW / 2),
Y: player.gameObject.baseRect.Y - (config.CameraH / 2),
W: config.CameraW,
H: config.CameraH,
}
if player.camera.HasIntersection(bulletRect) {
if player.knownBullets[bullet.id] == nil {
r, g, b, a := bullet.color.RGBA()
_ = sendBullet(
uint32(bullet.posX),
uint32(bullet.posY),
bullet.direction,
uint8(r),
uint8(g),
uint8(b),
uint8(a),
bullet.id,
bullet.super,
bullet.ownerID,
player.connection)
player.knownBullets[bullet.id] = bullet
}
}
}
bulletMutex.RUnlock()
}
func (player *Player) updateRemotePlayerBulletParticles(bulletParticles map[uint32]*BulletParticle, wgX *sync.WaitGroup) {
defer wgX.Done()
player.knownBulletParticles = make(map[uint32]*BulletParticle)
bulletParticleMutex.RLock()
for _, bulletParticle := range bulletParticles {
bulletRect := &sdl.Rect{
X: bulletParticle.posX,
Y: bulletParticle.posY,
W: 1,
H: 1,
}
player.camera = &sdl.Rect{
X: player.gameObject.baseRect.X - (config.CameraW / 2),
Y: player.gameObject.baseRect.Y - (config.CameraH / 2),
W: config.CameraW,
H: config.CameraH,
}
if player.camera.HasIntersection(bulletRect) {
if player.knownBullets[bulletParticle.id] == nil {
r, g, b, a := bulletParticle.color.RGBA()
_ = sendBulletParticle(
uint32(bulletParticle.posX),
uint32(bulletParticle.posY),
bulletParticle.expirationTimer,
uint8(r),
uint8(g),
uint8(b),
uint8(a),
bulletParticle.id,
player.connection)
player.knownBulletParticles[bulletParticle.id] = bulletParticle
}
}
}
bulletParticleMutex.RUnlock()
}
func (player *Player) updateRemotePlayerBases(bases map[uint32]*Base, players map[uint32]*Player, wgX *sync.WaitGroup) {
defer wgX.Done()
player.knownBases = make(map[uint32]*Base)
if player.connection == nil || !config.Server {
return
}
baseMutex.RLock()
for _, base := range bases {
player.camera = &sdl.Rect{
X: player.gameObject.baseRect.X - (config.CameraW / 2),
Y: player.gameObject.baseRect.Y - (config.CameraH / 2),
W: config.CameraW,
H: config.CameraH,
}
for _, baseRect := range base.gameObject.getCurrentRects() {
if player.camera.HasIntersection(base.gameObject.adjustRectWorld(baseRect.rect)) {
if player.knownPlayers[base.ownerID] == nil {
_ = sendBaseLocation(
uint32(base.gameObject.baseRect.X),
uint32(base.gameObject.baseRect.Y),
base.ownerID,
players[base.ownerID].playerColorID,
player.connection)
player.knownBases[base.ownerID] = base
}
break
}
}
}
baseMutex.RUnlock()
}
func (player *Player) sendPlayerUpdate(wgX *sync.WaitGroup) bool {
defer wgX.Done()
if player.connection != nil && config.Server {
if player.previousEnergy != player.energy ||
player.previousShields != player.shields ||
player.previousAmmunition != player.ammunition {
err := sendPlayerUpdate(player.energy, player.ammunition, player.shields, player.connection)
player.previousEnergy = player.energy
player.previousShields = player.shields
player.previousAmmunition = player.ammunition
return err == nil
} else {
return true
}
}
return false
}
func (player *Player) sendServerInfoToPlayer() bool {
if player.connection != nil && config.Server {
err := sendServerInfo(
player.playerID,
player.playerColorID,
serverConfig.MaxEnergy,
serverConfig.MaxAmmunition,
serverConfig.MaxShields,
serverConfig.MapWidth,
serverConfig.MapHeight,
serverConfig.NormalShotCost,
serverConfig.SuperShotCost,
serverConfig.ReloadCost,
serverConfig.MovementCost,
serverConfig.DiggingCost,
serverConfig.ShootDiggingCostBonus,
serverConfig.ShootCooldown,
serverConfig.RechargeCooldownOwn,
serverConfig.RechargeCooldownOpponent,
serverConfig.RepairCooldown,
serverConfig.DiggingCooldown,
serverConfig.MovementCooldown,
serverConfig.MovementCooldownNoEnergy,
serverConfig.DiggingCooldownNoEnergy,
serverConfig.ReloadCooldown,
serverConfig.BlastRadius,
serverConfig.ReloadWait,
player.connection)
return err == nil
}
return false
}
func (player *Player) sendVersionBackToPlayer() bool {
if player.connection != nil && config.Server {
versionArray := getCurrentVersion()
err := sendPlayerStartResponse(
versionArray[0],
versionArray[1],
versionArray[2],
player.connection)
return err == nil
}
return false
}
func (player *Player) sendLocationToPlayer() bool {
if player.connection != nil && config.Server {
err := sendPositionUpdate(
uint32(player.gameObject.baseRect.X),
uint32(player.gameObject.baseRect.Y),
player.gameObject.orientation,
player.connection)
return err == nil
}
return false
}
func (player *Player) sendPositionToServer() bool {
if player.connection != nil && config.Client {
err := sendPlayerLocation(
uint32(player.gameObject.baseRect.X),
uint32(player.gameObject.baseRect.Y),
player.gameObject.orientation,
player.connection)
return err == nil
}
return false
}
func sendVersionToServer(connection *net.TCPConn) {
if connection != nil && config.Client {
versionArray := getCurrentVersion()
_ = sendPlayerStartRequest(
versionArray[0],
versionArray[1],
versionArray[2],
connection)
return
}
}
func (player *Player) sendDigToServer(posX, posY uint32, isShooting bool) {
if player.connection != nil && config.Client {
_ = sendDigBlock(
posX,
posY,
isShooting,
player.connection)
}
}
func (player *Player) sendShootToServer(isSuper bool) {
if player.connection != nil && config.Client {
_ = sendShoot(
isSuper,
player.connection)
}
}