diff --git a/fonts/FFForward.ttf b/fonts/FFForward.ttf new file mode 100644 index 0000000..250624a Binary files /dev/null and b/fonts/FFForward.ttf differ diff --git a/fonts/agencyb.ttf b/fonts/agencyb.ttf new file mode 100644 index 0000000..8704061 Binary files /dev/null and b/fonts/agencyb.ttf differ diff --git a/fonts/arcade_n.ttf b/fonts/arcade_n.ttf new file mode 100644 index 0000000..4730e44 Binary files /dev/null and b/fonts/arcade_n.ttf differ diff --git a/fonts/fonts.go b/fonts/fonts.go new file mode 100644 index 0000000..b530647 --- /dev/null +++ b/fonts/fonts.go @@ -0,0 +1,85 @@ +package fonts + +import ( + "log" + + _ "embed" + + "github.com/hajimehoshi/ebiten/examples/resources/fonts" + "golang.org/x/image/font" + "golang.org/x/image/font/opentype" + "golang.org/x/image/font/sfnt" +) + +const ( + FontDPI = 72 + FontSizeStandard = 16 + FontSizeLarge = 24 + FontSizeBig = 60 + FontSizeArcade = 12 + FontSizeArcadeBig = 40 + FontSizeArcadeHuge = 80 +) + +type FontStruct struct { + Standard font.Face + Large font.Face + Glitch font.Face + GlitchBig font.Face + Arcade font.Face + ArcadeLarge font.Face + ArcadeHuge font.Face +} + +var ( + + //go:embed agencyb.ttf + agency_ttf []byte + + //go:embed arcade_n.ttf + arcade_ttf []byte + + SurviveFont FontStruct +) + +func LoadFontFatal(src []byte) *sfnt.Font { + tt, err := opentype.Parse(src) + if err != nil { + log.Fatal(err) + } + return tt +} + +func GetFaceFatal(fnt *sfnt.Font, dpi, size float64) font.Face { + var face font.Face + var err error + + if dpi > 0 && size > 0 && fnt != nil { + face, err = opentype.NewFace(fnt, &opentype.FaceOptions{ + Size: size, + DPI: dpi, + Hinting: font.HintingVertical, + }) + if err != nil { + log.Fatal(err) + } + } + return face +} + +func init() { + SurviveFont = FontStruct{} + + fnt := LoadFontFatal(fonts.MPlus1pRegular_ttf) + SurviveFont.Standard = GetFaceFatal(fnt, FontDPI, FontSizeStandard) + SurviveFont.Large = GetFaceFatal(fnt, FontDPI, FontSizeLarge) + + fnt2 := LoadFontFatal(agency_ttf) + SurviveFont.Glitch = GetFaceFatal(fnt2, FontDPI, FontSizeLarge) + SurviveFont.GlitchBig = GetFaceFatal(fnt2, FontDPI, FontSizeBig) + + fnt3 := LoadFontFatal(arcade_ttf) + SurviveFont.Arcade = GetFaceFatal(fnt3, FontDPI, FontSizeArcade) + SurviveFont.ArcadeLarge = GetFaceFatal(fnt3, FontDPI, FontSizeArcadeBig) + SurviveFont.ArcadeHuge = GetFaceFatal(fnt3, FontDPI, FontSizeArcadeHuge) +} diff --git a/game.go b/game.go index 872c1a8..3f70137 100644 --- a/game.go +++ b/game.go @@ -1,13 +1,16 @@ package main import ( + "fmt" "image/color" "log" "math" "math/rand/v2" + "mover/fonts" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/hajimehoshi/ebiten/v2/text" "github.com/hajimehoshi/ebiten/v2/vector" ) @@ -17,16 +20,22 @@ const ( ) type Game struct { - collisionMask *ebiten.Image - projectileMask *ebiten.Image + collisionMask *ebiten.Image + projectileMask *ebiten.Image + heroCollisionMask *ebiten.Image + heroCollisionCpy *ebiten.Image Pos Coordinates Paused bool initialized bool - mover *Mover + gameover bool + reset bool + runtime float64 + hero *Hero projectiles map[int]*Projectile explosion *Explosion + score int counter int timer int targets []*Mover @@ -41,15 +50,31 @@ func (g *Game) Initialize() { origin := Coordinates{X: 640 / 2, Y: 480 / 2} - g.mover = NewMover() - g.mover.SetOrigin(origin) - g.mover.ToggleRotate() + g.hero = NewHero() + g.hero.SetOrigin(origin) + g.hero.ToggleRotate() + + g.gameover = false g.collisionMask = ebiten.NewImage(screenWidth, screenHeight) g.projectileMask = ebiten.NewImage(screenWidth, screenHeight) + g.heroCollisionMask = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT) + g.heroCollisionCpy = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT) g.explosion = NewExplosion() g.explosion.SetOrigin(origin) + g.score = 0 + g.reset = false + + for j := 0; j < len(g.targets); j++ { + g.targets[j] = nil + } + g.targets = g.targets[:0] + + g.score = 0 + g.counter = 0 + g.timer = 0 + g.runtime = 0. } @@ -71,11 +96,12 @@ func (g *Game) Update() error { } } - if !g.initialized { + if !g.initialized || g.reset { g.Initialize() g.projectiles = make(map[int]*Projectile) g.initialized = true + g.reset = false } else { g.StepGame() } @@ -87,19 +113,29 @@ func (g *Game) Update() error { func (g *Game) Draw(screen *ebiten.Image) { - g.mover.Draw() + g.hero.Draw() op := &ebiten.DrawImageOptions{} - /* - dx := 40 * math.Cos(float64(g.counter)/16) - dy := 40 * math.Sin(float64(g.counter)/16) - a := float64(g.counter) / (math.Pi * 2) - */ + if !g.gameover { + g.runtime = float64(g.counter) / 60. + } + + s := fmt.Sprintf("%02.3f", g.runtime) + if !g.gameover { + text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) + text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) + } else { + if (g.counter/30)%2 == 0 { + text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) + text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) + } + } + op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2) - op.GeoM.Rotate(g.mover.Angle) - op.GeoM.Translate(g.mover.Pos.X, g.mover.Pos.Y) - screen.DrawImage(g.mover.Sprite, op) + op.GeoM.Rotate(g.hero.Angle) + op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y) + screen.DrawImage(g.hero.Sprite, op) for _, target := range g.targets { target.Draw() @@ -142,10 +178,13 @@ func (g *Game) CleanupTargets() { i++ } } + //then culling the last elements of the slice - /*if len(g.targets)-i > 0 { - fmt.Printf("Removing %d elements\n", len(g.targets)-i) - }*/ + if len(g.targets)-i > 0 { + // fmt.Printf("Removing %d elements\n", len(g.targets)-i) + g.score += len(g.targets) - i + } + for j := i; j < len(g.targets); j++ { g.targets[j] = nil } @@ -160,20 +199,20 @@ func (g *Game) StepGame() { g.UpdateHeroPosition() - g.mover.Update() + g.hero.Update() g.explosion.Update() g.UpdateTargets() g.UpdateProjectiles() - //append new projectiles - g.AppendProjectiles() - - //add new target with increasing frequency - g.AddNewTargets() - - //handle pulsewave updates - g.HandlePulseWaveUpdate() + if !g.gameover { + //append new projectiles + g.AppendProjectiles() + //add new target with increasing frequency + g.AddNewTargets() + //handle pulsewave updates + g.HandlePulseWaveUpdate() + } g.CleanupTargets() g.counter++ @@ -198,8 +237,8 @@ func (g *Game) HandlePulseWaveUpdate() { //check collisions for _, target := range g.targets { - dx := target.Pos.X - g.mover.Pos.X - dy := target.Pos.Y - g.mover.Pos.Y + dx := target.Pos.X - g.hero.Pos.X + dy := target.Pos.Y - g.hero.Pos.Y r := math.Sqrt(dx*dx + dy*dy) if r >= g.explosion.Radius-5 && r <= g.explosion.Radius+5 && target.Action <= MoverActionDamaged && !target.Touched { @@ -261,9 +300,9 @@ func (g *Game) UpdateProjectiles() { func (g *Game) UpdateTargets() { for _, target := range g.targets { - if !target.Hit { - dx := g.mover.Pos.X - target.Pos.X - dy := g.mover.Pos.Y - target.Pos.Y + if !target.Hit && g.hero.Action < MoverActionDying { + dx := g.hero.Pos.X - target.Pos.X + dy := g.hero.Pos.Y - target.Pos.Y angle := math.Atan2(dy, dx) maxspeed := 3. @@ -271,6 +310,30 @@ func (g *Game) UpdateTargets() { target.Pos.Y += maxspeed * math.Sin(angle) } + //compute collision with hero + if g.hero.Pos.X >= target.Pos.X-MOVER_WIDTH/2 && g.hero.Pos.X <= target.Pos.X+MOVER_WIDTH/2 && g.hero.Pos.Y >= target.Pos.Y-MOVER_HEIGHT/2 && g.hero.Pos.Y <= target.Pos.Y+MOVER_HEIGHT/2 && target.Action < MoverActionDying { + g.heroCollisionMask.Clear() + g.heroCollisionMask.DrawImage(g.hero.Sprite, nil) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Reset() + op.Blend = ebiten.BlendSourceIn + op.GeoM.Translate((g.hero.Pos.X-target.Pos.X)-MOVER_WIDTH/2, (g.hero.Pos.Y-target.Pos.Y)-MOVER_HEIGHT/2) + g.heroCollisionMask.DrawImage(target.Sprite, op) + + //var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4) + var pixels []byte = make([]byte, MOVER_HEIGHT*MOVER_HEIGHT*4) + g.heroCollisionMask.ReadPixels(pixels) + for i := 0; i < len(pixels); i = i + 4 { + if pixels[i+3] != 0 { + fmt.Println("pixel death") + g.hero.Action = MoverActionDying + g.gameover = true + break + } + } + } + target.Update() } } @@ -284,8 +347,8 @@ func (g *Game) ResetTargetTouches() { func (g *Game) AppendProjectiles() { if g.counter%14 == 0 && ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton7) { - g.projectiles[g.counter] = NewProjectile(Coordinates{X: g.mover.Pos.X, Y: g.mover.Pos.Y}, g.mover.Angle, 5.) - g.projectiles[g.counter+1] = NewProjectile(Coordinates{X: g.mover.Pos.X, Y: g.mover.Pos.Y}, g.mover.Angle+math.Pi, 5.) + g.projectiles[g.counter] = NewProjectile(Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle, 5.) + g.projectiles[g.counter+1] = NewProjectile(Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle+math.Pi, 5.) } } @@ -293,14 +356,18 @@ func (g *Game) HandleInput() { if len(g.gamepadIDs) > 0 { if ebiten.IsGamepadButtonPressed(0, ebiten.GamepadButton11) { if !g.explosion.Active { - g.explosion.SetOrigin(g.mover.Pos) + g.explosion.SetOrigin(g.hero.Pos) g.explosion.Reset() g.explosion.ToggleActivate() } } if inpututil.IsGamepadButtonJustPressed(0, ebiten.GamepadButton9) { - g.Paused = !g.Paused + if g.gameover { + g.reset = true + } else { + g.Paused = !g.Paused + } } //account for controller sensitivity @@ -315,7 +382,7 @@ func (g *Game) HandleInput() { } inputangle := math.Atan2(yaxis, xaxis) - g.mover.SetAngle(inputangle) + g.hero.SetAngle(inputangle) } } @@ -324,10 +391,10 @@ func (g *Game) UpdateHeroPosition() { inpx := ebiten.GamepadAxisValue(0, 0) inpy := ebiten.GamepadAxisValue(0, 1) if inpx >= 0.15 || inpx <= -0.15 { - g.mover.Pos.X += ebiten.GamepadAxisValue(0, 0) * 5 + g.hero.Pos.X += ebiten.GamepadAxisValue(0, 0) * 5 } if inpy >= 0.15 || inpy <= -0.15 { - g.mover.Pos.Y += ebiten.GamepadAxisValue(0, 1) * 5 + g.hero.Pos.Y += ebiten.GamepadAxisValue(0, 1) * 5 } } diff --git a/go.mod b/go.mod index 2bc8c1e..d86b8a5 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,11 @@ go 1.22.0 toolchain go1.22.8 -require github.com/hajimehoshi/ebiten/v2 v2.8.2 +require ( + github.com/hajimehoshi/ebiten v1.12.12 + github.com/hajimehoshi/ebiten/v2 v2.8.2 + golang.org/x/image v0.20.0 +) require ( github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect @@ -13,4 +17,5 @@ require ( github.com/jezek/xgb v1.1.1 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect ) diff --git a/hero.go b/hero.go new file mode 100644 index 0000000..ae4f3e0 --- /dev/null +++ b/hero.go @@ -0,0 +1,120 @@ +package main + +import ( + "bytes" + "image" + "log" + + _ "embed" + "image/color" + _ "image/png" + + "github.com/hajimehoshi/ebiten/v2" +) + +var ( + heroImage *ebiten.Image + + //go:embed hero.png + hero_img []byte +) + +const ( + HeroActionDefault = iota + HeroActionDamaged + HeroActionDying + HeroActionExploding + HeroActionDead + HeroActionMax +) + +type HeroAction uint + +func init() { + img, _, err := image.Decode(bytes.NewReader(hero_img)) + if err != nil { + log.Fatal(err) + } + heroImage = ebiten.NewImageFromImage(img) +} + +type Hero struct { + Sprite *ebiten.Image + Maks *ebiten.Image + MaksDest *ebiten.Image + Angle float64 + Pos Coordinates + Origin Coordinates + Action HeroAction + cycles int + rotating bool + Toggled bool + Hit bool + Touched bool + dyingcount int +} + +func NewHero() *Hero { + m := &Hero{ + Sprite: ebiten.NewImage(48, 48), + Maks: ebiten.NewImage(48, 48), + MaksDest: ebiten.NewImage(48, 48), + Action: HeroActionDefault, + cycles: 4, + Angle: 0, + rotating: false, + Toggled: false, + dyingcount: 0, + } + + m.Maks.Fill(color.White) + return m +} + +func (m *Hero) ToggleRotate() { + m.rotating = !m.rotating +} + +func (m *Hero) SetAngle(a float64) { + m.Angle = a +} + +func (m *Hero) SetOrigin(coords Coordinates) { + m.Origin = coords + m.Pos = coords +} + +func (m *Hero) Draw() { + m.Sprite.Clear() + m.MaksDest.Clear() + + idx := (m.cycles / 8) % 4 + + y0 := 0 + y1 := 48 + x0 := 48 * idx + x1 := x0 + 48 + + switch m.Action { + case HeroActionDefault: + m.Sprite.DrawImage(heroImage.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil) + default: + } +} + +func (m *Hero) Update() { + m.cycles++ +} + +func (m *Hero) SetHit() { + m.Action++ // = (m.Action + 1) % HeroActionMax +} + +func (m *Hero) ToggleColor() { + //m.Toggled = !m.Toggled + if m.Action == HeroActionDefault { + m.Action = HeroActionDamaged + } else if m.Action == HeroActionDamaged { + m.Action = HeroActionDefault + } +} diff --git a/hero.png b/hero.png new file mode 100644 index 0000000..a622d0d Binary files /dev/null and b/hero.png differ diff --git a/main.go b/main.go index 72e262c..eac11e0 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ const ( ) func main() { - ver := "Mover Test v0.05" + ver := "survive v0.06" fmt.Println(ver)