Compare commits
11 Commits
Author | SHA1 | Date |
---|---|---|
Trevor Slocum | be89bd5e9c | |
Trevor Slocum | 3483dd4174 | |
Trevor Slocum | a28dfd7d94 | |
Trevor Slocum | 3b8602014d | |
Trevor Slocum | 1aa0cc9912 | |
Trevor Slocum | fe2797dd84 | |
Trevor Slocum | 8685a3e37c | |
Trevor Slocum | d41d7ad0a3 | |
Trevor Slocum | c77dd14e09 | |
Trevor Slocum | 0860c30cfd | |
Trevor Slocum | 361529de18 |
61
README.md
61
README.md
|
@ -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
|
||||
|
|
|
@ -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.
Binary file not shown.
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
1
flags.go
1
flags.go
|
@ -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)")
|
||||
|
|
210
game/game.go
210
game/game.go
|
@ -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++
|
||||
|
|
|
@ -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
14
go.mod
|
@ -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
25
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
|
102
system/ui.go
102
system/ui.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue