diff --git a/.gitignore b/.gitignore index aef148e..a9c4922 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea *.sh *.wasm +boxcars dist diff --git a/game/assets/checker_black.png b/game/assets/checker_black.png index 63c7bdf..66d7506 100644 Binary files a/game/assets/checker_black.png and b/game/assets/checker_black.png differ diff --git a/game/board.go b/game/board.go index 9d24199..eff2d20 100644 --- a/game/board.go +++ b/game/board.go @@ -51,6 +51,8 @@ type board struct { moveQueue chan *stateUpdate + drawFrame chan bool + debug int // Print and draw debug information } @@ -69,6 +71,7 @@ func NewBoard() *board { spaceRects: make([][4]int, 26), V: make([]int, 42), moveQueue: make(chan *stateUpdate, 10), + drawFrame: make(chan bool, 10), } for i := range b.Sprites.sprites { @@ -88,6 +91,8 @@ func NewBoard() *board { b.spaces[space] = append(b.spaces[space], s) } + go b.handleDraw() + go b.handlePieceMoves() b.op = &ebiten.DrawImageOptions{} @@ -97,6 +102,31 @@ func NewBoard() *board { return b } +func (b *board) handleDraw() { + drawFreq := time.Second / 144 // TODO + lastDraw := time.Now() + for v := range b.drawFrame { + if !v { + return + } + since := time.Since(lastDraw) + if since < drawFreq { + t := time.NewTimer(drawFreq - since) + DELAYDRAW: + for { + select { + case <-b.drawFrame: + continue DELAYDRAW + case <-t.C: + break DELAYDRAW + } + } + } + ebiten.ScheduleFrame() + lastDraw = time.Now() + } +} + func (b *board) newSprite(white bool) *Sprite { s := &Sprite{} s.colorWhite = white @@ -106,6 +136,18 @@ func (b *board) newSprite(white bool) *Sprite { func (b *board) updateBackgroundImage() { tableColor := color.RGBA{0, 102, 51, 255} + frameColor := color.RGBA{65, 40, 14, 255} + borderColor := color.RGBA{0, 0, 0, 255} + faceColor := color.RGBA{120, 63, 25, 255} + triangleA := color.RGBA{225.0, 188, 125, 255} + triangleB := color.RGBA{120.0, 17.0, 0, 255} + + borderSize := b.horizontalBorderSize + if borderSize > b.barWidth/2 { + borderSize = b.barWidth / 2 + } + frameW := b.w - ((b.horizontalBorderSize - borderSize) * 2) + innerW := b.w - (b.horizontalBorderSize * 2) // Outer board width (including frame) // Table box := image.NewRGBA(image.Rect(0, 0, b.w, b.h)) @@ -113,39 +155,33 @@ func (b *board) updateBackgroundImage() { img.Fill(tableColor) b.backgroundImage = ebiten.NewImageFromImage(img) - // Border - borderColor := color.RGBA{65, 40, 14, 255} - borderSize := b.horizontalBorderSize - if borderSize > b.barWidth/2 { - borderSize = b.barWidth / 2 - } - box = image.NewRGBA(image.Rect(0, 0, b.w-((b.horizontalBorderSize-borderSize)*2), b.h)) + // Frame + box = image.NewRGBA(image.Rect(0, 0, frameW, b.h)) img = ebiten.NewImageFromImage(box) - img.Fill(borderColor) + img.Fill(frameColor) b.op.GeoM.Reset() b.op.GeoM.Translate(float64(b.horizontalBorderSize-borderSize), 0) b.backgroundImage.DrawImage(img, b.op) // Face - box = image.NewRGBA(image.Rect(0, 0, b.w-(b.horizontalBorderSize*2), b.h-(b.verticalBorderSize*2))) + box = image.NewRGBA(image.Rect(0, 0, innerW, b.h-(b.verticalBorderSize*2))) img = ebiten.NewImageFromImage(box) - img.Fill(color.RGBA{120, 63, 25, 255}) + img.Fill(faceColor) b.op.GeoM.Reset() b.op.GeoM.Translate(float64(b.horizontalBorderSize), float64(b.verticalBorderSize)) b.backgroundImage.DrawImage(img, b.op) - baseImg := image.NewRGBA(image.Rect(0, 0, b.w-(b.horizontalBorderSize*2), b.h-(b.verticalBorderSize*2))) - gc := draw2dimg.NewGraphicContext(baseImg) - // Bar box = image.NewRGBA(image.Rect(0, 0, b.barWidth, b.h)) img = ebiten.NewImageFromImage(box) - img.Fill(borderColor) + img.Fill(frameColor) b.op.GeoM.Reset() b.op.GeoM.Translate(float64((b.w/2)-(b.barWidth/2)), 0) b.backgroundImage.DrawImage(img, b.op) // Draw triangles + baseImg := image.NewRGBA(image.Rect(0, 0, b.w-(b.horizontalBorderSize*2), b.h-(b.verticalBorderSize*2))) + gc := draw2dimg.NewGraphicContext(baseImg) for i := 0; i < 2; i++ { triangleTip := float64((b.h - (b.verticalBorderSize * 2)) / 2) if i == 0 { @@ -160,9 +196,9 @@ func (b *board) updateBackgroundImage() { } if colorA { - gc.SetFillColor(color.RGBA{219.0, 185, 113, 255}) + gc.SetFillColor(triangleA) } else { - gc.SetFillColor(color.RGBA{120.0, 17.0, 0, 255}) + gc.SetFillColor(triangleB) } tx := b.spaceWidth * j @@ -177,12 +213,56 @@ func (b *board) updateBackgroundImage() { gc.Fill() } } - img = ebiten.NewImageFromImage(baseImg) - b.op.GeoM.Reset() b.op.GeoM.Translate(float64(b.horizontalBorderSize), float64(b.verticalBorderSize)) b.backgroundImage.DrawImage(img, b.op) + + // Border + borderImage := image.NewRGBA(image.Rect(0, 0, b.w, b.h)) + gc = draw2dimg.NewGraphicContext(borderImage) + gc.SetStrokeColor(borderColor) + // - Outside left + gc.SetLineWidth(2) + gc.MoveTo(float64(1), float64(0)) + gc.LineTo(float64(1), float64(b.h)) + // - Center + gc.SetLineWidth(2) + gc.MoveTo(float64(frameW/2), float64(0)) + gc.LineTo(float64(frameW/2), float64(b.h)) + // - Outside right + gc.MoveTo(float64(frameW), float64(0)) + gc.LineTo(float64(frameW), float64(b.h)) + gc.Close() + gc.Stroke() + // - Inside left + gc.SetLineWidth(1) + edge := float64((((innerW) - b.barWidth) / 2) + borderSize) + gc.MoveTo(float64(borderSize), float64(b.verticalBorderSize)) + gc.LineTo(edge, float64(b.verticalBorderSize)) + gc.LineTo(edge, float64(b.h-b.verticalBorderSize)) + gc.LineTo(float64(borderSize), float64(b.h-b.verticalBorderSize)) + gc.LineTo(float64(borderSize), float64(b.verticalBorderSize)) + gc.Close() + gc.Stroke() + // - Inside right + edgeStart := float64((innerW / 2) + (b.barWidth / 2) + borderSize) + edgeEnd := float64(innerW + borderSize) + gc.MoveTo(float64(edgeStart), float64(b.verticalBorderSize)) + gc.LineTo(edgeEnd, float64(b.verticalBorderSize)) + gc.LineTo(edgeEnd, float64(b.h-b.verticalBorderSize)) + gc.LineTo(float64(edgeStart), float64(b.h-b.verticalBorderSize)) + gc.LineTo(float64(edgeStart), float64(b.verticalBorderSize)) + gc.Close() + gc.Stroke() + img = ebiten.NewImageFromImage(borderImage) + b.op.GeoM.Reset() + b.op.GeoM.Translate(float64(b.horizontalBorderSize-borderSize), 0) + b.backgroundImage.DrawImage(img, b.op) +} + +func (b *board) ScheduleFrame() { + b.drawFrame <- true } func (b *board) draw(screen *ebiten.Image) { @@ -280,7 +360,7 @@ func (b *board) setRect(x, y, w, h int) { b.horizontalBorderSize = 0 - b.triangleOffset = float64(b.h-(b.verticalBorderSize*2)) / 33 + b.triangleOffset = float64(b.h-(b.verticalBorderSize*2)) / 15 for { b.verticalBorderSize = 7 // TODO configurable @@ -304,7 +384,6 @@ func (b *board) setRect(x, y, w, h int) { if extraSpace >= largeBarWidth { b.barWidth = largeBarWidth } - // TODO barwidth in calcs is wrong b.horizontalBorderSize = ((b.w - (b.spaceWidth * 12)) - b.barWidth) / 2 if b.horizontalBorderSize < 0 { @@ -312,6 +391,7 @@ func (b *board) setRect(x, y, w, h int) { } loadAssets(b.spaceWidth) + for i := 0; i < b.Sprites.num; i++ { s := b.Sprites.sprites[i] s.w, s.h = imgCheckerWhite.Size() @@ -524,37 +604,77 @@ func (b *board) ProcessState() { func (b *board) _movePiece(sprite *Sprite, from int, to int, speed int) { moveSize := 1 - moveDelay := time.Duration(2/speed) * time.Millisecond + moveDelay := time.Duration(1/speed) * time.Millisecond - stackTo := len(b.spaces[to]) - if stackTo == 1 && sprite.colorWhite != b.spaces[to][0].colorWhite { - stackTo = 0 // Hit - } - x, y, _, _ := b.stackSpaceRect(to, stackTo) - x, y = b.offsetPosition(x, y) + space := from + for { + if space == to { + break + } else if to > space { + space++ + } else { + space-- + } - if sprite.x != x { - // Center - cy := (b.h / 2) - (b.spaceWidth / 2) - for { - if sprite.y == cy { - break - } - if sprite.y < cy { - sprite.y += moveSize - if sprite.y > cy { - sprite.y = cy + // Go to bar or home immediately + if from == 0 || from == 25 || to == 0 || to == 25 { + space = to + } + + stack := len(b.spaces[space]) + if stack == 1 && sprite.colorWhite != b.spaces[space][0].colorWhite { + stack = 0 // Hit + } + + x, y, _, _ := b.stackSpaceRect(space, stack) + x, y = b.offsetPosition(x, y) + + cy := y + if cy > sprite.y == b.bottomRow(space) { + cy = sprite.y + } + + if sprite.x != x { + // Center + for { + if sprite.y == cy { + break } - } else if sprite.y > cy { - sprite.y -= moveSize if sprite.y < cy { - sprite.y = cy + sprite.y += moveSize + if sprite.y > cy { + sprite.y = cy + } + } else if sprite.y > cy { + sprite.y -= moveSize + if sprite.y < cy { + sprite.y = cy + } } + b.ScheduleFrame() + time.Sleep(moveDelay) + } + for { + if sprite.x == x { + break + } + if sprite.x < x { + sprite.x += moveSize + if sprite.x > x { + sprite.x = x + } + } else if sprite.x > x { + sprite.x -= moveSize + if sprite.x < x { + sprite.x = x + } + } + b.ScheduleFrame() + time.Sleep(moveDelay / 2) } - time.Sleep(moveDelay) } for { - if sprite.x == x { + if sprite.x == x && sprite.y == y { break } if sprite.x < x { @@ -568,36 +688,20 @@ func (b *board) _movePiece(sprite *Sprite, from int, to int, speed int) { sprite.x = x } } - time.Sleep(moveDelay / 2) - } - } - for { - if sprite.x == x && sprite.y == y { - break - } - if sprite.x < x { - sprite.x += moveSize - if sprite.x > x { - sprite.x = x - } - } else if sprite.x > x { - sprite.x -= moveSize - if sprite.x < x { - sprite.x = x - } - } - if sprite.y < y { - sprite.y += moveSize - if sprite.y > y { - sprite.y = y - } - } else if sprite.y > y { - sprite.y -= moveSize if sprite.y < y { - sprite.y = y + sprite.y += moveSize + if sprite.y > y { + sprite.y = y + } + } else if sprite.y > y { + sprite.y -= moveSize + if sprite.y < y { + sprite.y = y + } } + b.ScheduleFrame() + time.Sleep(moveDelay) } - time.Sleep(moveDelay) } // TODO do not add bear off pieces @@ -609,6 +713,10 @@ func (b *board) _movePiece(sprite *Sprite, from int, to int, speed int) { } } b.moving = nil + + b.ScheduleFrame() + + time.Sleep(time.Second) } func (b *board) handlePieceMoves() { @@ -638,8 +746,11 @@ func (b *board) handlePieceMoves() { b._movePiece(sprite, from, to, 1) if moveAfter != nil { toBar := 0 - if b.V[fibs.StateDirection] == 1 { - toBar = 25 + if b.V[fibs.StateTurn] == b.V[fibs.StatePlayerColor] { + toBar = 25 // TODO how is this determined? + } + if b.V[fibs.StateDirection] == -1 { + toBar = 25 - toBar } b._movePiece(moveAfter, to, toBar, 2) } @@ -658,9 +769,8 @@ func (b *board) update() { // TODO allow grabbing multiple pieces by grabbing further down the stack if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { - x, y := ebiten.CursorPosition() - if b.dragging == nil { + x, y := ebiten.CursorPosition() s := b.spriteAt(x, y) if s != nil { b.dragging = s diff --git a/game/game.go b/game/game.go index 192b45e..d89efd9 100644 --- a/game/game.go +++ b/game/game.go @@ -8,10 +8,12 @@ import ( "image/color" _ "image/png" "log" + "os" "strings" "time" "code.rocketnine.space/tslocum/fibs" + "code.rocketnine.space/tslocum/kibodo" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" @@ -162,6 +164,9 @@ type Game struct { inputBuffer string Debug int + + keyboard *kibodo.Keyboard + shownKeyboard bool } func NewGame() *Game { @@ -176,8 +181,12 @@ func NewGame() *Game { Board: NewBoard(), runeBuffer: make([]rune, 24), - } + keyboard: kibodo.NewKeyboard(), + } + g.keyboard.SetKeys(kibodo.KeysQWERTY) + + // TODO go func() { t := time.NewTicker(time.Second / 4) for range t.C { @@ -270,10 +279,30 @@ func (g *Game) Update() error { // Called by ebiten only when input occurs return err } + if ebiten.IsWindowBeingClosed() { + g.Exit() + return nil + } + + err = g.keyboard.Update() + if err != nil { + return fmt.Errorf("failed to update virtual keyboard: %s", err) + } + if !g.loggedIn { f := func() { var clearBuffer bool defer func() { + if strings.ContainsRune(g.inputBuffer, '\n') { + g.inputBuffer = strings.Split(g.inputBuffer, "\n")[0] + clearBuffer = true + } + if !g.usernameConfirmed { + g.Username = g.inputBuffer + } else { + g.Password = g.inputBuffer + } + if clearBuffer { g.inputBuffer = "" @@ -285,6 +314,23 @@ func (g *Game) Update() error { // Called by ebiten only when input occurs } }() + if !g.shownKeyboard { + ch := make(chan *kibodo.Input, 10) + go func() { + for input := range ch { + if input.Rune > 0 { + g.inputBuffer += string(input.Rune) + continue + } + if input.Key == ebiten.KeyEnter { + g.inputBuffer += "\n" + } + } + }() + g.keyboard.Show(ch) + g.shownKeyboard = true + } + if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) && len(g.inputBuffer) > 0 { g.inputBuffer = g.inputBuffer[:len(g.inputBuffer)-1] } @@ -296,17 +342,6 @@ func (g *Game) Update() error { // Called by ebiten only when input occurs g.runeBuffer = ebiten.AppendInputChars(g.runeBuffer[:0]) if len(g.runeBuffer) > 0 { g.inputBuffer += string(g.runeBuffer) - - if strings.ContainsRune(g.inputBuffer, '\n') { - g.inputBuffer = strings.Split(g.inputBuffer, "\n")[0] - clearBuffer = true - } - if !g.usernameConfirmed { - g.Username = g.inputBuffer - } else { - g.Password = g.inputBuffer - } - log.Println("INPUT BUFFER IS:" + g.inputBuffer) } } @@ -350,6 +385,8 @@ func (g *Game) Draw(screen *ebiten.Image) { // Log in screen if !g.loggedIn { + g.keyboard.Draw(screen) + const welcomeText = `Please enter your FIBS username and password. If you do not have a FIBS account yet, visit http://www.fibs.com/help.html#register` @@ -372,19 +409,18 @@ http://www.fibs.com/help.html#register` g.Board.draw(screen) - if g.Debug == 1 { + if g.Debug > 0 { debugBox := image.NewRGBA(image.Rect(10, 20, 200, 200)) debugImg := ebiten.NewImageFromImage(debugBox) g.drawBuffer.Reset() - g.drawBuffer.Write([]byte(fmt.Sprintf("FPS %0.0f\nTPS %0.0f", ebiten.CurrentFPS(), ebiten.CurrentTPS()))) + g.drawBuffer.Write([]byte(fmt.Sprintf("FPS %0.0f %c\nTPS %0.0f", ebiten.CurrentFPS(), spinner[g.spinnerIndex], ebiten.CurrentTPS()))) - /* TODO enable when vsync is able to be turned off g.spinnerIndex++ if g.spinnerIndex == 4 { g.spinnerIndex = 0 - }*/ + } scaleFactor := ebiten.DeviceScaleFactor() if scaleFactor != 1.0 { @@ -415,9 +451,17 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { g.screenW, g.screenH = outsideWidth, outsideHeight g.Board.setRect(0, 0, g.screenW, g.screenH) + + displayArea := 200 + g.keyboard.SetRect(0, displayArea, g.screenW, g.screenH-displayArea) return outsideWidth, outsideHeight } func (g *Game) resetImageOptions() { g.op.GeoM.Reset() } + +func (g *Game) Exit() { + g.Board.drawFrame <- false + os.Exit(0) +} diff --git a/go.mod b/go.mod index 03d050b..6168c40 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.17 require ( code.rocketnine.space/tslocum/fibs v0.0.0-00010101000000-000000000000 - github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.12.0.20210825183521-91a72880271d + code.rocketnine.space/tslocum/kibodo v0.0.0-20210830194839-05789279ce56 + github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.12.0.20210828073710-0e5dca9453a5 github.com/llgcode/draw2d v0.0.0-20210313082411-577c1ead272a github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d diff --git a/go.sum b/go.sum index 193b1ea..0780a39 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +code.rocketnine.space/tslocum/kibodo v0.0.0-20210830194839-05789279ce56 h1:+KVT4Zw9CEQkxdNP81wuTvX3EHRPpPUQFwn6UVLoMFY= +code.rocketnine.space/tslocum/kibodo v0.0.0-20210830194839-05789279ce56/go.mod h1:nWGK8LvmYgMZQcwGMYOOuZ19VsVYL5E1hREEJ2gV46M= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/go-gl/gl v0.0.0-20180407155706-68e253793080/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= github.com/go-gl/glfw v0.0.0-20180426074136-46a8d530c326 h1:QqWaXlVeUGwSH7hO8giZP2Y06Qjl1LWR+FWC22YQsU8= @@ -8,8 +10,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/hajimehoshi/bitmapfont/v2 v2.1.3 h1:JefUkL0M4nrdVwVq7MMZxSTh6mSxOylm+C4Anoucbb0= github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs= -github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.12.0.20210825183521-91a72880271d h1:EMEmUvZhS8hZ4eq1732CWs7aKcSWXhVvcxllfhVZc9Y= -github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.12.0.20210825183521-91a72880271d/go.mod h1:B4Cje+Kb1ZjztrKFPaiMWOGXO3Vp8u+zIBdxkZqkyD4= +github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.12.0.20210828073710-0e5dca9453a5 h1:fUSKz2wvklV02UTmBXXDlNKc6molRGUu5O8b80AvEa4= +github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.12.0.20210828073710-0e5dca9453a5/go.mod h1:B4Cje+Kb1ZjztrKFPaiMWOGXO3Vp8u+zIBdxkZqkyD4= github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= diff --git a/main.go b/main.go index 5c24258..c10ea2a 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,8 @@ import ( "flag" "log" "os" + "os/signal" + "syscall" "time" "code.rocketnine.space/tslocum/boxcars/game" @@ -20,14 +22,10 @@ func main() { ebiten.SetWindowTitle("Boxcars") ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowResizable(true) + ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMinimum) ebiten.SetMaxTPS(60) // TODO allow users to set custom value ebiten.SetRunnableOnUnfocused(true) // Note - this currently does nothing in ebiten - - // TODO set up system to call scheduleframe automatically - //ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMinimum) - // TODO breaks justpressedkey - - //ebiten.SetWindowClosingHandled(true) TODO implement + ebiten.SetWindowClosingHandled(true) fullscreenWidth, fullscreenHeight := ebiten.ScreenSizeInFullscreen() if fullscreenWidth <= screenWidth || fullscreenHeight <= screenHeight { @@ -68,6 +66,16 @@ func main() { } }() + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, + syscall.SIGINT, + syscall.SIGTERM) + go func() { + <-sigc + + g.Exit() + }() + if err := ebiten.RunGame(g); err != nil { log.Fatal(err) }