You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
387 lines
10 KiB
387 lines
10 KiB
package system
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
"strings"
|
|
|
|
"code.rocketnine.space/tslocum/boxbrawl/component"
|
|
"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/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 or a local player.`
|
|
const uiHostPrompt = `Press <H> to host a network
|
|
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...`
|
|
const controlsInfo = `<H> Punch <J> Kick <K> Block <L>+<Dir> Taunt`
|
|
|
|
type UISystem struct {
|
|
*component.Once
|
|
|
|
initialized bool
|
|
buffer string
|
|
updateTicks int
|
|
|
|
tmpImg *ebiten.Image
|
|
|
|
hitboxImg *ebiten.Image
|
|
}
|
|
|
|
func (u *UISystem) initialize() {
|
|
u.tmpImg = ebiten.NewImage(world.InternalScreenWidth/2, world.InternalScreenHeight/2)
|
|
u.hitboxImg = ebiten.NewImage(32, 32)
|
|
u.updateBuffer()
|
|
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)...)
|
|
prompt = append(prompt, []byte("\n\n"+controlsInfo)...)
|
|
} 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)...)
|
|
}
|
|
prompt = append(prompt, []byte("\n\n"+controlsInfo)...)
|
|
}
|
|
|
|
u.buffer = string(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 nil
|
|
}
|
|
|
|
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 {
|
|
u.drawCenteredText(screen, u.buffer)
|
|
} 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 := "LOCAL PLAYER"
|
|
switch world.AI {
|
|
case world.AIStandard:
|
|
aiLabel = "STANDARD AI"
|
|
case world.AIMirror:
|
|
aiLabel = "MIRROR AI"
|
|
case world.AIBlock:
|
|
aiLabel = "BLOCK AI"
|
|
}
|
|
label := fmt.Sprintf("OPPONENT TYPE: %s (CONTROL+O)", aiLabel)
|
|
ebitenutil.DebugPrint(u.tmpImg, label)
|
|
|
|
width := float64(len(label) * 12)
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Reset()
|
|
op.GeoM.Scale(2, 2)
|
|
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
|
|
}
|
|
|
|
func (u *UISystem) drawCenteredText(screen *ebiten.Image, text string) {
|
|
const (
|
|
charWidth = 6
|
|
charHeight = 16
|
|
textScale = 2
|
|
)
|
|
|
|
lines := strings.Split(text, "\n")
|
|
var w int
|
|
for _, line := range lines {
|
|
l := len(line)
|
|
if l > w {
|
|
w = l
|
|
}
|
|
}
|
|
|
|
u.tmpImg.Clear()
|
|
for i, line := range lines {
|
|
x, y := ((w-len(line))/2)*charWidth, i*charHeight
|
|
ebitenutil.DebugPrintAt(u.tmpImg, line, x, y)
|
|
}
|
|
|
|
width := float64(w) * charWidth * textScale
|
|
height := float64(len(lines) * charHeight * textScale)
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Reset()
|
|
op.GeoM.Scale(textScale, textScale)
|
|
op.GeoM.Translate(world.InternalScreenWidth/2-width/2, world.InternalScreenHeight/2-height/2)
|
|
screen.DrawImage(u.tmpImg, op)
|
|
}
|