You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
802 lines
19 KiB
Go
802 lines
19 KiB
Go
package game
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"image/color"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.rocketnine.space/tslocum/boxbrawl/asset"
|
|
"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/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 {
|
|
// Set up entity component system.
|
|
entity.NewOnceEntity()
|
|
gohan.AddSystem(&system.MapSystem{})
|
|
gohan.AddSystem(&system.PlayerSystem{})
|
|
gohan.AddSystem(&system.UISystem{})
|
|
|
|
// Start playing music.
|
|
if !world.StartMuted {
|
|
asset.SoundMusic.Play()
|
|
}
|
|
|
|
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,
|
|
Grounded: true,
|
|
}
|
|
|
|
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,
|
|
Grounded: true,
|
|
}
|
|
|
|
g.Winner = 0
|
|
|
|
botTauntTicks = 0
|
|
botTauntTotalTicks = 0
|
|
|
|
g.playerStateUpdated()
|
|
}
|
|
|
|
func (g *Game) Layout(_, _ int) (screenWidth, screenHeight int) {
|
|
// Maintain constant internal resolution.
|
|
if world.InternalScreenWidth != world.ScreenWidth || world.InternalScreenHeight != world.ScreenHeight {
|
|
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.Printf("failed to read port in network address: %s", err)
|
|
world.ConnectPromptConfirmed = false
|
|
return
|
|
}
|
|
|
|
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) ReadInputsP1() InputBits {
|
|
var in InputBits
|
|
|
|
if ebiten.IsKeyPressed(ebiten.KeyW) {
|
|
in.setButtonOn(ButtonUp)
|
|
}
|
|
if ebiten.IsKeyPressed(ebiten.KeyS) {
|
|
in.setButtonOn(ButtonDown)
|
|
}
|
|
if ebiten.IsKeyPressed(ebiten.KeyA) {
|
|
in.setButtonOn(ButtonLeft)
|
|
}
|
|
if 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) || ebiten.IsKeyPressed(ebiten.KeyKPEnter) {
|
|
in.setButtonOn(ButtonStart)
|
|
}
|
|
|
|
return in
|
|
}
|
|
|
|
func (g *Game) ReadInputsP2() InputBits {
|
|
var in InputBits
|
|
|
|
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) {
|
|
in.setButtonOn(ButtonUp)
|
|
}
|
|
if ebiten.IsKeyPressed(ebiten.KeyArrowDown) {
|
|
in.setButtonOn(ButtonDown)
|
|
}
|
|
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
|
|
in.setButtonOn(ButtonLeft)
|
|
}
|
|
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
|
|
in.setButtonOn(ButtonRight)
|
|
}
|
|
|
|
if ebiten.IsKeyPressed(ebiten.KeySlash) {
|
|
in.setButtonOn(ButtonPunch)
|
|
}
|
|
|
|
if ebiten.IsKeyPressed(ebiten.KeyQuote) {
|
|
in.setButtonOn(ButtonKick)
|
|
}
|
|
|
|
if ebiten.IsKeyPressed(ebiten.KeyBracketRight) {
|
|
in.setButtonOn(ButtonBlock)
|
|
}
|
|
|
|
if ebiten.IsKeyPressed(ebiten.KeyBackspace) {
|
|
in.setButtonOn(ButtonTaunt)
|
|
}
|
|
|
|
if ebiten.IsKeyPressed(ebiten.KeyEnter) || ebiten.IsKeyPressed(ebiten.KeyKPEnter) {
|
|
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
|
|
}
|
|
}
|
|
|
|
outOfBounds := p.Y < -world.GroundHeight-component.PlayerHeight
|
|
if outOfBounds {
|
|
p.VX, p.VY = 0, 0
|
|
} else 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
|
|
}
|
|
|
|
p.Grounded = collideG != -1
|
|
|
|
// Advance walking animation frame.
|
|
if p.Walking() {
|
|
const walkFrames = 8
|
|
if p.WalkFrameReverse {
|
|
p.WalkFrame--
|
|
if p.WalkFrame < 0 {
|
|
p.WalkFrame = 1
|
|
p.WalkFrameReverse = false
|
|
}
|
|
} else {
|
|
p.WalkFrame++
|
|
if p.WalkFrame >= walkFrames {
|
|
p.WalkFrame = walkFrames - 2
|
|
p.WalkFrameReverse = true
|
|
}
|
|
}
|
|
}
|
|
|
|
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 && outOfBounds {
|
|
g.Winner = o.PlayerNum
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *Game) UpdateByInputs(inputs []InputBits) {
|
|
const (
|
|
punchHitStrength = 4.0
|
|
punchStunTicks = 15
|
|
kickHitStrength = 7.0
|
|
KickStunTicks = 19
|
|
)
|
|
|
|
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
|
|
|
|
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
|
|
|
|
// Advance crouching animation frame.
|
|
const crouchFrames = 10
|
|
if player.Crouching && player.CrouchFrame < crouchFrames-1 {
|
|
player.CrouchFrame++
|
|
} else if !player.Crouching && player.CrouchFrame != 0 {
|
|
player.CrouchFrame--
|
|
}
|
|
|
|
g.Players[i].Crouching = false
|
|
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 g.Players[i].NoPunch && !input.isButtonOn(ButtonPunch) {
|
|
g.Players[i].NoPunch = false
|
|
}
|
|
if g.Players[i].NoKick && !input.isButtonOn(ButtonKick) {
|
|
g.Players[i].NoKick = false
|
|
}
|
|
|
|
if input.isButtonOn(ButtonPunch) && !g.Players[i].NoPunch {
|
|
g.Players[i].Action = component.ActionPunch
|
|
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionPunch])
|
|
|
|
g.Players[i].NoPunch = true
|
|
continue
|
|
}
|
|
if input.isButtonOn(ButtonKick) && !g.Players[i].NoKick {
|
|
g.Players[i].Action = component.ActionKick
|
|
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionKick])
|
|
|
|
g.Players[i].NoKick = true
|
|
continue
|
|
}
|
|
|
|
if input.isButtonOn(ButtonUp) && g.Players[i].Grounded {
|
|
g.Players[i].VY = world.JumpVelocity
|
|
}
|
|
if input.isButtonOn(ButtonDown) {
|
|
g.Players[i].Crouching = true
|
|
}
|
|
if input.isButtonOn(ButtonLeft) {
|
|
g.Players[i].VX = -1
|
|
}
|
|
if input.isButtonOn(ButtonRight) {
|
|
g.Players[i].VX = 1
|
|
}
|
|
} else {
|
|
g.Players[i].CrouchFrame = 0
|
|
}
|
|
|
|
// 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 := punchHitStrength
|
|
stunTicks := punchStunTicks
|
|
if player.Action == component.ActionKick {
|
|
hitStrength = kickHitStrength
|
|
stunTicks = KickStunTicks
|
|
}
|
|
|
|
// Apply blocking.
|
|
if g.Players[opp].Action == component.ActionBlock {
|
|
hitStrength /= 4.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 = stunTicks
|
|
|
|
// Play hit sound.
|
|
sound := asset.SoundHitP1
|
|
if i == 1 {
|
|
sound = asset.SoundHitP2
|
|
}
|
|
sound.SetVolume(0.2)
|
|
sound.Rewind()
|
|
sound.Play()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 g.Players[i].Action == component.ActionBlock && 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) {
|
|
if world.Local {
|
|
g.reset()
|
|
} else {
|
|
g.Players[i].PlayAgain = true
|
|
}
|
|
}
|
|
}
|
|
|
|
g.applyPhysics()
|
|
}
|
|
|
|
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.ReadInputsP1()
|
|
|
|
if world.AI == world.AIStandard {
|
|
inputs[1] = botInput()
|
|
} else if world.AI == world.AIMirror {
|
|
inputs[1] = mirrorInput(inputs[0])
|
|
} else if world.AI == world.AIBlock {
|
|
inputs[1] = inputs[0]
|
|
inputs[1].setButtonOn(ButtonBlock)
|
|
} else { // AINone
|
|
inputs[1] = g.ReadInputsP2()
|
|
}
|
|
|
|
g.UpdateByInputs(inputs)
|
|
g.playerStateUpdated()
|
|
}
|
|
|
|
func (g *Game) RunFrame() {
|
|
input := g.ReadInputsP1()
|
|
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
|
|
}
|
|
|
|
// Toggle fullscreen.
|
|
if (inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeyKPEnter)) && ebiten.IsKeyPressed(ebiten.KeyAlt) {
|
|
world.Fullscreen = !world.Fullscreen
|
|
ebiten.SetFullscreen(world.Fullscreen)
|
|
return nil
|
|
}
|
|
|
|
// Change local opponent type.
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyO) && ebiten.IsKeyPressed(ebiten.KeyControl) {
|
|
switch world.AI {
|
|
case world.AIStandard:
|
|
world.AI = world.AINone
|
|
case world.AINone:
|
|
world.AI = world.AIMirror
|
|
case world.AIMirror:
|
|
world.AI = world.AIBlock
|
|
default:
|
|
world.AI = world.AIStandard
|
|
}
|
|
}
|
|
|
|
// Toggle music.
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyM) && ebiten.IsKeyPressed(ebiten.KeyControl) {
|
|
if asset.SoundMusic.IsPlaying() {
|
|
asset.SoundMusic.Pause()
|
|
} else {
|
|
asset.SoundMusic.Play()
|
|
}
|
|
}
|
|
|
|
// Change debug level.
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyV) && ebiten.IsKeyPressed(ebiten.KeyControl) {
|
|
world.Debug++
|
|
if world.Debug > world.MaxDebug {
|
|
world.Debug = 0
|
|
}
|
|
}
|
|
|
|
// Decreate TPS.
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Increase TPS.
|
|
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)
|
|
}
|
|
}
|
|
|
|
// RunHeadless runs the game without using the display. Game.Draw is never called.
|
|
func (g *Game) RunHeadless() error {
|
|
var (
|
|
t = time.NewTicker(time.Second / time.Duration(world.TPS))
|
|
err error
|
|
)
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
err = g.Update()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *Game) Exit() {
|
|
os.Exit(0)
|
|
}
|