363 lines
9.3 KiB
Go
363 lines
9.3 KiB
Go
package system
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
|
|
"github.com/assemblaj/ggpo"
|
|
|
|
"code.rocketnine.space/tslocum/boxbrawl/component"
|
|
"code.rocketnine.space/tslocum/boxbrawl/world"
|
|
"code.rocketnine.space/tslocum/etk"
|
|
"code.rocketnine.space/tslocum/gohan"
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
|
)
|
|
|
|
const uiStartPrompt = `BOX BRAWL`
|
|
const uiBrowserIntro = `Click anywhere to enable keyboard input.`
|
|
const uiComputerPrompt = `Press <Enter> to play against the computer.`
|
|
const uiHostPrompt = `Press <H> to host a match against a remote player.`
|
|
const uiHostInfoPrompt = `Type your opponent's IP address and port (address:port) to host a match.`
|
|
const uiHostStartPrompt = `Press <Enter> to start hosting.`
|
|
const uiRemotePrompt = `Type your opponent's IP address and port (address:port) to join a match.`
|
|
const uiConnectPrompt = `Press <Enter> to connect.`
|
|
const uiBrowserPrompt = `Playing against remote players is unavailable in the browser version.
|
|
|
|
Download Box Brawl for Windows or Linux to play against remote players.`
|
|
const uiHostListeningPrompt = `Waiting for opponent to connect from %s...`
|
|
const uiClientConnectingPrompt = `Connecting to %s...`
|
|
|
|
type UISystem struct {
|
|
*component.Once
|
|
|
|
initialized bool
|
|
buffer *etk.Text
|
|
updateTicks int
|
|
|
|
tmpImg *ebiten.Image
|
|
|
|
hitboxImg *ebiten.Image
|
|
}
|
|
|
|
func (u *UISystem) initialize() {
|
|
u.buffer = etk.NewText("")
|
|
u.updateBuffer()
|
|
|
|
inputDemo := etk.NewFlex()
|
|
inputDemo.SetVertical(true)
|
|
|
|
inputDemo.AddChild(u.buffer)
|
|
|
|
etk.SetRoot(inputDemo)
|
|
etk.Layout(world.InternalScreenWidth, world.InternalScreenHeight)
|
|
|
|
u.tmpImg = ebiten.NewImage(250, 100)
|
|
|
|
u.hitboxImg = ebiten.NewImage(32, 32)
|
|
|
|
u.initialized = true
|
|
}
|
|
|
|
func (u *UISystem) updateBuffer() {
|
|
prompt := []byte(uiStartPrompt)
|
|
|
|
if world.WASM {
|
|
prompt = append(prompt, []byte("\n\n"+uiBrowserIntro)...)
|
|
prompt = append(prompt, []byte("\n\n"+uiComputerPrompt)...)
|
|
prompt = append(prompt, []byte("\n\n"+uiBrowserPrompt)...)
|
|
} else if world.ConnectionActive {
|
|
if world.ConnectPromptHost {
|
|
prompt = append(prompt, []byte("\n\n"+fmt.Sprintf(uiHostListeningPrompt, world.ConnectPromptText))...)
|
|
} else {
|
|
prompt = append(prompt, []byte("\n\n"+fmt.Sprintf(uiClientConnectingPrompt, world.ConnectPromptText))...)
|
|
}
|
|
} else {
|
|
promptEntered := len(world.ConnectPromptText) != 0
|
|
if promptEntered || world.ConnectPromptHost {
|
|
prompt = append(prompt, []byte("\n\n"+world.ConnectPromptText+"_")...)
|
|
if world.ConnectPromptHost {
|
|
prompt = append(prompt, []byte("\n\n"+uiHostInfoPrompt)...)
|
|
prompt = append(prompt, []byte("\n\n"+uiHostStartPrompt)...)
|
|
} else {
|
|
prompt = append(prompt, []byte("\n\n"+uiConnectPrompt)...)
|
|
prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...)
|
|
}
|
|
} else {
|
|
prompt = append(prompt, []byte("\n\n"+uiComputerPrompt)...)
|
|
prompt = append(prompt, []byte("\n\n"+uiHostPrompt)...)
|
|
prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...)
|
|
}
|
|
}
|
|
|
|
u.buffer.Clear()
|
|
u.buffer.Write(prompt)
|
|
}
|
|
|
|
func (u *UISystem) Update(e gohan.Entity) error {
|
|
if !u.initialized {
|
|
u.initialize()
|
|
}
|
|
|
|
if !world.ConnectPromptVisible {
|
|
return nil
|
|
}
|
|
|
|
var updated bool
|
|
|
|
if !world.WASM {
|
|
if len(world.ConnectPromptText) == 0 && ebiten.IsKeyPressed(ebiten.KeyH) {
|
|
world.ConnectPromptHost = true
|
|
u.updateBuffer()
|
|
updated = true
|
|
}
|
|
|
|
var a string
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit0) {
|
|
a += "0"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit1) {
|
|
a += "1"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit2) {
|
|
a += "2"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit3) {
|
|
a += "3"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit4) {
|
|
a += "4"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit5) {
|
|
a += "5"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit6) {
|
|
a += "6"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit7) {
|
|
a += "7"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit8) {
|
|
a += "8"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyDigit9) {
|
|
a += "9"
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyPeriod) {
|
|
a += "."
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeySemicolon) && ebiten.IsKeyPressed(ebiten.KeyShift) {
|
|
a += ":"
|
|
}
|
|
if a != "" {
|
|
world.ConnectPromptText += a
|
|
u.updateBuffer()
|
|
updated = true
|
|
}
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
|
|
if len(world.ConnectPromptText) != 0 {
|
|
world.ConnectPromptText = world.ConnectPromptText[:len(world.ConnectPromptText)-1]
|
|
u.updateBuffer()
|
|
updated = true
|
|
} else if world.ConnectPromptHost {
|
|
world.ConnectPromptHost = false
|
|
u.updateBuffer()
|
|
updated = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeyKPEnter) {
|
|
world.ConnectPromptConfirmed = true
|
|
}
|
|
|
|
if !updated {
|
|
u.updateTicks++
|
|
if u.updateTicks == 6 {
|
|
u.updateBuffer()
|
|
u.updateTicks = 0
|
|
}
|
|
}
|
|
|
|
return etk.Update()
|
|
}
|
|
|
|
func (u *UISystem) drawBox(screen *ebiten.Image, fillColor color.Color, r image.Rectangle) {
|
|
bounds := u.hitboxImg.Bounds()
|
|
if bounds.Dx() != r.Dx() || bounds.Dy() != r.Dy() {
|
|
u.hitboxImg = ebiten.NewImage(r.Dx(), r.Dy())
|
|
}
|
|
u.hitboxImg.Clear()
|
|
u.hitboxImg.Fill(color.RGBA{255, 255, 255, 255})
|
|
u.hitboxImg.SubImage(image.Rect(2, 2, r.Dx()-2, r.Dy()-2)).(*ebiten.Image).Fill(fillColor)
|
|
|
|
// Get screen position of top left corner of player.
|
|
drawX, drawY := world.GameCoordsToScreen(float64(r.Min.X), float64(r.Min.Y+r.Dy()))
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Translate(float64(drawX), float64(drawY))
|
|
op.ColorM.Scale(1, 1, 1, 1)
|
|
screen.DrawImage(u.hitboxImg, op)
|
|
}
|
|
|
|
func (u *UISystem) Draw(e gohan.Entity, screen *ebiten.Image) error {
|
|
if !u.initialized {
|
|
u.initialize()
|
|
}
|
|
|
|
if world.ConnectPromptVisible {
|
|
err := etk.Draw(screen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if world.Debug > 1 { // In-game and debug mode is enabled
|
|
var p, o *component.Player
|
|
for i := 0; i < 2; i++ {
|
|
if i == 0 {
|
|
p = &world.Player1
|
|
o = &world.Player2
|
|
} else {
|
|
p = &world.Player2
|
|
o = &world.Player1
|
|
}
|
|
|
|
if p.ActionTicksLeft != 0 {
|
|
// Draw a rect over stunned players.
|
|
if p.Action == component.ActionStunned {
|
|
playerRect := world.FloatRect(p.X, p.Y, p.X+float64(component.PlayerWidth), p.Y+float64(component.PlayerHeight))
|
|
|
|
fillColor := color.RGBA{123, 30, 255, 255}
|
|
u.drawBox(screen, fillColor, playerRect)
|
|
continue
|
|
}
|
|
|
|
// Draw frame data rects.
|
|
allData := component.FrameDataForActionTick(p.Action, p.ActionTicksLeft)
|
|
for _, frame := range allData {
|
|
frameRect := component.FlipRect(frame.R, component.PlayerOnRightSide(*p, *o))
|
|
frameRect = component.TranslateRect(frameRect, int(p.X), int(p.Y))
|
|
|
|
fillColor := color.RGBA{0, 255, 0, 255}
|
|
switch frame.T {
|
|
case component.HitboxHurt:
|
|
fillColor = color.RGBA{0, 0, 255, 255}
|
|
}
|
|
u.drawBox(screen, fillColor, frameRect)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if world.Local {
|
|
u.tmpImg.Clear()
|
|
|
|
aiLabel := "NONE"
|
|
switch world.AI {
|
|
case world.AIStandard:
|
|
aiLabel = "STANDARD"
|
|
case world.AIMirror:
|
|
aiLabel = "MIRROR"
|
|
case world.AIBlock:
|
|
aiLabel = "BLOCK"
|
|
}
|
|
label := fmt.Sprintf("OPPONENT TYPE: %s (CONTROL+O)", aiLabel)
|
|
ebitenutil.DebugPrint(u.tmpImg, label)
|
|
|
|
width := float64(len(label) * 6)
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Reset()
|
|
op.GeoM.Scale(1, 1)
|
|
op.GeoM.Translate(world.InternalScreenWidth/2-width/2, 0)
|
|
screen.DrawImage(u.tmpImg, op)
|
|
}
|
|
|
|
if world.Winner != 0 {
|
|
u.tmpImg.Clear()
|
|
|
|
label := "YOU LOSE!"
|
|
if world.Winner == world.CurrentPlayer {
|
|
label = "YOU WIN!"
|
|
}
|
|
ebitenutil.DebugPrint(u.tmpImg, label)
|
|
|
|
width := float64(len(label) * 24)
|
|
height := float64(64) * 2
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Scale(4, 4)
|
|
op.GeoM.Translate(world.InternalScreenWidth/2-width/2, world.InternalScreenHeight/2-height/2)
|
|
screen.DrawImage(u.tmpImg, op)
|
|
|
|
u.tmpImg.Clear()
|
|
|
|
label = "PRESS ENTER OR START TO PLAY AGAIN"
|
|
p := world.Player1
|
|
if world.CurrentPlayer == 2 {
|
|
p = world.Player2
|
|
}
|
|
if p.PlayAgain {
|
|
label = "WAITING FOR OPPONENT..."
|
|
}
|
|
ebitenutil.DebugPrint(u.tmpImg, label)
|
|
|
|
width = float64(len(label) * 12)
|
|
height = float64(32)
|
|
|
|
op.GeoM.Reset()
|
|
op.GeoM.Scale(2, 2)
|
|
op.GeoM.Translate(world.InternalScreenWidth/2-width/2, world.InternalScreenHeight/2+height/2)
|
|
screen.DrawImage(u.tmpImg, op)
|
|
}
|
|
|
|
if world.Debug != 0 {
|
|
var (
|
|
ping int64
|
|
framesBehind float64
|
|
)
|
|
if !world.ConnectPromptVisible && !world.Local {
|
|
p1Stats, err := world.Backend.GetNetworkStats(ggpo.PlayerHandle(1))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get network stats for player 1: %s", err)
|
|
}
|
|
p2Stats, err := world.Backend.GetNetworkStats(ggpo.PlayerHandle(2))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get network stats for player 2: %s", err)
|
|
}
|
|
|
|
yourStats := p1Stats
|
|
if world.CurrentPlayer == 1 {
|
|
yourStats = p2Stats
|
|
}
|
|
|
|
ping = yourStats.Network.Ping
|
|
framesBehind = math.Round(float64(yourStats.Timesync.LocalFramesBehind))
|
|
}
|
|
framesLabel := "AHEAD"
|
|
if framesBehind > 0 {
|
|
framesLabel = "BEHIND"
|
|
}
|
|
|
|
var (
|
|
tps float64
|
|
fps float64
|
|
)
|
|
if !world.Headless {
|
|
tps = ebiten.ActualTPS()
|
|
fps = ebiten.ActualFPS()
|
|
}
|
|
|
|
debugText := fmt.Sprintf("FRAMES %s %.0f\nPING %d\nTPS %0.0f\nFPS %0.0f",
|
|
framesLabel,
|
|
math.Abs(framesBehind),
|
|
ping,
|
|
tps,
|
|
fps)
|
|
ebitenutil.DebugPrintAt(screen, debugText, 2, 0)
|
|
}
|
|
return nil
|
|
}
|