boxbrawl/game/game.go

471 lines
12 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"
)
type Game struct {
Players []component.Player
}
var addedGame bool
func NewGame() (*Game, error) {
player1 := component.Player{
PlayerNum: 1,
Color: color.RGBA{255, 0, 0, 255},
Action: component.ActionIdle,
ActionTicksLeft: 1,
X: 50,
Y: 50,
}
player2 := component.Player{
PlayerNum: 2,
Color: color.RGBA{0, 0, 255, 255},
Action: component.ActionIdle,
ActionTicksLeft: 1,
X: 150,
Y: 50,
}
g := &Game{
Players: []component.Player{player1, player2},
}
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) 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")
// TODO
}
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 := world.LocalPort
if localPort == 0 {
localPort = p + 1
}
if world.LocalPort != 0 {
log.Printf("Client port for connection: %d", world.LocalPort)
}
numPlayers := 2
playerSize := 20
players := make([]ggpo.Player, numPlayers)
if world.ConnectPromptHost {
log.Printf("Hosting at " + address + ":" + port + "...")
players[0] = ggpo.NewLocalPlayer(playerSize, 1)
players[1] = ggpo.NewRemotePlayer(playerSize, 2, "127.0.0.1", localPort)
} else {
log.Printf("Connecting to " + address + ":" + port + "...")
world.CurrentPlayer = 2
players[0] = ggpo.NewRemotePlayer(playerSize, 1, address, p)
players[1] = ggpo.NewLocalPlayer(playerSize, 2)
}
l := p
if !world.ConnectPromptHost {
l = localPort
}
g.InitNetworking(l, 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 {
world.CurrentPlayer = int(handle)
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.setButton(ButtonUp)
}
if ebiten.IsKeyPressed(ebiten.KeyArrowDown) || ebiten.IsKeyPressed(ebiten.KeyS) {
in.setButton(ButtonDown)
}
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA) {
in.setButton(ButtonLeft)
}
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD) {
in.setButton(ButtonRight)
}
if inpututil.IsKeyJustPressed(ebiten.KeyH) {
in.setButton(ButtonPunch)
}
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]
// Apply gravity.
if 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
}
}
// Apply velocity.
p.X, p.Y = p.X+p.VX, p.Y+p.VY
// Apply player collision.
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))
if playerRect.Overlaps(oppRect) {
if playerRect.Min.X < oppRect.Min.X {
p.X = o.X - component.PlayerSize
} else {
p.X = o.X + component.PlayerSize
}
}
// Apply ground collision.
if p.Y < 0 {
p.Y = 0
}
}
}
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 player.Action != component.ActionStunned {
if input.isButtonOn(ButtonUp) && !component.TranslateRect(playerRect, 0, -1).Overlaps(oppRect) {
grounded := g.Players[i].Y > -world.FloatValueThreshold && g.Players[i].Y < world.FloatValueThreshold
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
}
if g.Players[i].Action == component.ActionIdle {
if input.isButtonOn(ButtonPunch) {
g.Players[i].Action = component.ActionPunch
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionPunch]) // TODO
continue
}
}
}
// TODO player starts in idle action?
if g.Players[i].ActionTicksLeft != 0 {
// Run current frame logic.
// TODO handle invulnerability and blocking
allFrameData := component.FrameDataForActionTick(g.Players[i].Action, g.Players[i].ActionTicksLeft)
for _, frame := range allFrameData {
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))) {
// Send the opponent flying in some direction.
if opponent.X <= player.X { // Opponent is to the left of the player.
opponent.VX = -4
} else { // Opponent is to the right of the player.
opponent.VX = 4
}
// Stun the opponent.
opponent.Action = component.ActionStunned
opponent.ActionTicksLeft = 7
}
}
}
// Advance to the next frame.
g.Players[i].ActionTicksLeft--
if g.Players[i].ActionTicksLeft == 0 {
// Return to the idle action.
g.Players[i].Action = component.ActionIdle
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionIdle]) // TODO
}
}
}
g.applyPhysics()
}
func (g *Game) ReadInputsP2() InputBits {
var in InputBits
// TODO Support local multiplayer?
return in
}
func (g *Game) playerStateUpdated() {
world.Player1, world.Player2 = g.Players[0], g.Players[1]
}
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.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 {
err := world.Backend.Idle(0)
if err != nil {
panic(err)
}
g.RunFrame()
}
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)
}