boxbrawl/game/game.go

654 lines
16 KiB
Go

package game
import (
"crypto/sha1"
"image/color"
"log"
"math"
"os"
"strconv"
"strings"
"code.rocketnine.space/tslocum/boxbrawl/component"
"code.rocketnine.space/tslocum/boxbrawl/entity"
"code.rocketnine.space/tslocum/boxbrawl/system"
"code.rocketnine.space/tslocum/boxbrawl/world"
"code.rocketnine.space/tslocum/etk"
"code.rocketnine.space/tslocum/gohan"
"github.com/assemblaj/ggpo"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
const (
playerSpawnGap = 50
)
type Game struct {
Players []component.Player
Winner int
}
var addedGame bool
func NewGame() (*Game, error) {
g := &Game{
Players: make([]component.Player, 2),
}
g.reset()
if !addedGame {
entity.NewOnceEntity()
gohan.AddSystem(&system.MapSystem{})
gohan.AddSystem(&system.PlayerSystem{})
gohan.AddSystem(&system.UISystem{})
addedGame = true
}
return g, nil
}
func (g *Game) clone() (result *Game) {
result = &Game{}
*result = *g
result.Players = make([]component.Player, len(g.Players))
for i := range g.Players {
result.Players[i] = g.Players[i].Clone()
}
return
}
func (g *Game) reset() {
g.Players[0] = component.Player{
PlayerNum: 1,
Color: color.RGBA{255, 0, 0, 255},
Action: component.ActionIdle,
ActionTicksLeft: len(component.AllPlayerFrames[component.ActionIdle]),
X: -playerSpawnGap - component.PlayerWidth,
Y: 0,
}
g.Players[1] = component.Player{
PlayerNum: 2,
Color: color.RGBA{0, 0, 255, 255},
Action: component.ActionIdle,
ActionTicksLeft: len(component.AllPlayerFrames[component.ActionIdle]),
X: playerSpawnGap,
Y: 0,
}
g.Winner = 0
}
func (g *Game) Layout(_, _ int) (screenWidth, screenHeight int) {
// Maintain constant internal resolution.
if world.InternalScreenWidth != world.ScreenWidth || world.InternalScreenHeight != world.ScreenHeight {
if world.ScreenWidth != 0 || world.ScreenHeight != 0 {
etk.Layout(world.InternalScreenWidth, world.InternalScreenHeight)
}
world.ScreenWidth, world.ScreenHeight = world.InternalScreenWidth, world.InternalScreenHeight
}
return world.ScreenWidth, world.ScreenHeight
}
func (g *Game) startLocalGame() {
log.Println("Playing against the computer")
world.Local = true
world.ConnectionActive = true
world.ConnectPromptVisible = false
}
func (g *Game) startNetworkGame() {
address := ""
port := world.ConnectPromptText
if strings.ContainsRune(port, ':') {
split := strings.Split(port, ":")
if len(split) == 2 {
address = split[0]
port = split[1]
}
}
p, err := strconv.Atoi(port)
if err != nil {
log.Fatalf("failed to read port: %s", err)
}
localPort := p
if world.LocalPort != 0 {
localPort = world.LocalPort
}
log.Printf("Client port for connection: %d", localPort)
numPlayers := 2
playerSize := 20
players := make([]ggpo.Player, numPlayers)
if world.ConnectPromptHost {
log.Printf("Connecting to " + address + ":" + port + " as host...")
players[0] = ggpo.NewLocalPlayer(playerSize, 1)
players[1] = ggpo.NewRemotePlayer(playerSize, 2, address, p)
} else {
log.Printf("Connecting to " + address + ":" + port + " as guest...")
world.CurrentPlayer = 2
players[0] = ggpo.NewRemotePlayer(playerSize, 1, address, p)
players[1] = ggpo.NewLocalPlayer(playerSize, 2)
}
g.InitNetworking(localPort, numPlayers, players, 0)
world.ConnectionActive = true
}
func (g *Game) InitNetworking(localPort int, numPlayers int, players []ggpo.Player, numSpectators int) {
session := NewGameSession()
var inputBits InputBits = 0
inputSize := len(encodeInputs(inputBits))
peer := ggpo.NewPeer(session, localPort, numPlayers, inputSize)
world.Backend = &peer
session.backend = &peer
err := peer.InitializeConnection()
if err != nil {
log.Fatalf("failed to initialize connection: %s", err)
}
err = peer.SetDisconnectTimeout(3000)
if err != nil {
log.Fatalf("failed to set disconnect timeout: %s", err)
}
err = peer.SetDisconnectNotifyStart(1000)
if err != nil {
log.Fatalf("failed to set disconnect notify start: %s", err)
}
peer.Start()
for i := 0; i < numPlayers+numSpectators; i++ {
var handle ggpo.PlayerHandle
err = peer.AddPlayer(&players[i], &handle)
if err != nil {
log.Fatalf("failed to add player: %s", err)
}
if players[i].PlayerType == ggpo.PlayerTypeLocal {
err = peer.SetFrameDelay(handle, frameDelay)
if err != nil {
log.Fatalf("failed to set frame delay: %s", err)
}
}
}
}
func (g *Game) ReadInputs() InputBits {
var in InputBits
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW) {
in.setButtonOn(ButtonUp)
}
if ebiten.IsKeyPressed(ebiten.KeyArrowDown) || ebiten.IsKeyPressed(ebiten.KeyS) {
in.setButtonOn(ButtonDown)
}
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA) {
in.setButtonOn(ButtonLeft)
}
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD) {
in.setButtonOn(ButtonRight)
}
if ebiten.IsKeyPressed(ebiten.KeyH) {
in.setButtonOn(ButtonPunch)
}
if ebiten.IsKeyPressed(ebiten.KeyJ) {
in.setButtonOn(ButtonKick)
}
if ebiten.IsKeyPressed(ebiten.KeyK) {
in.setButtonOn(ButtonBlock)
}
if ebiten.IsKeyPressed(ebiten.KeyL) {
in.setButtonOn(ButtonTaunt)
}
if ebiten.IsKeyPressed(ebiten.KeyEnter) {
in.setButtonOn(ButtonStart)
}
return in
}
func (g *Game) applyPhysics() {
for i := 0; i < 2; i++ {
opp := 0
if i == 0 {
opp = 1
}
p := &g.Players[i]
o := &g.Players[opp]
playerRect := world.FloatRect(g.Players[i].X, g.Players[i].Y, g.Players[i].X+float64(component.PlayerSize), g.Players[i].Y+float64(component.PlayerSize))
oppRect := world.FloatRect(g.Players[opp].X, g.Players[opp].Y, g.Players[opp].X+float64(component.PlayerSize), g.Players[opp].Y+float64(component.PlayerSize))
// Apply ground collision.
var (
collideXY = -1
collideX = -1
collideY = -1
collideG = -1
)
for j, physRect := range world.PhysicsRects {
if physRect.Overlaps(playerRect) {
collideXY = j
collideX = j
collideY = j
}
if physRect.Overlaps(component.TranslateRect(playerRect, int(p.VX), int(p.VY))) {
collideXY = j
}
if physRect.Overlaps(component.TranslateRect(playerRect, int(p.VX), 0)) {
collideX = j
}
if physRect.Overlaps(component.TranslateRect(playerRect, 0, int(p.VY))) {
collideY = j
}
if physRect.Overlaps(component.TranslateRect(playerRect, 0, -1)) {
collideG = j
}
}
if collideG == -1 && p.VY > -world.Gravity {
p.VY -= math.Max(math.Abs(p.VY/2.5), 0.1)
if p.VY < -world.Gravity {
p.VY = -world.Gravity
}
}
if collideXY == -1 {
p.X, p.Y = p.X+p.VX, p.Y+p.VY
} else if collideX == -1 {
p.X = p.X + p.VX
p.VY = 0
} else if collideY == -1 {
p.Y = p.Y + p.VY
p.VX = 0
} else {
p.VX, p.VY = 0, 0
}
if collideX != -1 && collideY != -1 {
collideRect := world.PhysicsRects[collideX]
leftDist := p.X + component.PlayerWidth - float64(collideRect.Min.X)
rightDist := p.X - float64(collideRect.Max.X)
if leftDist < rightDist { // Closer to left.
p.X = float64(collideRect.Min.X) - component.PlayerWidth
} else { // Closer to right.
p.X = float64(collideRect.Max.X)
}
bottomDist := p.Y - component.PlayerHeight - float64(collideRect.Min.Y)
topDist := p.Y - float64(collideRect.Max.Y)
if bottomDist < topDist { // Closer to bottom.
p.Y = float64(collideRect.Min.Y) - component.PlayerHeight
} else { // Closer to top.
p.Y = float64(collideRect.Max.Y)
}
}
// Apply player collision.
if playerRect.Overlaps(oppRect) {
if playerRect.Min.X < oppRect.Min.X {
p.X = o.X - component.PlayerSize
} else {
p.X = o.X + component.PlayerSize
}
p.VX, o.VX = o.VX, p.VX // TODO
}
if g.Winner == 0 && p.Y < -world.GroundHeight {
g.Winner = o.PlayerNum
}
}
}
func (g *Game) UpdateByInputs(inputs []InputBits) {
var player, opponent *component.Player
var playerFlipped, oppFlipped bool
for i, input := range inputs {
opp := 0
if i == 0 {
opp = 1
}
player = &g.Players[i]
opponent = &g.Players[opp]
if component.PlayerOnRightSide(*player, *opponent) { // Player is on the right side.
playerFlipped = true
oppFlipped = false
} else { // Opponent is on the right side.
playerFlipped = false
oppFlipped = true
}
_ = oppFlipped // TODO
playerRect := world.FloatRect(g.Players[i].X, g.Players[i].Y, g.Players[i].X+float64(component.PlayerSize), g.Players[i].Y+float64(component.PlayerSize))
oppRect := world.FloatRect(g.Players[opp].X, g.Players[opp].Y, g.Players[opp].X+float64(component.PlayerSize), g.Players[opp].Y+float64(component.PlayerSize))
g.Players[i].VX = g.Players[i].VX * 0.8
g.Players[i].VY = g.Players[i].VY * 0.8
if g.Players[i].Action == component.ActionIdle {
if input.isButtonOn(ButtonBlock) {
g.Players[i].Action = component.ActionBlock
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionBlock])
continue
}
if input.isButtonOn(ButtonTaunt) {
var tauntAction component.PlayerAction
switch {
case input.isButtonOn(ButtonUp):
tauntAction = component.ActionTaunt1
case input.isButtonOn(ButtonRight):
tauntAction = component.ActionTaunt2
case input.isButtonOn(ButtonDown):
tauntAction = component.ActionTaunt3
case input.isButtonOn(ButtonLeft):
tauntAction = component.ActionTaunt4
}
if tauntAction != 0 {
g.Players[i].Action = tauntAction
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[tauntAction])
continue
}
}
if input.isButtonOn(ButtonPunch) {
if !g.Players[i].NoPunch {
g.Players[i].Action = component.ActionPunch
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionPunch]) // TODO
g.Players[i].NoPunch = true
continue
}
} else if g.Players[i].NoPunch {
g.Players[i].NoPunch = false
}
if input.isButtonOn(ButtonUp) && !component.TranslateRect(playerRect, 0, -1).Overlaps(oppRect) {
var grounded bool
for _, physRect := range world.PhysicsRects {
if g.Players[i].Y < float64(physRect.Max.Y)+world.FloatValueThreshold && g.Players[i].Y > float64(physRect.Max.Y)-world.FloatValueThreshold {
grounded = true
break
}
}
if grounded {
g.Players[i].VY = world.JumpVelocity
}
}
if input.isButtonOn(ButtonDown) && !component.TranslateRect(playerRect, 0, 1).Overlaps(oppRect) {
//g.Players[i].VY = -1
// TODO crouch
}
if input.isButtonOn(ButtonLeft) && !component.TranslateRect(playerRect, -1, 0).Overlaps(oppRect) {
g.Players[i].VX = -1
}
if input.isButtonOn(ButtonRight) && !component.TranslateRect(playerRect, 1, 0).Overlaps(oppRect) {
g.Players[i].VX = 1
}
}
// TODO player starts in idle action?
if g.Players[i].ActionTicksLeft != 0 {
// Run current frame logic.
// TODO handle invulnerability and blocking
var goToFrame int
allFrameData := component.FrameDataForActionTick(g.Players[i].Action, g.Players[i].ActionTicksLeft)
for _, frame := range allFrameData {
if frame.G != 0 {
goToFrame = frame.G
}
frameRect := component.FlipRect(frame.R, playerFlipped)
// Apply hitbox.
if frame.T == component.HitboxHurt {
// Hit opponent.
if oppRect.Overlaps(component.TranslateRect(frameRect, int(player.X), int(player.Y))) {
hitStrength := 4.0
if g.Players[opp].Action == component.ActionBlock {
hitStrength = 1.0
}
// Send the opponent flying in some direction.
if opponent.X <= player.X { // Opponent is to the left of the player.
opponent.VX = -hitStrength
} else { // Opponent is to the right of the player.
opponent.VX = hitStrength
}
// Stun the opponent.
if g.Players[opp].Action != component.ActionBlock {
opponent.Action = component.ActionStunned
opponent.ActionTicksLeft = 14
}
}
}
}
if !input.isButtonOn(ButtonTaunt) {
goToFrame = 0
}
if goToFrame != 0 {
// Handle frame jump.
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[g.Players[i].Action]) - goToFrame
} else {
// Advance to the next frame.
g.Players[i].ActionTicksLeft--
}
if g.Players[i].ActionTicksLeft == 0 {
// Hold block.
if input.isButtonOn(ButtonBlock) {
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionBlock])
continue
}
// Return to the idle action.
g.Players[i].Action = component.ActionIdle
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionIdle]) // TODO
}
}
if world.Winner != 0 && input.isButtonOn(ButtonStart) {
g.Players[i].PlayAgain = true
}
}
g.applyPhysics()
}
func (g *Game) ReadInputsP2() InputBits {
var in InputBits
// TODO Support local multiplayer?
return in
}
func (g *Game) playerStateUpdated() {
if g.Winner != 0 && g.Players[0].PlayAgain && g.Players[1].PlayAgain {
g.reset()
}
world.Player1, world.Player2 = g.Players[0], g.Players[1]
world.Winner = g.Winner
}
func (g *Game) RunLocalFrame() {
inputs := make([]InputBits, 2)
inputs[0] = g.ReadInputs()
if world.AI == world.AIMirror {
inputs[1] = mirrorInput(inputs[0])
} else if world.AI == world.AIBlock {
inputs[1] = inputs[0]
inputs[1].setButtonOn(ButtonBlock)
}
g.UpdateByInputs(inputs)
g.playerStateUpdated()
}
func (g *Game) RunFrame() {
input := g.ReadInputs()
buffer := encodeInputs(input)
//fmt.Println("Attempting to add local inputs")
result := world.Backend.AddLocalInput(ggpo.PlayerHandle(world.CurrentPlayer), buffer, len(buffer))
//fmt.Println("Attempt to add local inputs complete")
if result == nil {
//fmt.Println("Attempt to add local inputs was successful")
var values [][]byte
disconnectFlags := 0
//fmt.Println("Attempting to synchronize inputs")
values, result = world.Backend.SyncInput(&disconnectFlags)
if result == nil {
//fmt.Println("Attempt synchronize inputs was sucessful")
inputs := decodeInputs(values)
//fmt.Println("Advancing Frame from game loop")
g.AdvanceFrame(inputs, disconnectFlags)
} else {
//fmt.Printf("Attempt synchronize inputs was unsuccessful: %s\n", result)
}
} else {
//fmt.Printf("Attempt to add local inputs unsuccessful: %s\n", result)
}
g.playerStateUpdated()
}
func (g *Game) Checksum() int {
h := sha1.New()
h.Write([]byte(g.String()))
toSum := h.Sum(nil)
sum := 0
for _, v := range toSum {
sum += int(v)
}
return sum
}
func (g *Game) AdvanceFrame(inputs []InputBits, disconnectFlags int) {
g.UpdateByInputs(inputs)
err := world.Backend.AdvanceFrame(uint32(g.Checksum()))
if err != nil {
panic(err)
}
}
func (g *Game) Update() error {
if ebiten.IsWindowBeingClosed() || (!world.WASM && ebiten.IsKeyPressed(ebiten.KeyEscape)) {
g.Exit()
return nil
}
if inpututil.IsKeyJustPressed(ebiten.KeyI) && ebiten.IsKeyPressed(ebiten.KeyControl) {
switch world.AI {
case world.AIMirror:
world.AI = world.AIBlock
case world.AIBlock:
world.AI = world.AINone
default:
world.AI = world.AIMirror
}
}
if inpututil.IsKeyJustPressed(ebiten.KeyV) && ebiten.IsKeyPressed(ebiten.KeyControl) {
world.Debug++
if world.Debug > world.MaxDebug {
world.Debug = 0
}
}
if inpututil.IsKeyJustPressed(ebiten.KeyMinus) && ebiten.IsKeyPressed(ebiten.KeyControl) {
for i, preset := range world.TPSPresets {
if preset == world.TPS {
if i > 0 {
world.TPS = world.TPSPresets[i-1]
ebiten.SetTPS(world.TPS)
log.Printf("Set TPS to %d", world.TPS)
break
}
}
}
}
if inpututil.IsKeyJustPressed(ebiten.KeyEqual) && ebiten.IsKeyPressed(ebiten.KeyControl) {
for i, preset := range world.TPSPresets {
if preset == world.TPS {
if i < len(world.TPSPresets)-1 {
world.TPS = world.TPSPresets[i+1]
ebiten.SetTPS(world.TPS)
log.Printf("Set TPS to %d", world.TPS)
break
}
}
}
}
if world.ConnectPromptConfirmed && !world.ConnectionActive {
if world.ConnectPromptText == "" {
g.startLocalGame()
} else {
g.startNetworkGame()
}
g.playerStateUpdated()
}
if world.ConnectionActive {
if !world.Local {
err := world.Backend.Idle(0)
if err != nil {
panic(err)
}
g.RunFrame()
} else {
g.RunLocalFrame()
}
}
err := gohan.Update()
if err != nil {
return err
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
err := gohan.Draw(screen)
if err != nil {
log.Fatal(err)
}
}
func (g *Game) Exit() {
os.Exit(0)
}