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 0000000..536dd25 Binary files /dev/null and b/assets/worm.png differ diff --git a/assets/wormdefault.png b/assets/wormdefault.png new file mode 100644 index 0000000..3150572 Binary files /dev/null and b/assets/wormdefault.png differ 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 +}