455 lines
11 KiB
Go
455 lines
11 KiB
Go
package game
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"image/color"
|
|
"log"
|
|
"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 -= 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
|
|
for i, input := range inputs {
|
|
opp := 0
|
|
if i == 0 {
|
|
opp = 1
|
|
}
|
|
player = &g.Players[i]
|
|
opponent = &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))
|
|
|
|
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 == float64(component.PlayerSize)
|
|
// TODO check when last jump, grounded
|
|
if grounded {
|
|
g.Players[i].VY = 20
|
|
}
|
|
}
|
|
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 {
|
|
// Apply hitbox.
|
|
if frame.T == component.HitboxHurt {
|
|
// Hit opponent.
|
|
if oppRect.Overlaps(component.TranslateRect(frame.R, 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()
|
|
}
|
|
|
|
return gohan.Update()
|
|
}
|
|
|
|
func (g *Game) Draw(screen *ebiten.Image) {
|
|
err := gohan.Draw(screen)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (g *Game) Exit() {
|
|
os.Exit(0)
|
|
}
|