boxbrawl/system/ui.go

328 lines
8.6 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 uiComputerPrompt = `Press <Enter> to play against the computer.`
const uiHostPrompt = `Press <H> to host a match against a remote player.`
const uiHostInfoPrompt = `Type a port to host a match against a remote player.`
const uiHostStartPrompt = `Press <Enter> to start hosting.`
const uiRemotePrompt = `Type an IP address and port (address:port) to play against a remote player.`
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 a connection on %s...`
const uiClientConnectingPrompt = `Connecting to %s...`
type UISystem struct {
*component.Once
initialized bool
buffer *etk.Text
updateTicks int
gameOverImg *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.gameOverImg = 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"+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.PlayerSize), p.Y+float64(component.PlayerSize))
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.Winner != 0 {
u.gameOverImg.Clear()
label := "YOU LOSE!"
if world.Winner == world.CurrentPlayer {
label = "YOU WIN!"
}
ebitenutil.DebugPrint(u.gameOverImg, 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.gameOverImg, op)
u.gameOverImg.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.gameOverImg, 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.gameOverImg, op)
}
if world.Debug != 0 {
var ping int64
var framesBehind float64
if !world.ConnectPromptVisible {
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"
}
debugText := fmt.Sprintf("FRAMES %s %.0f\nPING %d\nTPS %0.0f\nFPS %0.0f",
framesLabel,
math.Abs(framesBehind),
ping,
ebiten.ActualTPS(),
ebiten.ActualFPS())
ebitenutil.DebugPrintAt(screen, debugText, 2, 0)
}
return nil
}