Files
rabbitroller/lib/game.go
T
2026-05-28 19:27:23 +02:00

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)
}
}