Compare commits

...

11 Commits
wip ... main

17 changed files with 653 additions and 159 deletions

View File

@ -2,12 +2,66 @@
[![Donate via LiberaPay](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](https://liberapay.com/rocketnine.space)
[![Donate via Patreon](https://img.shields.io/badge/dynamic/json?color=%23e85b46&label=Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F5252223)](https://www.patreon.com/rocketnine)
[Fighting](https://en.wikipedia.org/wiki/Fighting_game) video game
Multiplayer [fighting](https://en.wikipedia.org/wiki/Fighting_game) video game featuring [rollback](https://en.wikipedia.org/wiki/Netcode#Rollback) networking
This game was created for the [Fighting Jam #001](https://itch.io/jam/fight-jam-001).
This game was created for the [Fighting Jam #001](https://itch.io/jam/fight-jam-001) game jam.
## Play
### Browser
[**Play in your browser**](https://rocketnine.itch.io/box-brawl)
Networking is not available in the browser version.
### Download
[**Download for Windows and Linux**](https://rocketnine.itch.io/box-brawl#download)
To play against remote opponents, you will need to ensure UDP traffic is
allowed between both of your computers.
This usually involves port forwarding and ensuring firewalls are disabled on
both computers.
You can also use programs like [ZeroTier](https://www.zerotier.com) and [Hamachi](https://vpn.net)
to create virtual networks between you and an opponent. The network will appear
to the system as a local network. Because of this, most security restrictions
are bypassed, and port forwarding is usually not required.
Only multiplayer over a local network (whether truly local or virtual) has been
tested successfully.
#### Multiplayer guide
You and your opponent will need to choose a port on which you will accept UDP
traffic from the other person. This is referred to as your local port,
which can specified when playing Box Brawl using the `--local` argument.
In the following examples, user A has the IP address `1.1.1.1` and user B has
the IP address `2.2.2.2`. User A will listen for a connection on port `17000`.
User B will listen for a connection on port `19000`.
Simply replace the example IP addresses and ports with actual IP addresses and
ports, and run the command specified (depending on whether you are the host or
the guest).
User A (who has IP `1.1.1.1`) should run the following command:
```
boxbrawl --local 17000 --host 2.2.2.2:19000
```
User B (who has IP `2.2.2.2`) should run the following command:
```
boxbrawl --local 19000 --connect 1.1.1.1:17000
```
In the above commands, each user first specifies which port to listen for
connections from the opponent, then specifies the IP address and port where
their opponent is listening for connections.
### Compile
Install the dependencies listed for [your platform](https://github.com/hajimehoshi/ebiten/blob/main/README.md#platforms),
@ -24,10 +78,11 @@ Please share issues and suggestions [here](https://code.rocketnine.space/tslocum
## Credits
- [Trevor Slocum](https://rocketnine.space) - Game design and programming
- [node punk](https://open.spotify.com/artist/15eFpWQPNRxB89PnFNWvjU?si=z-jfVwYHTxugaC-BGZiyNg) - Music
## Dependencies
- [ebitengine](https://github.com/hajimehoshi/ebiten) - Game engine
- [ggpo-go](https://github.com/assemblaj/ggpo) - Rollback networking library
- [gohan](https://code.rocketnine.space/tslocum/gohan) - Entity Component System framework
- [etk](https://code.rocketnine.space/tslocum/etk) - Graphical User Interface toolkit

View File

@ -1,21 +1,35 @@
package asset
import (
"bytes"
"embed"
"image"
"github.com/hajimehoshi/ebiten/v2"
"io"
_ "image/png"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/audio/vorbis"
"github.com/hajimehoshi/ebiten/v2/audio/wav"
)
//go:embed image
//go:embed image sound
var FS embed.FS
const tileSize = 64
var ImgPlayer = LoadImage("image/player.png")
const sampleRate = 44100
var (
audioContext = audio.NewContext(sampleRate)
SoundHitP1 = LoadWAV(audioContext, "sound/hit.wav")
SoundHitP2 = LoadWAV(audioContext, "sound/hit.wav")
SoundMusic = LoadOGG(audioContext, "sound/an_individual_who_fights_on_roads.ogg", true)
)
func LoadImage(p string) *ebiten.Image {
f, err := FS.Open(p)
if err != nil {
@ -39,6 +53,63 @@ func LoadBytes(p string) []byte {
return b
}
func LoadWAV(context *audio.Context, p string) *audio.Player {
f, err := FS.Open(p)
if err != nil {
panic(err)
}
defer f.Close()
stream, err := wav.DecodeWithSampleRate(sampleRate, f)
if err != nil {
panic(err)
}
player, err := context.NewPlayer(stream)
if err != nil {
panic(err)
}
// Workaround to prevent delays when playing for the first time.
player.SetVolume(0)
player.Play()
player.Pause()
player.Rewind()
player.SetVolume(1)
return player
}
func LoadOGG(context *audio.Context, p string, loop bool) *audio.Player {
b := LoadBytes(p)
stream, err := vorbis.DecodeWithSampleRate(sampleRate, bytes.NewReader(b))
if err != nil {
panic(err)
}
var s io.Reader
if loop {
s = audio.NewInfiniteLoop(stream, stream.Length())
} else {
s = stream
}
player, err := context.NewPlayer(s)
if err != nil {
panic(err)
}
// Workaround to prevent delays when playing for the first time.
player.SetVolume(0)
player.Play()
player.Pause()
player.Rewind()
player.SetVolume(1)
return player
}
func FrameAt(img *ebiten.Image, x int, y int) *ebiten.Image {
xPos, yPos := x*tileSize, y*tileSize
return img.SubImage(image.Rect(xPos, yPos, xPos+tileSize, yPos+tileSize)).(*ebiten.Image)

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

BIN
asset/sound/hit.wav Normal file

Binary file not shown.

View File

@ -102,6 +102,48 @@ var AllPlayerFrames = [][][]FrameData{
},
},
}, { // ActionPunch
{
stdHit(asset.FrameAt(asset.ImgPlayer, 1, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 2, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 2, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 3, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 4, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 6, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 7, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 7, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 8, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 9, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 10, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 11, 1)),
},
{
{
T: HitboxNormal,
@ -134,15 +176,36 @@ var AllPlayerFrames = [][][]FrameData{
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 4, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 3, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 3, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 2, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 2, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 1, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 1, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 1, 1)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 1, 1)),
},
@ -151,7 +214,107 @@ var AllPlayerFrames = [][][]FrameData{
},
}, { // ActionKick
{
// TODO
stdHit(asset.FrameAt(asset.ImgPlayer, 1, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 2, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 3, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 4, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 6, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 7, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 8, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 9, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 10, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 11, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 12, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 13, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 14, 12)),
{
T: HitboxHurt,
R: image.Rect(-5, -5, PlayerSize+10, PlayerSize+10),
},
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 13, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 12, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 11, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 10, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 9, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 8, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 7, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 6, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 6, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 5, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 4, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 4, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 3, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 3, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 2, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 2, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 1, 12)),
},
{
stdHit(asset.FrameAt(asset.ImgPlayer, 0, 12)),
},
}, { // ActionBlock
{

View File

@ -99,10 +99,19 @@ type Player struct {
Color color.Color
PlayerNum int
Grounded bool
Action PlayerAction
ActionTicksLeft int
WalkFrame int
WalkFrameReverse bool
Crouching bool
CrouchFrame int
NoPunch bool
NoKick bool
PlayAgain bool
}
@ -117,8 +126,51 @@ func (p *Player) Clone() Player {
return result
}
func (p *Player) Walking() bool {
const walkThreshold = 0.2
return p.Grounded && p.Action == ActionIdle && (p.VX < -walkThreshold || p.VX > walkThreshold)
}
func TranslateRect(r image.Rectangle, x int, y int) image.Rectangle {
r.Min.X, r.Min.Y = r.Min.X+x, r.Min.Y+y
r.Max.X, r.Max.Y = r.Max.X+x, r.Max.Y+y
return r
}
// WalkFrames are defined in chronological order.
var WalkFrames = []*ebiten.Image{
asset.FrameAt(asset.ImgPlayer, 0, 13),
asset.FrameAt(asset.ImgPlayer, 1, 13),
asset.FrameAt(asset.ImgPlayer, 2, 13),
asset.FrameAt(asset.ImgPlayer, 3, 13),
asset.FrameAt(asset.ImgPlayer, 4, 13),
asset.FrameAt(asset.ImgPlayer, 5, 13),
asset.FrameAt(asset.ImgPlayer, 6, 13),
asset.FrameAt(asset.ImgPlayer, 7, 13),
}
// CrouchFrames are defined in chronological order.
var CrouchFrames = []*ebiten.Image{
asset.FrameAt(asset.ImgPlayer, 0, 14),
asset.FrameAt(asset.ImgPlayer, 1, 14),
asset.FrameAt(asset.ImgPlayer, 2, 14),
asset.FrameAt(asset.ImgPlayer, 3, 14),
asset.FrameAt(asset.ImgPlayer, 4, 14),
asset.FrameAt(asset.ImgPlayer, 5, 14),
asset.FrameAt(asset.ImgPlayer, 6, 14),
asset.FrameAt(asset.ImgPlayer, 7, 14),
asset.FrameAt(asset.ImgPlayer, 8, 14),
asset.FrameAt(asset.ImgPlayer, 9, 14),
}
// CrouchWalkFrames are defined in chronological order.
var CrouchWalkFrames = []*ebiten.Image{
asset.FrameAt(asset.ImgPlayer, 0, 15),
asset.FrameAt(asset.ImgPlayer, 1, 15),
asset.FrameAt(asset.ImgPlayer, 2, 15),
asset.FrameAt(asset.ImgPlayer, 3, 15),
asset.FrameAt(asset.ImgPlayer, 4, 15),
asset.FrameAt(asset.ImgPlayer, 5, 15),
asset.FrameAt(asset.ImgPlayer, 6, 15),
asset.FrameAt(asset.ImgPlayer, 7, 15),
}

View File

@ -23,6 +23,7 @@ func parseFlags() {
flag.StringVar(&hostAddress, "host", "", "start hosting a match against a remote opponent at the specified address:port")
flag.StringVar(&connectAddress, "connect", "", "connect to a match hosted by a remote opponent at the specified address:port")
flag.IntVar(&world.LocalPort, "local", 0, "set local port (this should be different from your opponent's local port)")
flag.BoolVar(&world.StartMuted, "mute", false, "mute music")
flag.IntVar(&world.Debug, "debug", 0, "debug level (0 - disabled, 1 - print fps and net stats, 2 - draw hitboxes)")
flag.BoolVar(&printDebug, "debug-ggpo", false, "print GGPO debug messages")
flag.IntVar(&world.TPS, "tps", world.DefaultTPS, "set ticks per second (this is not normally required)")

View File

@ -10,11 +10,11 @@ import (
"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/etk"
"code.rocketnine.space/tslocum/gohan"
"github.com/assemblaj/ggpo"
"github.com/hajimehoshi/ebiten/v2"
@ -39,10 +39,17 @@ func NewGame() (*Game, error) {
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
}
@ -68,6 +75,7 @@ func (g *Game) reset() {
ActionTicksLeft: len(component.AllPlayerFrames[component.ActionIdle]),
X: -playerSpawnGap - component.PlayerWidth,
Y: 0,
Grounded: true,
}
g.Players[1] = component.Player{
@ -77,20 +85,20 @@ func (g *Game) reset() {
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 {
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
@ -116,7 +124,9 @@ func (g *Game) startNetworkGame() {
}
p, err := strconv.Atoi(port)
if err != nil {
log.Fatalf("failed to read port: %s", err)
log.Printf("failed to read port in network address: %s", err)
world.ConnectPromptConfirmed = false
return
}
localPort := p
@ -186,19 +196,19 @@ func (g *Game) InitNetworking(localPort int, numPlayers int, players []ggpo.Play
}
}
func (g *Game) ReadInputs() InputBits {
func (g *Game) ReadInputsP1() InputBits {
var in InputBits
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW) {
if ebiten.IsKeyPressed(ebiten.KeyW) {
in.setButtonOn(ButtonUp)
}
if ebiten.IsKeyPressed(ebiten.KeyArrowDown) || ebiten.IsKeyPressed(ebiten.KeyS) {
if ebiten.IsKeyPressed(ebiten.KeyS) {
in.setButtonOn(ButtonDown)
}
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA) {
if ebiten.IsKeyPressed(ebiten.KeyA) {
in.setButtonOn(ButtonLeft)
}
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD) {
if ebiten.IsKeyPressed(ebiten.KeyD) {
in.setButtonOn(ButtonRight)
}
@ -225,6 +235,45 @@ func (g *Game) ReadInputs() InputBits {
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
@ -266,7 +315,6 @@ func (g *Game) applyPhysics() {
}
outOfBounds := p.Y < -world.GroundHeight-component.PlayerHeight
if outOfBounds {
p.VX, p.VY = 0, 0
} else if collideG == -1 && p.VY > -world.Gravity {
@ -288,6 +336,26 @@ func (g *Game) applyPhysics() {
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]
@ -328,7 +396,12 @@ func (g *Game) applyPhysics() {
}
func (g *Game) UpdateByInputs(inputs []InputBits) {
const punchStunTicks = 15
const (
punchHitStrength = 4.0
punchStunTicks = 15
kickHitStrength = 7.0
KickStunTicks = 19
)
var player, opponent *component.Player
var playerFlipped, oppFlipped bool
@ -349,12 +422,20 @@ func (g *Game) UpdateByInputs(inputs []InputBits) {
}
_ = oppFlipped // TODO
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
// 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
@ -381,41 +462,42 @@ func (g *Game) UpdateByInputs(inputs []InputBits) {
}
}
if input.isButtonOn(ButtonPunch) {
if !g.Players[i].NoPunch {
g.Players[i].Action = component.ActionPunch
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionPunch]) // TODO
g.Players[i].NoPunch = true
continue
}
} else if g.Players[i].NoPunch {
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(ButtonUp) && !component.TranslateRect(playerRect, 0, -1).Overlaps(oppRect) {
var grounded bool
for _, physRect := range world.PhysicsRects {
if g.Players[i].Y < float64(physRect.Max.Y)+world.FloatValueThreshold && g.Players[i].Y > float64(physRect.Max.Y)-world.FloatValueThreshold {
grounded = true
break
}
}
if grounded {
g.Players[i].VY = world.JumpVelocity
}
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(ButtonDown) && !component.TranslateRect(playerRect, 0, 1).Overlaps(oppRect) {
//g.Players[i].VY = -1
// TODO crouch
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(ButtonLeft) && !component.TranslateRect(playerRect, -1, 0).Overlaps(oppRect) {
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) && !component.TranslateRect(playerRect, 1, 0).Overlaps(oppRect) {
if input.isButtonOn(ButtonRight) {
g.Players[i].VX = 1
}
} else {
g.Players[i].CrouchFrame = 0
}
// TODO player starts in idle action?
@ -438,9 +520,16 @@ func (g *Game) UpdateByInputs(inputs []InputBits) {
if frame.T == component.HitboxHurt {
// Hit opponent.
if oppRect.Overlaps(component.TranslateRect(frameRect, int(player.X), int(player.Y))) {
hitStrength := 4.0
hitStrength := punchHitStrength
stunTicks := punchStunTicks
if player.Action == component.ActionKick {
hitStrength = kickHitStrength
stunTicks = KickStunTicks
}
// Apply blocking.
if g.Players[opp].Action == component.ActionBlock {
hitStrength = 1.0
hitStrength /= 4.0
}
// Send the opponent flying in some direction.
@ -453,7 +542,16 @@ func (g *Game) UpdateByInputs(inputs []InputBits) {
// Stun the opponent.
if g.Players[opp].Action != component.ActionBlock {
opponent.Action = component.ActionStunned
opponent.ActionTicksLeft = punchStunTicks
opponent.ActionTicksLeft = stunTicks
// Play hit sound.
sound := asset.SoundHitP1
if i == 1 {
sound = asset.SoundHitP2
}
sound.SetVolume(0.2)
sound.Rewind()
sound.Play()
}
}
}
@ -471,7 +569,7 @@ func (g *Game) UpdateByInputs(inputs []InputBits) {
}
if g.Players[i].ActionTicksLeft == 0 {
// Hold block.
if input.isButtonOn(ButtonBlock) {
if g.Players[i].Action == component.ActionBlock && input.isButtonOn(ButtonBlock) {
g.Players[i].ActionTicksLeft = len(component.AllPlayerFrames[component.ActionBlock])
continue
}
@ -494,13 +592,6 @@ func (g *Game) UpdateByInputs(inputs []InputBits) {
g.applyPhysics()
}
func (g *Game) ReadInputsP2() InputBits {
var in InputBits
// TODO Support local multiplayer?
return in
}
func (g *Game) playerStateUpdated() {
if g.Winner != 0 && g.Players[0].PlayAgain && g.Players[1].PlayAgain {
g.reset()
@ -513,7 +604,7 @@ func (g *Game) playerStateUpdated() {
func (g *Game) RunLocalFrame() {
inputs := make([]InputBits, 2)
inputs[0] = g.ReadInputs()
inputs[0] = g.ReadInputsP1()
if world.AI == world.AIStandard {
inputs[1] = botInput()
@ -522,6 +613,8 @@ func (g *Game) RunLocalFrame() {
} else if world.AI == world.AIBlock {
inputs[1] = inputs[0]
inputs[1].setButtonOn(ButtonBlock)
} else { // AINone
inputs[1] = g.ReadInputsP2()
}
g.UpdateByInputs(inputs)
@ -529,7 +622,7 @@ func (g *Game) RunLocalFrame() {
}
func (g *Game) RunFrame() {
input := g.ReadInputs()
input := g.ReadInputsP1()
buffer := encodeInputs(input)
//fmt.Println("Attempting to add local inputs")
@ -595,16 +688,25 @@ func (g *Game) Update() error {
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
case world.AIBlock:
world.AI = world.AINone
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++

View File

@ -114,6 +114,7 @@ var (
botTicks int
botWait bool
botLastPunch int
botLastKick int
botBlockTicks int
botTaunt component.PlayerAction
botTauntTicks int
@ -128,17 +129,28 @@ func botInput() InputBits {
o := &world.Player1
const (
botMaxActionTime = 20
botWaitChance = 3
botMaxActionTime = 15
botWaitChance = 7
botPunchDistance = 25
botBlockDistance = 25
botBlockTime = 17
botBlockChance = 4
botKickDistance = 25
botKickChance = 4
botBlockDistance = 35
botBlockTime = 20
botBlockChance = 2
botTauntMinTime = 60
botTauntTime = 200
botTauntMaxTime = 550
)
defer func() {
if !input.isButtonOn(ButtonPunch) {
botLastPunch++
}
if !input.isButtonOn(ButtonKick) {
botLastKick++
}
}()
if botTicks == 0 {
botTicks = rand.Intn(botMaxActionTime)
botWait = rand.Intn(botWaitChance) == 0 && world.Winner == 0
@ -146,6 +158,7 @@ func botInput() InputBits {
botTicks--
}
if !botWait {
// Handle taunting.
if (botTauntTicks > 0 || world.Winner == 2) && botTauntTotalTicks < botTauntMaxTime {
if botTauntTicks == 0 {
if p.Action == component.ActionIdle {
@ -189,29 +202,30 @@ func botInput() InputBits {
}
if world.Winner == 0 {
if p.X < o.X {
if p.X < o.X && p.X < world.GroundWidth/2-component.PlayerWidth {
input.setButtonOn(ButtonRight)
} else {
} else if p.X >= o.X && p.X > -world.GroundWidth/2 {
input.setButtonOn(ButtonLeft)
}
}
if botBlockTicks > 0 || (math.Abs(p.X-o.X) < botBlockDistance && o.Action == component.ActionPunch && rand.Intn(botBlockChance) != 0) {
input.setButtonOn(ButtonBlock)
if botBlockTicks > 0 {
botBlockTicks--
} else {
botBlockTicks = botBlockTime
opponentAttacking := o.Action == component.ActionPunch || o.Action == component.ActionKick
if botBlockTicks > 0 || (math.Abs(p.X-o.X) < botBlockDistance && opponentAttacking && rand.Intn(botBlockChance) == 0) {
input.setButtonOn(ButtonBlock)
if botBlockTicks > 0 {
botBlockTicks--
} else {
botBlockTicks = botBlockTime
}
} else if p.Action == component.ActionIdle && math.Abs(p.X-o.X) < botKickDistance && rand.Intn(botKickChance) == 0 && botLastKick > 1 {
input.setButtonOn(ButtonKick)
botLastKick = 0
} else if p.Action == component.ActionIdle && math.Abs(p.X-o.X) < botPunchDistance && botLastPunch > 1 {
input.setButtonOn(ButtonPunch)
botLastPunch = 0
}
} else if math.Abs(p.X-o.X) < botPunchDistance && botLastPunch > 1 {
input.setButtonOn(ButtonPunch)
botLastPunch = 0
}
}
if !input.isButtonOn(ButtonPunch) {
botLastPunch++
}
return input
}

14
go.mod
View File

@ -3,22 +3,22 @@ module code.rocketnine.space/tslocum/boxbrawl
go 1.19
require (
code.rocketnine.space/tslocum/etk v0.0.0-20230103193701-368514415e01
code.rocketnine.space/tslocum/gohan v1.0.0
github.com/assemblaj/ggpo v0.0.0-20230106194913-0f5ca8313c51
github.com/hajimehoshi/ebiten/v2 v2.4.15
github.com/assemblaj/ggpo v0.0.0-20230129160025-68dbb12db83e
github.com/hajimehoshi/ebiten/v2 v2.4.16
)
require (
code.rocketnine.space/tslocum/messeji v1.0.2 // indirect
github.com/ebitengine/purego v0.1.1 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
github.com/hajimehoshi/file2byteslice v1.0.0 // indirect
github.com/hajimehoshi/oto/v2 v2.3.1 // indirect
github.com/jezek/xgb v1.1.0 // indirect
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a // indirect
golang.org/x/exp/shiny v0.0.0-20230108222341-4b8118a2686a // indirect
github.com/jfreymuth/oggvorbis v1.0.4 // indirect
github.com/jfreymuth/vorbis v1.0.2 // indirect
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 // indirect
golang.org/x/exp/shiny v0.0.0-20230131160201-f062dba9d201 // indirect
golang.org/x/image v0.3.0 // indirect
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
)

25
go.sum
View File

@ -1,33 +1,31 @@
code.rocketnine.space/tslocum/etk v0.0.0-20230103193701-368514415e01 h1:uIe8pMDrqth/zk7e/O6kGw1wH2NNhTp6Hwad3xueLx0=
code.rocketnine.space/tslocum/etk v0.0.0-20230103193701-368514415e01/go.mod h1:aZXxIwgveNf6Yu96khlkpXdlRKwDoee8H3bYs9y8RnU=
code.rocketnine.space/tslocum/gohan v1.0.0 h1:WBcJq7nVfmr1EB8bew6xWlB5Q1714yWJ3a9/q6aBBrY=
code.rocketnine.space/tslocum/gohan v1.0.0/go.mod h1:12yOt5Ygl/RVwnnZSVZRuS1W6gCaHJgezcvg8+THk10=
code.rocketnine.space/tslocum/messeji v1.0.2 h1:3/68FnXWaBDMhfUGb8FvNpVgAHY8DX+VL7pyA/CcY94=
code.rocketnine.space/tslocum/messeji v1.0.2/go.mod h1:bSXsyjvKhFXQ7GsUxWZdO2JX83xOT/VTqFCR04thk+c=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/assemblaj/ggpo v0.0.0-20230106194913-0f5ca8313c51 h1:7utlNj3OWPUxtFgav/0Eu1EXOrsNS5lrq+NWnwTRnAs=
github.com/assemblaj/ggpo v0.0.0-20230106194913-0f5ca8313c51/go.mod h1:ZKiAYEZgxDlGHGeP/VZsv1+xIRo9kQpgUFmjP/PR0lQ=
github.com/assemblaj/ggpo v0.0.0-20230129160025-68dbb12db83e h1:tfcqziT05uc2ow93d8csUPVmt7EKnO3cGwMb2+mGOJ4=
github.com/assemblaj/ggpo v0.0.0-20230129160025-68dbb12db83e/go.mod h1:ZKiAYEZgxDlGHGeP/VZsv1+xIRo9kQpgUFmjP/PR0lQ=
github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
github.com/ebitengine/purego v0.1.1 h1:HI8nW+LniW9Yb34k34jBs8nz+PNzsw68o7JF8jWFHHE=
github.com/ebitengine/purego v0.1.1/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/hajimehoshi/bitmapfont/v2 v2.2.2 h1:4z08Fk1m3pjtlO7BdoP48u5bp/Y8xmKshf44aCXgYpE=
github.com/hajimehoshi/bitmapfont/v2 v2.2.2/go.mod h1:Ua/x9Dkz7M9CU4zr1VHWOqGwjKdXbOTRsH7lWfb1Co0=
github.com/hajimehoshi/ebiten/v2 v2.4.15 h1:yvhCrDv9y7TpdHtdux5ES/IwP6Pfplz5rJVxE0Z+ZPU=
github.com/hajimehoshi/ebiten/v2 v2.4.15/go.mod h1:BZcqCU4XHmScUi+lsKexocWcf4offMFwfp8dVGIB/G4=
github.com/hajimehoshi/ebiten/v2 v2.4.16 h1:vhuMtaB78N2HlNMfImV/SZkDPNJhOxgFrEIm1uh838o=
github.com/hajimehoshi/ebiten/v2 v2.4.16/go.mod h1:BZcqCU4XHmScUi+lsKexocWcf4offMFwfp8dVGIB/G4=
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
github.com/hajimehoshi/file2byteslice v1.0.0 h1:ljd5KTennqyJ4vG9i/5jS8MD1prof97vlH5JOdtw3WU=
github.com/hajimehoshi/file2byteslice v1.0.0/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto/v2 v2.3.1 h1:qrLKpNus2UfD674oxckKjNJmesp9hMh7u7QCrStB3Rc=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
github.com/jakecoffman/cp v1.2.1/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
github.com/jezek/xgb v1.0.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jfreymuth/oggvorbis v1.0.4 h1:cyJCd0XSoxkKzUPmqM0ZoQJ0h/WbhfyvUR+FTMxQEac=
github.com/jfreymuth/oggvorbis v1.0.4/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
@ -38,10 +36,10 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a h1:tlXy25amD5A7gOfbXdqCGN5k8ESEed/Ee1E5RcrYnqU=
golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp/shiny v0.0.0-20230108222341-4b8118a2686a h1:mTWP1/jZnkR5zmffmGg9HfL6w81dS/u6ZJSOa4i8ot8=
golang.org/x/exp/shiny v0.0.0-20230108222341-4b8118a2686a/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201 h1:BEABXpNXLEz0WxtA+6CQIz2xkg80e+1zrhWyMcq8VzE=
golang.org/x/exp v0.0.0-20230131160201-f062dba9d201/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp/shiny v0.0.0-20230131160201-f062dba9d201 h1:Y5QA5ZjU4BIgfpTmSB+07A4sIMFk6nLx0Q0/mJlIduE=
golang.org/x/exp/shiny v0.0.0-20230131160201-f062dba9d201/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
@ -86,7 +84,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=

View File

@ -38,20 +38,18 @@ func (s *MapSystem) Draw(e gohan.Entity, screen *ebiten.Image) error {
screen.Fill(color.RGBA{0, 100, 100, 255})
groundColor := color.RGBA{50, 50, 50, 255}
const groundTopHeight = 10
//groundTopColor := color.RGBA{100, 100, 100, 255}
const groundBorderSize = 3
groundBorderColor := color.RGBA{30, 30, 30, 255}
groundColor := color.RGBA{20, 20, 20, 255}
for _, r := range world.PhysicsRects {
screen.SubImage(world.GameRectToScreen(r)).(*ebiten.Image).Fill(groundColor)
groundRect := world.GameRectToScreen(r)
screen.SubImage(groundRect).(*ebiten.Image).Fill(groundBorderColor)
inset := groundRect.Inset(groundBorderSize)
inset.Max.Y += groundBorderSize
screen.SubImage(inset).(*ebiten.Image).Fill(groundColor)
}
/*r := image.Rect(-world.GroundWidth/2, -world.ScreenHeight, world.GroundWidth/2, 0)
screen.SubImage(world.GameRectToScreen(r)).(*ebiten.Image).Fill(groundColor)
r = image.Rect(-world.GroundWidth/2, -groundTopHeight, world.GroundWidth/2, 0)
screen.SubImage(world.GameRectToScreen(r)).(*ebiten.Image).Fill(groundTopColor)*/
return nil
}

View File

@ -58,6 +58,21 @@ func (s *PlayerSystem) Draw(e gohan.Entity, screen *ebiten.Image) error {
break
}
drawCrouch := p.Crouching || (p.Action == component.ActionIdle && p.CrouchFrame != 0)
if p.Walking() {
if drawCrouch {
if p.CrouchFrame == 9 {
sprite = component.CrouchWalkFrames[p.WalkFrame]
} else {
sprite = component.CrouchFrames[p.CrouchFrame]
}
} else {
sprite = component.WalkFrames[p.WalkFrame]
}
} else if drawCrouch {
sprite = component.CrouchFrames[p.CrouchFrame]
}
if sprite != nil {
x, y := world.GameCoordsToScreen(p.X-24, p.Y+64)
op := &ebiten.DrawImageOptions{}

View File

@ -5,13 +5,12 @@ import (
"image"
"image/color"
"math"
"github.com/assemblaj/ggpo"
"strings"
"code.rocketnine.space/tslocum/boxbrawl/component"
"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/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
@ -19,23 +18,31 @@ import (
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 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 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.
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...`
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 *etk.Text
buffer string
updateTicks int
tmpImg *ebiten.Image
@ -44,21 +51,9 @@ type UISystem struct {
}
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.tmpImg = ebiten.NewImage(world.InternalScreenWidth/2, world.InternalScreenHeight/2)
u.hitboxImg = ebiten.NewImage(32, 32)
u.updateBuffer()
u.initialized = true
}
@ -69,6 +64,7 @@ func (u *UISystem) updateBuffer() {
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))...)
@ -91,10 +87,10 @@ func (u *UISystem) updateBuffer() {
prompt = append(prompt, []byte("\n\n"+uiHostPrompt)...)
prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...)
}
prompt = append(prompt, []byte("\n\n"+controlsInfo)...)
}
u.buffer.Clear()
u.buffer.Write(prompt)
u.buffer = string(prompt)
}
func (u *UISystem) Update(e gohan.Entity) error {
@ -181,8 +177,7 @@ func (u *UISystem) Update(e gohan.Entity) error {
u.updateTicks = 0
}
}
return etk.Update()
return nil
}
func (u *UISystem) drawBox(screen *ebiten.Image, fillColor color.Color, r image.Rectangle) {
@ -209,10 +204,7 @@ func (u *UISystem) Draw(e gohan.Entity, screen *ebiten.Image) error {
}
if world.ConnectPromptVisible {
err := etk.Draw(screen)
if err != nil {
return err
}
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++ {
@ -254,23 +246,23 @@ func (u *UISystem) Draw(e gohan.Entity, screen *ebiten.Image) error {
if world.Local {
u.tmpImg.Clear()
aiLabel := "NONE"
aiLabel := "LOCAL PLAYER"
switch world.AI {
case world.AIStandard:
aiLabel = "STANDARD"
aiLabel = "STANDARD AI"
case world.AIMirror:
aiLabel = "MIRROR"
aiLabel = "MIRROR AI"
case world.AIBlock:
aiLabel = "BLOCK"
aiLabel = "BLOCK AI"
}
label := fmt.Sprintf("OPPONENT TYPE: %s (CONTROL+O)", aiLabel)
ebitenutil.DebugPrint(u.tmpImg, label)
width := float64(len(label) * 6)
width := float64(len(label) * 12)
op := &ebiten.DrawImageOptions{}
op.GeoM.Reset()
op.GeoM.Scale(1, 1)
op.GeoM.Scale(2, 2)
op.GeoM.Translate(world.InternalScreenWidth/2-width/2, 0)
screen.DrawImage(u.tmpImg, op)
}
@ -360,3 +352,35 @@ func (u *UISystem) Draw(e gohan.Entity, screen *ebiten.Image) error {
}
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)
}

View File

@ -19,21 +19,19 @@ const (
JumpVelocity = 20
MaxDebug = 2
FloatValueThreshold = 1 // This is required because image.Rectangle uses integers
GroundWidth = 600
GroundHeight = 100
MaxDebug = 2
)
type AIType int
const (
AIStandard AIType = iota
AINone
AIMirror
AIBlock
AINone
)
var (
@ -47,6 +45,8 @@ var (
CamX, CamY = 0, 0 // TODO currently static
StartMuted bool // Start with music muted.
LocalPort int
ConnectPromptVisible = true // When false, we are connected
@ -65,8 +65,10 @@ var (
AI AIType // AI configuration
Backend ggpo.Backend
)
// These variables are cached to prevent race conditions.
// These variables are cached to prevent race conditions.
var (
Player1 component.Player
Player2 component.Player
Winner int