175 lines
3.4 KiB
Go
175 lines
3.4 KiB
Go
package lib
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
)
|
|
|
|
type State int
|
|
|
|
const (
|
|
StateTitle State = iota
|
|
StatePlay
|
|
StateOver
|
|
)
|
|
|
|
type Game struct {
|
|
assets *Assets
|
|
|
|
state State
|
|
playerLane int
|
|
score int
|
|
miss int
|
|
rabbits []*Rabbit
|
|
spawnTimer int
|
|
frame int
|
|
flash int
|
|
|
|
lcd *ebiten.Image
|
|
}
|
|
|
|
func NewGame(a *Assets) *Game {
|
|
return &Game{
|
|
assets: a,
|
|
state: StateTitle,
|
|
playerLane: 2,
|
|
lcd: ebiten.NewImage(LcdW, LcdH),
|
|
}
|
|
}
|
|
|
|
func (g *Game) reset() {
|
|
g.playerLane = 2
|
|
g.score = 0
|
|
g.miss = 0
|
|
g.rabbits = g.rabbits[:0]
|
|
g.spawnTimer = 0
|
|
g.flash = 0
|
|
}
|
|
|
|
// --- ebiten.Game ---
|
|
|
|
func (g *Game) Update() error {
|
|
g.frame++
|
|
switch g.state {
|
|
case StateTitle, StateOver:
|
|
if startPressed() {
|
|
g.reset()
|
|
g.state = StatePlay
|
|
}
|
|
case StatePlay:
|
|
g.updatePlay()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *Game) Draw(screen *ebiten.Image) {
|
|
screen.DrawImage(g.assets.Bg, nil)
|
|
|
|
switch g.state {
|
|
case StateTitle:
|
|
g.drawTitle(g.lcd)
|
|
case StatePlay:
|
|
g.drawPlay(g.lcd)
|
|
case StateOver:
|
|
g.drawOver(g.lcd)
|
|
}
|
|
|
|
opts := &ebiten.DrawImageOptions{}
|
|
opts.GeoM.Scale(LcdScale, LcdScale)
|
|
opts.GeoM.Translate(LcdDrawX, LcdDrawY)
|
|
screen.DrawImage(g.lcd, opts)
|
|
}
|
|
|
|
func (g *Game) Layout(_, _ int) (int, int) {
|
|
return ScreenW, ScreenH
|
|
}
|
|
|
|
// --- play loop ---
|
|
|
|
func (g *Game) updatePlay() {
|
|
if pressedRepeat(ebiten.KeyLeft, 12, 6) && g.playerLane > 0 {
|
|
g.playerLane--
|
|
}
|
|
if pressedRepeat(ebiten.KeyRight, 12, 6) && g.playerLane < LaneCount-1 {
|
|
g.playerLane++
|
|
}
|
|
|
|
g.spawnTimer++
|
|
interval := 70 - g.score/2
|
|
if interval < 22 {
|
|
interval = 22
|
|
}
|
|
if g.spawnTimer >= interval {
|
|
g.spawnTimer = 0
|
|
g.rabbits = append(g.rabbits, newRabbit(g.score))
|
|
}
|
|
|
|
for i := len(g.rabbits) - 1; i >= 0; i-- {
|
|
r := g.rabbits[i]
|
|
r.T += r.Speed
|
|
if r.T < 1 {
|
|
continue
|
|
}
|
|
if r.Lane == g.playerLane {
|
|
g.miss++
|
|
g.flash = 12
|
|
if g.miss >= MaxMiss {
|
|
g.state = StateOver
|
|
}
|
|
} else {
|
|
g.score++
|
|
}
|
|
g.rabbits = append(g.rabbits[:i], g.rabbits[i+1:]...)
|
|
}
|
|
|
|
if g.flash > 0 {
|
|
g.flash--
|
|
}
|
|
}
|
|
|
|
// --- scene composition ---
|
|
|
|
func (g *Game) drawPlay(dst *ebiten.Image) {
|
|
dst.Fill(ColorBg)
|
|
drawRoad(dst)
|
|
drawHud(dst, g.assets.HudFont, g.score, g.miss)
|
|
// far rabbits first so closer ones overlap on top
|
|
sort.Slice(g.rabbits, func(i, j int) bool { return g.rabbits[i].T < g.rabbits[j].T })
|
|
for _, r := range g.rabbits {
|
|
r.Draw(dst, g.assets.Rabbit)
|
|
}
|
|
drawScooter(dst, g.assets.Scooter, LaneX[g.playerLane], PlayerY, g.flash > 0, g.frame)
|
|
}
|
|
|
|
func (g *Game) drawTitle(dst *ebiten.Image) {
|
|
dst.Fill(ColorBg)
|
|
drawRoad(dst)
|
|
for i := 0; i < LaneCount; i++ {
|
|
drawScooter(dst, g.assets.Scooter, LaneX[i], PlayerY, false, g.frame)
|
|
}
|
|
for i := 1; i <= 3; i++ {
|
|
demo := &Rabbit{
|
|
Lane: (g.frame/30 + i) % LaneCount,
|
|
T: float64((g.frame+i*25)%90) / 90.0,
|
|
}
|
|
demo.Draw(dst, g.assets.Rabbit)
|
|
}
|
|
if (g.frame/30)%2 == 0 {
|
|
drawTextCentered(dst, "PRESS SPACE TO START", 120, 56, g.assets.HudFont, ColorFg)
|
|
}
|
|
drawTextCentered(dst, "ARROWS MOVE SPACE START", 120, 72, g.assets.HudFont, ColorFg)
|
|
}
|
|
|
|
func (g *Game) drawOver(dst *ebiten.Image) {
|
|
g.drawPlay(dst)
|
|
rectFill(dst, 60, 50, 120, 36, ColorBg)
|
|
rectStroke(dst, 60, 50, 120, 36, ColorFg)
|
|
drawTextCentered(dst, "GAME OVER", 120, 54, g.assets.HudFont, ColorFg)
|
|
drawTextCentered(dst, fmt.Sprintf("SCORE %03d", g.score), 120, 66, g.assets.HudFont, ColorFg)
|
|
if (g.frame/30)%2 == 0 {
|
|
drawTextCentered(dst, "SPACE = RETRY", 120, 76, g.assets.HudFont, ColorFg)
|
|
}
|
|
}
|