From cbc4ba5eb3a28e17b1c86d5595b8f8e5285e0b43 Mon Sep 17 00:00:00 2001 From: iegod Date: Fri, 15 Nov 2024 16:11:45 -0500 Subject: [PATCH] Refactor to use better interfaces and event callbacks. --- assets/imagebank.go | 8 +- assets/worm.png | Bin 0 -> 7366 bytes assets/wormdefault.png | Bin 0 -> 8999 bytes elements/boss.go | 26 ++- elements/enemies.go | 23 +++ elements/flyeye.go | 153 ++++++++++++++ elements/mover.go | 3 +- gamedata/enemystates.go | 12 ++ gamedata/gameevents.go | 11 + gamedata/gameinputs.go | 12 ++ gameelement/background.go | 83 ++++++++ gameelement/canvas.go | 403 +++++++++++++++++++++++++++++++++++++ gameelement/gameelement.go | 15 ++ main.go | 3 +- screens/game.go | 44 +++- screens/primary.go | 227 +++++++++++++++++++++ 16 files changed, 1009 insertions(+), 14 deletions(-) create mode 100644 assets/worm.png create mode 100644 assets/wormdefault.png create mode 100644 elements/enemies.go create mode 100644 elements/flyeye.go create mode 100644 gamedata/enemystates.go create mode 100644 gamedata/gameevents.go create mode 100644 gamedata/gameinputs.go create mode 100644 gameelement/background.go create mode 100644 gameelement/canvas.go create mode 100644 gameelement/gameelement.go create mode 100644 screens/primary.go diff --git a/assets/imagebank.go b/assets/imagebank.go index 8ecbc06..6c7291c 100644 --- a/assets/imagebank.go +++ b/assets/imagebank.go @@ -23,7 +23,8 @@ const ( TileSet ImgAssetName = "TileSet" Altar ImgAssetName = "Altar" Weapon ImgAssetName = "Weapon" - Worm ImgAssetName = "Worm" + WormDamaged ImgAssetName = "WormDamaged" + Worm ImgAssetName = "WormDefault" ) var ( @@ -51,6 +52,8 @@ var ( weapon_img []byte //go:embed worm.png worm_img []byte + //go:embed wormdefault.png + wormdefault_img []byte ) func LoadImages() { @@ -66,7 +69,8 @@ func LoadImages() { ImageBank[TileSet] = LoadImagesFatal(tileset_img) ImageBank[Altar] = LoadImagesFatal(altar_img) ImageBank[Weapon] = LoadImagesFatal(weapon_img) - ImageBank[Worm] = LoadImagesFatal(worm_img) + ImageBank[WormDamaged] = LoadImagesFatal(worm_img) + ImageBank[Worm] = LoadImagesFatal(wormdefault_img) } diff --git a/assets/worm.png b/assets/worm.png new file mode 100644 index 0000000000000000000000000000000000000000..536dd25fa7261d468aa2308f6a89c20b56f9ba7c GIT binary patch literal 7366 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 JmMS0u{|~L6{oeop literal 0 HcmV?d00001 diff --git a/assets/wormdefault.png b/assets/wormdefault.png new file mode 100644 index 0000000000000000000000000000000000000000..315057248f8c7ff196ae2c1072b2821773abf9f7 GIT binary patch literal 8999 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 literal 0 HcmV?d00001 diff --git a/elements/boss.go b/elements/boss.go index fa4efe9..761fbeb 100644 --- a/elements/boss.go +++ b/elements/boss.go @@ -16,6 +16,8 @@ type Boss struct { Spawned bool Pos gamedata.Coordinates Right bool + Health int + Touched bool damage bool cycle int Action MoverAction @@ -32,6 +34,8 @@ func NewBoss() *Boss { hitcount: 0, Maks: ebiten.NewImage(96, 96), MaskDest: ebiten.NewImage(96, 96), + Health: 100, + Touched: false, } b.Maks.Fill(color.White) return b @@ -45,6 +49,9 @@ func (b *Boss) Update() { b.damageduration = 0 } } + if b.Action == MoverActionDead { + b.Spawned = false + } b.cycle++ } @@ -71,15 +78,17 @@ func (b *Boss) Draw() { switch b.Action { case MoverActionDefault: + b.Sprite.DrawImage(assets.ImageBank[assets.Worm].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) + case MoverActionDamaged: if (b.cycle/5)%2 == 0 && b.damage { - b.MaskDest.DrawImage(assets.ImageBank[assets.Worm].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) + b.MaskDest.DrawImage(assets.ImageBank[assets.WormDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) op := &ebiten.DrawImageOptions{} op.GeoM.Reset() op.Blend = ebiten.BlendSourceAtop b.MaskDest.DrawImage(b.Maks, op) b.Sprite.DrawImage(b.MaskDest, nil) } else { - b.Sprite.DrawImage(assets.ImageBank[assets.Worm].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) + b.Sprite.DrawImage(assets.ImageBank[assets.WormDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) } case MoverActionExploding: op.GeoM.Scale(2, 2) @@ -95,7 +104,9 @@ func (b *Boss) Draw() { func (b *Boss) SetHit() { b.hitcount++ b.damage = true - if b.hitcount > 10 { + b.Health-- + + if b.Health <= 0 { b.Action = MoverActionExploding b.cycle = 0 } @@ -107,4 +118,13 @@ func (b *Boss) Reset() { b.damageduration = 0 b.Action = MoverActionDefault b.Spawned = false + b.Health = 100 +} + +func (b *Boss) ToggleColor() { + if b.Action == MoverActionDefault { + b.Action = MoverActionDamaged + } else if b.Action == MoverActionDamaged { + b.Action = MoverActionDefault + } } diff --git a/elements/enemies.go b/elements/enemies.go new file mode 100644 index 0000000..ef287ce --- /dev/null +++ b/elements/enemies.go @@ -0,0 +1,23 @@ +package elements + +import ( + "mover/gamedata" + + "github.com/hajimehoshi/ebiten/v2" +) + +type Enemies interface { + Update() error + Draw() + GetPosition() gamedata.Coordinates + SetPosition(gamedata.Coordinates) + SetTarget(gamedata.Coordinates) + GetSprite() *ebiten.Image + GetEnemyState() gamedata.EnemyState + SetHit() + SetToggle() + IsToggled() bool + SetTouched() + ClearTouched() + IsTouched() bool +} diff --git a/elements/flyeye.go b/elements/flyeye.go new file mode 100644 index 0000000..87920d7 --- /dev/null +++ b/elements/flyeye.go @@ -0,0 +1,153 @@ +package elements + +import ( + "image" + "image/color" + "math" + "mover/assets" + "mover/gamedata" + + "github.com/hajimehoshi/ebiten/v2" +) + +type FlyEye struct { + Sprite *ebiten.Image + Maks *ebiten.Image + MaksDest *ebiten.Image + position gamedata.Coordinates + target gamedata.Coordinates + state gamedata.EnemyState + cycle int + dyingcount int + hit bool + touched bool + toggle bool +} + +func NewFlyEye() *FlyEye { + f := &FlyEye{ + Sprite: ebiten.NewImage(46, 46), + Maks: ebiten.NewImage(48, 48), + MaksDest: ebiten.NewImage(48, 48), + cycle: 0, + dyingcount: 0, + hit: false, + touched: false, + toggle: false, + } + f.Maks.Fill(color.White) + return f +} + +func (f *FlyEye) Update() error { + + //close loop on target + if f.state <= gamedata.EnemyStateHit { + dx := f.target.X - f.position.X + dy := f.target.Y - f.position.Y + if math.Abs(dx) > 3 || math.Abs(dy) > 3 { + angle := math.Atan2(dy, dx) + + f.position.X += math.Cos(angle) * 3 + f.position.Y += math.Sin(angle) * 3 + } + } + + if f.state == gamedata.EnemyStateDying { + f.dyingcount++ + } + + f.cycle++ + + return nil +} + +func (f *FlyEye) Draw() { + f.Sprite.Clear() + f.MaksDest.Clear() + + idx := (f.cycle / 8) % 4 + + y0 := 0 + y1 := 48 + x0 := 48 * idx + x1 := x0 + 48 + + switch f.state { + case gamedata.EnemyStateDefault: + if !f.toggle { + f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeNormal].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil) + } else { + f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil) + } + case gamedata.EnemyStateDying: + //after some condition, set to exploding + if (f.cycle/5)%2 == 0 { + + f.MaksDest.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil) + op := &ebiten.DrawImageOptions{} + op.GeoM.Reset() + op.Blend = ebiten.BlendSourceAtop + f.MaksDest.DrawImage(f.Maks, op) + f.Sprite.DrawImage(f.MaksDest, nil) + + } else { + f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDamaged].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil) + } + if f.dyingcount >= 31 { + f.cycle = 0 + f.state = gamedata.EnemyStateExploding + } + case gamedata.EnemyStateExploding: + f.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDying].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil) + if idx == 3 { + f.state = gamedata.EnemyStateDead + } + } +} + +func (f *FlyEye) GetPosition() gamedata.Coordinates { + return f.position +} + +func (f *FlyEye) SetTarget(p gamedata.Coordinates) { + f.target = p +} + +func (f *FlyEye) GetSprite() *ebiten.Image { + return f.Sprite +} + +func (f *FlyEye) SetHit() { + f.hit = true + f.state = gamedata.EnemyStateDying + f.cycle = 0 +} + +func (f *FlyEye) IsTouched() bool { + return f.touched +} + +func (f *FlyEye) SetTouched() { + f.touched = true +} + +func (f *FlyEye) ClearTouched() { + f.touched = false +} + +func (f *FlyEye) SetToggle() { + f.toggle = !f.toggle +} + +func (f *FlyEye) IsToggled() bool { + return f.toggle +} + +func (f *FlyEye) GetEnemyState() gamedata.EnemyState { + return f.state +} + +func (f *FlyEye) SetPosition(p gamedata.Coordinates) { + f.position = p +} diff --git a/elements/mover.go b/elements/mover.go index fe03c9e..ed4859b 100644 --- a/elements/mover.go +++ b/elements/mover.go @@ -114,7 +114,7 @@ func (m *Mover) Draw() { } } -func (m *Mover) Update() { +func (m *Mover) Update() error { /* dx := 0. //40 * math.Cos(float64(m.cycles)/16) dy := 0. //40 * math.Sin(float64(m.cycles)/16) @@ -130,6 +130,7 @@ func (m *Mover) Update() { m.dyingcount++ } m.cycles++ + return nil } func (m *Mover) SetHit() { diff --git a/gamedata/enemystates.go b/gamedata/enemystates.go new file mode 100644 index 0000000..afb2478 --- /dev/null +++ b/gamedata/enemystates.go @@ -0,0 +1,12 @@ +package gamedata + +type EnemyState int + +const ( + EnemyStateDefault = iota + EnemyStateHit + EnemyStateDying + EnemyStateExploding + EnemyStateDead + EnemyStateMax +) diff --git a/gamedata/gameevents.go b/gamedata/gameevents.go new file mode 100644 index 0000000..2b0e0b0 --- /dev/null +++ b/gamedata/gameevents.go @@ -0,0 +1,11 @@ +package gamedata + +type GameEvent int + +const ( + GameEventPlayerDeath = iota + GameEventCharge + GameEventNewShot + GameEventTargetHit + GameEventExplosion +) diff --git a/gamedata/gameinputs.go b/gamedata/gameinputs.go new file mode 100644 index 0000000..ac83dde --- /dev/null +++ b/gamedata/gameinputs.go @@ -0,0 +1,12 @@ +package gamedata + +type GameInputs struct { + XAxis float64 + YAxis float64 + ShotAngle float64 + Shot bool + Start bool + Charge bool + Quit bool + Reset bool +} diff --git a/gameelement/background.go b/gameelement/background.go new file mode 100644 index 0000000..b2a99e8 --- /dev/null +++ b/gameelement/background.go @@ -0,0 +1,83 @@ +package gameelement + +import ( + "image" + "math/rand/v2" + "mover/assets" + "mover/gamedata" + + "github.com/hajimehoshi/ebiten/v2" +) + +type Background struct { + Sprite *ebiten.Image + initialized bool +} + +func NewBackground(a gamedata.Area) *Background { + b := &Background{ + Sprite: ebiten.NewImage(a.Width, a.Height), + initialized: false, + } + return b +} + +func (b *Background) SetInputs(gamedata.GameInputs) { + +} + +func (b *Background) Update() error { + + if !b.initialized { + b.Initialize() + } else { + + } + + return nil +} + +func (b *Background) Draw(drawimg *ebiten.Image) { + //all the stuff before + op := &ebiten.DrawImageOptions{} + drawimg.DrawImage(b.Sprite, op) +} + +func (b *Background) Initialize() { + b.ConstructBackground() + b.initialized = true +} + +func (b *Background) ConstructBackground() { + BLOCK_SIZE := 32 + + for i := 0; i < b.Sprite.Bounds().Dx()/BLOCK_SIZE; i++ { + for j := 0; j < b.Sprite.Bounds().Dy()/BLOCK_SIZE; j++ { + + //select random tile in x and y from tileset + idx_y := rand.IntN(256 / BLOCK_SIZE) + idx_x := rand.IntN(256 / BLOCK_SIZE) + + x0 := BLOCK_SIZE * idx_x + y0 := BLOCK_SIZE * idx_y + x1 := x0 + BLOCK_SIZE + y1 := y0 + BLOCK_SIZE + + //translate for grid element we're painting + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(i*BLOCK_SIZE), float64(j*BLOCK_SIZE)) + b.Sprite.DrawImage(assets.ImageBank[assets.TileSet].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) + } + } + + ax := float64(rand.IntN(b.Sprite.Bounds().Dx()/BLOCK_SIZE) * BLOCK_SIZE) + ay := float64(rand.IntN(b.Sprite.Bounds().Dy()/BLOCK_SIZE) * BLOCK_SIZE) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(ax, ay) + b.Sprite.DrawImage(assets.ImageBank[assets.Altar], op) +} + +func (b *Background) RegisterEvents(e gamedata.GameEvent, f func()) { + +} diff --git a/gameelement/canvas.go b/gameelement/canvas.go new file mode 100644 index 0000000..a797854 --- /dev/null +++ b/gameelement/canvas.go @@ -0,0 +1,403 @@ +package gameelement + +import ( + "fmt" + "image/color" + "math" + "math/rand/v2" + "mover/assets" + "mover/elements" + "mover/fonts" + "mover/gamedata" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text" + "github.com/hajimehoshi/ebiten/v2/vector" +) + +type Canvas struct { + Sprite *ebiten.Image + collisionMask *ebiten.Image + projectileMask *ebiten.Image + heroCollisionMask *ebiten.Image + heroCollisionCpy *ebiten.Image + + eventmap map[gamedata.GameEvent]func() + + initialized bool + lastInputs gamedata.GameInputs + runtime float64 + counter int + score int + hero *elements.Hero + charge *elements.Explosion + enemies []elements.Enemies + projectiles []*elements.Projectile + gameover bool +} + +func NewCanvas(a gamedata.Area) *Canvas { + c := &Canvas{ + Sprite: ebiten.NewImage(a.Width, a.Height), + projectileMask: ebiten.NewImage(a.Width, a.Height), + collisionMask: ebiten.NewImage(a.Width, a.Height), + heroCollisionMask: ebiten.NewImage(46, 46), + heroCollisionCpy: ebiten.NewImage(46, 46), + hero: elements.NewHero(), + charge: elements.NewExplosion(), + initialized: false, + gameover: false, + score: 0, + runtime: 0., + } + c.eventmap = make(map[gamedata.GameEvent]func()) + return c +} + +func (c *Canvas) SetInputs(gi gamedata.GameInputs) { + c.lastInputs = gi +} + +func (c *Canvas) Update() error { + if !c.initialized { + c.Initialize() + } else { + //update positions() + //hero first + c.UpdateHero() + c.UpdateProjectiles() + c.UpdateCharge() + c.UpdateEnemies() + c.CleanupTargets() + + } + c.counter++ + return nil +} + +func (c *Canvas) Draw(drawimg *ebiten.Image) { + c.Sprite.Clear() + c.projectileMask.Clear() + + //vector.DrawFilledCircle(c.Sprite, float32(c.hero.Pos.X), float32(c.hero.Pos.Y), 100, color.White, true) + + c.hero.Draw() + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(c.hero.Pos.X-48/2, c.hero.Pos.Y-48/2) + c.Sprite.DrawImage(c.hero.Sprite, op) + + if !c.gameover { + op.GeoM.Reset() + op.GeoM.Translate(0, -16) + op.GeoM.Rotate(c.lastInputs.ShotAngle) + op.GeoM.Translate(c.hero.Pos.X, c.hero.Pos.Y) + c.Sprite.DrawImage(assets.ImageBank[assets.Weapon], op) + } + + for _, e := range c.enemies { + e.Draw() + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(e.GetPosition().X-46/2, e.GetPosition().Y-46/2) + c.Sprite.DrawImage(e.GetSprite(), op) + } + + for _, p := range c.projectiles { + //drawimg.DrawImage() + vector.DrawFilledCircle(c.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true) + } + + c.Sprite.DrawImage(c.projectileMask, nil) + + vector.StrokeCircle(c.Sprite, float32(c.charge.Origin.X), float32(c.charge.Origin.Y), float32(c.charge.Radius), 3, color.White, true) + + if !c.gameover { + c.runtime = float64(c.counter) / 60. + } + s := fmt.Sprintf("%02.3f", c.runtime) + if !c.gameover { + text.Draw(c.Sprite, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) + text.Draw(c.Sprite, fmt.Sprintf("SCORE: %d", c.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) + } else { + if (c.counter/30)%2 == 0 { + text.Draw(c.Sprite, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) + text.Draw(c.Sprite, fmt.Sprintf("SCORE: %d", c.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) + } + + text.Draw(c.Sprite, "PRESS START TO TRY AGAIN", fonts.SurviveFont.Arcade, 640/2-150, 480/2, color.White) + } + + //op := &ebiten.DrawImageOptions{} + op.GeoM.Reset() + drawimg.DrawImage(c.Sprite, op) +} + +func (c *Canvas) Initialize() { + + c.InitializeHero() + c.enemies = c.enemies[:0] + c.gameover = false + c.initialized = true + c.score = 0 + c.counter = 0 + c.runtime = 0. + + //temporary + c.hero.Action = elements.HeroActionDefault +} + +func (c *Canvas) UpdateHero() { + c.hero.Update() + if !c.gameover { + c.UpdateHeroPosition() + c.ComputeHeroCollisions() + c.AddProjectiles() + } +} + +func (c *Canvas) UpdateHeroPosition() { + if c.lastInputs.XAxis >= 0.15 || c.lastInputs.XAxis <= -0.15 { + c.hero.Left = c.lastInputs.XAxis < 0 + c.hero.Pos.X += c.lastInputs.XAxis * 5 + } + + if c.lastInputs.YAxis >= 0.15 || c.lastInputs.YAxis <= -0.15 { + c.hero.Pos.Y += c.lastInputs.YAxis * 5 + } +} + +func (c *Canvas) ComputeHeroCollisions() { + for _, e := range c.enemies { + //compute collision with hero + if c.hero.Pos.X >= e.GetPosition().X-46/2 && c.hero.Pos.X <= e.GetPosition().X+46/2 && + c.hero.Pos.Y >= e.GetPosition().Y-46/2 && c.hero.Pos.Y <= e.GetPosition().Y+46/2 && + e.GetEnemyState() < gamedata.EnemyStateDying { + + // target.Action < elements.MoverActionDying && g.hero.Action < elements.HeroActionDying { + + c.heroCollisionMask.Clear() + c.heroCollisionMask.DrawImage(c.hero.Sprite, nil) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Reset() + op.Blend = ebiten.BlendSourceIn + op.GeoM.Translate((c.hero.Pos.X-e.GetPosition().X)-float64(e.GetSprite().Bounds().Dx())/2, (c.hero.Pos.Y-e.GetPosition().Y)-float64(e.GetSprite().Bounds().Dy())/2) + c.heroCollisionMask.DrawImage(e.GetSprite(), op) + + if c.HasCollided(c.heroCollisionMask, 46*46*4) { + c.hero.SetHit() + c.gameover = true + + if c.eventmap[gamedata.GameEventPlayerDeath] != nil { + c.eventmap[gamedata.GameEventPlayerDeath]() + } + } + } + } +} + +func (c *Canvas) AddProjectiles() { + + //add new projectiles + if c.lastInputs.Shot && c.counter%14 == 0 { + loc := gamedata.Coordinates{ + X: c.hero.Pos.X, + Y: c.hero.Pos.Y, + } + angle := c.lastInputs.ShotAngle + velocity := 5. + c.projectiles = append(c.projectiles, elements.NewProjectile(loc, angle, velocity)) + + if c.hero.Upgrade { + c.projectiles = append(c.projectiles, elements.NewProjectile(loc, angle+math.Pi, velocity)) + } + + if c.eventmap[gamedata.GameEventNewShot] != nil { + c.eventmap[gamedata.GameEventNewShot]() + } + } + +} + +func (c *Canvas) InitializeHero() { + //recenter the hero + pos := gamedata.Coordinates{ + X: float64(c.Sprite.Bounds().Dx() / 2), + Y: float64(c.Sprite.Bounds().Dy() / 2), + } + c.hero.SetOrigin(pos) +} + +func (c *Canvas) UpdateProjectiles() { + i := 0 + for _, p := range c.projectiles { + p.Update() + + projectilevalid := true + + if p.Pos.X < -640/2 || p.Pos.X > 1.5*640 || p.Pos.Y < -480/2 || p.Pos.Y > 1.5*480 { + projectilevalid = false + } + + for _, e := range c.enemies { + if p.Pos.X >= e.GetPosition().X-48/2 && p.Pos.X <= e.GetPosition().X+48/2 && + p.Pos.Y >= e.GetPosition().Y-48/2 && p.Pos.Y <= e.GetPosition().Y+48/2 && + e.IsToggled() && e.GetEnemyState() < gamedata.EnemyStateDying { + c.collisionMask.Clear() + c.collisionMask.DrawImage(c.projectileMask, nil) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Reset() + op.Blend = ebiten.BlendSourceIn + op.GeoM.Translate(e.GetPosition().X-float64(e.GetSprite().Bounds().Dx())/2, e.GetPosition().Y-float64(e.GetSprite().Bounds().Dy())/2) + c.collisionMask.DrawImage(e.GetSprite(), op) + + if c.HasCollided(c.collisionMask, 640*480*4) { + //fmt.Println("pixel collision") + //delete(g.projectiles, k) + projectilevalid = false + //target.ToggleColor() + e.SetHit() + //target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480}) + //target.Hit = true + + /*player := audioContext.NewPlayerFromBytes(assets.TargetHit) + player.Play()*/ + if c.eventmap[gamedata.GameEventTargetHit] != nil { + c.eventmap[gamedata.GameEventTargetHit]() + } + } + } + } + + if projectilevalid { + c.projectiles[i] = p + i++ + } + } + + for j := i; j < len(c.projectiles); j++ { + c.projectiles[j] = nil + } + c.projectiles = c.projectiles[:i] + +} + +func (c *Canvas) UpdateCharge() { + + if c.lastInputs.Charge && !c.charge.Active && !c.gameover { + c.charge.SetOrigin(c.hero.Pos) + c.charge.Reset() + c.charge.ToggleActivate() + + if c.eventmap[gamedata.GameEventCharge] != nil { + c.eventmap[gamedata.GameEventCharge]() + } + } + + c.charge.Update() + if c.charge.Active { + if c.charge.Radius > math.Sqrt(640*640+480*480) { + c.charge.ToggleActivate() + c.charge.Reset() + c.ResetTargetTouches() + } + + for _, e := range c.enemies { + dx := e.GetPosition().X - c.hero.Pos.X + dy := e.GetPosition().Y - c.hero.Pos.Y + r := math.Sqrt(dx*dx + dy*dy) + + if r >= c.charge.Radius-5 && r <= c.charge.Radius+5 && + !e.IsTouched() && e.GetEnemyState() <= gamedata.EnemyStateHit { + e.SetToggle() + e.SetTouched() + } + } + } +} + +func (c *Canvas) ResetTargetTouches() { + for _, e := range c.enemies { + e.ClearTouched() + } +} + +func (c *Canvas) UpdateEnemies() { + //update existing enemies + for _, e := range c.enemies { + if !c.gameover { + e.SetTarget(c.hero.Pos) + } else { + e.SetTarget(e.GetPosition()) + } + + e.Update() + } + if !c.gameover { + //spawn new enemies + f := 40000 / (c.counter + 1) + + if c.counter%f == 0 { + newenemy := elements.NewFlyEye() + + x0 := rand.Float64() * 640 + y0 := rand.Float64() * 480 + quadrant := rand.IntN(3) + + switch quadrant { + case 0: + newenemy.SetPosition(gamedata.Coordinates{X: x0, Y: -48}) + case 1: + newenemy.SetPosition(gamedata.Coordinates{X: x0, Y: 480 + 48}) + case 2: + newenemy.SetPosition(gamedata.Coordinates{X: -48, Y: y0}) + case 3: + newenemy.SetPosition(gamedata.Coordinates{X: 640 + x0, Y: y0}) + } + + newenemy.SetTarget(c.hero.Pos) + + c.enemies = append(c.enemies, newenemy) + } + } +} + +func (c *Canvas) HasCollided(mask *ebiten.Image, size int) bool { + var result bool = false + var pixels []byte = make([]byte, size) + mask.ReadPixels(pixels) + for i := 0; i < len(pixels); i = i + 4 { + if pixels[i+3] != 0 { + result = true + break + } + } + return result +} + +func (c *Canvas) RegisterEvents(e gamedata.GameEvent, f func()) { + c.eventmap[e] = f +} + +func (c *Canvas) CleanupTargets() { + // remove dead targets by iterating over all targets + i := 0 + for _, e := range c.enemies { + //moving valid targets to the front of the slice + if e.GetEnemyState() < elements.MoverActionDead { + c.enemies[i] = e + i++ + } + } + + //then culling the last elements of the slice, and conveniently we can update + //our base score with the number of elements removed (bonuses calculated elsewhere) + if len(c.enemies)-i > 0 { + c.score += len(c.enemies) - i + } + + for j := i; j < len(c.enemies); j++ { + c.enemies[j] = nil + } + c.enemies = c.enemies[:i] +} diff --git a/gameelement/gameelement.go b/gameelement/gameelement.go new file mode 100644 index 0000000..f8158ac --- /dev/null +++ b/gameelement/gameelement.go @@ -0,0 +1,15 @@ +package gameelement + +import ( + "mover/gamedata" + + "github.com/hajimehoshi/ebiten/v2" +) + +type GameElement interface { + SetInputs(gamedata.GameInputs) + Update() error + Draw(drawimg *ebiten.Image) + Initialize() + RegisterEvents(e gamedata.GameEvent, f func()) +} diff --git a/main.go b/main.go index 549fd34..4a8ded0 100644 --- a/main.go +++ b/main.go @@ -35,6 +35,7 @@ func loadScreens(m *screenmanager.Manager) { assets.LoadImages() assets.LoadSounds() m.AddScene(screens.NewStartScreen()) - m.AddScene(screens.NewGame()) + //m.AddScene(screens.NewGame()) + m.AddScene(screens.NewPrimary()) m.ResetScenes() } diff --git a/screens/game.go b/screens/game.go index f33c8af..d490b4e 100644 --- a/screens/game.go +++ b/screens/game.go @@ -62,6 +62,7 @@ func NewGame() *Game { musicInitialized: false, boss: elements.NewBoss(), } + return g } @@ -195,6 +196,14 @@ func (g *Game) Draw(screen *ebiten.Image) { op.GeoM.Translate(-MOVER_WIDTH, -MOVER_HEIGHT) op.GeoM.Translate(g.boss.Pos.X, g.boss.Pos.Y) screen.DrawImage(g.boss.Sprite, op) + + //text.Draw(screen, fmt.Sprintf("%d", g.boss.Health), fonts.SurviveFont.Arcade, 100, 50, color.White) + + //boss health bar + x0 := g.boss.Pos.X - 96 + y0 := g.boss.Pos.Y - 60 + vector.DrawFilledRect(screen, float32(x0), float32(y0), 204, 12, color.Black, true) + vector.DrawFilledRect(screen, float32(x0+2), float32(y0+2), float32(g.boss.Health)*2, 8, color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}, true) } g.projectileMask.Clear() @@ -280,14 +289,15 @@ func (g *Game) StepGame() { //append new projectiles g.AppendProjectiles() - //add new target with increasing frequency - g.SpawnEnemies() - //handle pulsewave updates g.HandlePulseWaveUpdate() - if !g.boss.Spawned && g.counter > 600 { - g.SpawnBoss() + if !g.boss.Spawned { + //add new target with increasing frequency + g.SpawnEnemies() + if g.counter > 2000 { + g.SpawnBoss() + } } } @@ -346,6 +356,20 @@ func (g *Game) HandlePulseWaveUpdate() { //target.SetHit() } } + + //check for boss + if g.boss.Spawned { + dx := g.boss.Pos.X - g.hero.Pos.X + dy := g.boss.Pos.Y - g.hero.Pos.Y + r := math.Sqrt(dx*dx + dy*dy) + + if r >= g.explosion.Radius-40 && r <= g.explosion.Radius+40 && + g.boss.Action <= elements.MoverActionDamaged && !g.boss.Touched { + g.boss.ToggleColor() + g.boss.Touched = true + //target.SetHit() + } + } } } @@ -393,10 +417,10 @@ func (g *Game) UpdateProjectiles() { } } - //boss check first, boundary check + //boss check: first, boundary check if p.Pos.X >= g.boss.Pos.X-MOVER_WIDTH && p.Pos.X <= g.boss.Pos.X+MOVER_WIDTH && p.Pos.Y >= g.boss.Pos.Y-MOVER_HEIGHT && p.Pos.Y <= g.boss.Pos.Y+MOVER_HEIGHT && - g.boss.Action < elements.MoverActionDying { + g.boss.Action == elements.MoverActionDamaged { //fmt.Println("potential collision") //the following computes total collisions in the image using a projectile mask that is a duplicate of what is on screen @@ -476,6 +500,7 @@ func (g *Game) ResetTargetTouches() { for _, t := range g.targets { t.Touched = false } + g.boss.Touched = false } func (g *Game) AppendProjectiles() { @@ -636,6 +661,7 @@ func (g *Game) UpdateBoss() { g.boss.Update() if g.boss.Action == elements.MoverActionExploding && !g.boss.SplodeInitiated { + g.score += 10 player := audioContext.NewPlayerFromBytes(assets.Splode) player.Play() g.boss.SplodeInitiated = true @@ -697,3 +723,7 @@ func (g *Game) HasCollided(mask *ebiten.Image, size int) bool { } return result } + +func (g *Game) SetInputs(gamedata.GameInputs) { + +} diff --git a/screens/primary.go b/screens/primary.go new file mode 100644 index 0000000..9c6e771 --- /dev/null +++ b/screens/primary.go @@ -0,0 +1,227 @@ +package screens + +import ( + "math" + "mover/assets" + "mover/gamedata" + "mover/gameelement" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/audio" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +type Primary struct { + events map[ScreenManagerEvent]func() + dimensions gamedata.Area + elements []gameelement.GameElement + gameevents map[gamedata.GameEvent]bool + + paused bool + gameover bool + musicInitialized bool + + audioplayer *audio.Player +} + +func NewPrimary() *Primary { + p := &Primary{ + events: make(map[ScreenManagerEvent]func()), + paused: false, + gameover: false, + musicInitialized: false, + } + + p.gameevents = make(map[gamedata.GameEvent]bool) + + p.elements = append(p.elements, gameelement.NewBackground(gamedata.Area{Width: 640, Height: 480})) + + canvas := gameelement.NewCanvas(gamedata.Area{Width: 640, Height: 480}) + canvas.RegisterEvents(gamedata.GameEventPlayerDeath, p.EventHandlerPlayerDeath) + canvas.RegisterEvents(gamedata.GameEventCharge, p.EventHandlerCharge) + canvas.RegisterEvents(gamedata.GameEventNewShot, p.EventHandlerNewShot) + canvas.RegisterEvents(gamedata.GameEventTargetHit, p.EventHandlerTargetHit) + canvas.RegisterEvents(gamedata.GameEventExplosion, p.EventHandlerExplosion) + + p.elements = append(p.elements, canvas) + + return p +} + +func (p *Primary) Update() error { + + if !p.musicInitialized { + s := audio.NewInfiniteLoop(assets.SoundBank[assets.MainLoop], assets.SoundBank[assets.MainLoop].Length()) + p.audioplayer, _ = audioContext.NewPlayer(s) + p.audioplayer.Play() + p.musicInitialized = true + } + + //collect all inputs + inputs := p.CollectInputs() + if inputs.Quit { + p.events[EventEndgame]() + } + + if inputs.Reset { + p.Reset() + } + + p.ProcessEventAudio() + + if inputs.Start { + if p.gameover { + p.Reset() + } else { + p.TogglePause() + } + } + + //primary game loop, for each element pass along the inputs + //and process its update logic + if !p.paused { + for _, ge := range p.elements { + ge.SetInputs(inputs) + ge.Update() + } + } + + return nil +} + +func (p *Primary) Draw(screen *ebiten.Image) { + //here we simply call each game elements draw function + //as a layer on top of each other + for _, ge := range p.elements { + ge.Draw(screen) + } +} + +func (p *Primary) SetEventHandler(e ScreenManagerEvent, f func()) { + p.events[e] = f +} + +func (p *Primary) SetDimensions(a gamedata.Area) { + p.dimensions = a +} + +func (p *Primary) CollectInputs() gamedata.GameInputs { + if inpututil.IsKeyJustPressed(ebiten.KeyQ) { + p.events[EventEndgame]() + } + + gi := gamedata.GameInputs{} + + //axes + inpx := ebiten.GamepadAxisValue(0, 0) + inpy := ebiten.GamepadAxisValue(0, 1) + + //handle wasd input + if ebiten.IsKeyPressed(ebiten.KeyD) { + inpx = 1 + } + if ebiten.IsKeyPressed(ebiten.KeyA) { + inpx = -1 + } + if ebiten.IsKeyPressed(ebiten.KeyS) { + inpy = 1 + } + if ebiten.IsKeyPressed(ebiten.KeyW) { + inpy = -1 + } + + gi.XAxis = inpx + gi.YAxis = inpy + + xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal) + yaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical) + + if yaxis <= 0.09 && yaxis >= -0.09 { + yaxis = 0 + } + if xaxis <= 0.09 && xaxis >= -0.09 { + xaxis = 0 + } + + gi.ShotAngle = math.Atan2(yaxis, xaxis) + + gi.Charge = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonRightStick) + gi.Start = inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonCenterRight) + gi.Shot = ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonFrontBottomRight) + gi.Quit = inpututil.IsKeyJustPressed(ebiten.KeyQ) + gi.Reset = inpututil.IsKeyJustPressed(ebiten.KeyR) + + return gi +} + +func (p *Primary) TogglePause() { + p.paused = !p.paused + var player *audio.Player + if p.paused { + player = audioContext.NewPlayerFromBytes(assets.PauseIn) + p.audioplayer.Pause() + } else { + player = audioContext.NewPlayerFromBytes(assets.PauseOut) + p.audioplayer.Play() + } + player.Play() +} + +func (p *Primary) Reset() { + p.paused = false + p.gameover = false + + for _, ge := range p.elements { + ge.Initialize() + } +} + +func (p *Primary) ProcessEventAudio() { + for event, occurred := range p.gameevents { + if occurred { + p.PlayAudio(event) + p.gameevents[event] = false + } + } +} + +func (p *Primary) PlayAudio(e gamedata.GameEvent) { + switch e { + case gamedata.GameEventPlayerDeath: + player := audioContext.NewPlayerFromBytes(assets.HeroDeath) + player.Play() + case gamedata.GameEventCharge: + player := audioContext.NewPlayerFromBytes(assets.Magic) + player.Play() + case gamedata.GameEventNewShot: + player := audioContext.NewPlayerFromBytes(assets.Shot) + player.Play() + case gamedata.GameEventTargetHit: + player := audioContext.NewPlayerFromBytes(assets.TargetHit) + player.Play() + case gamedata.GameEventExplosion: + player := audioContext.NewPlayerFromBytes(assets.Splode) + player.Play() + } +} + +func (p *Primary) EventHandlerPlayerDeath() { + p.gameevents[gamedata.GameEventPlayerDeath] = true + p.gameover = true +} + +func (p *Primary) EventHandlerCharge() { + p.gameevents[gamedata.GameEventCharge] = true +} + +func (p *Primary) EventHandlerNewShot() { + p.gameevents[gamedata.GameEventNewShot] = true +} + +func (p *Primary) EventHandlerTargetHit() { + p.gameevents[gamedata.GameEventTargetHit] = true +} + +func (p *Primary) EventHandlerExplosion() { + p.gameevents[gamedata.GameEventExplosion] = true +}