From 478ba994d6b6e2657b2cf1d3159482c83e329e24 Mon Sep 17 00:00:00 2001 From: iegod Date: Wed, 13 Nov 2024 07:44:56 -0500 Subject: [PATCH] Added new enemy character. WIP. --- assets/imagebank.go | 4 + elements/boss.go | 110 ++++++++++++++++++++ screenmanager/manager.go | 2 +- screens/game.go | 219 +++++++++++++++++++++++++++++++++------ screens/start.go | 15 ++- touch/touch.go | 51 +++++++++ 6 files changed, 367 insertions(+), 34 deletions(-) create mode 100644 elements/boss.go create mode 100644 touch/touch.go diff --git a/assets/imagebank.go b/assets/imagebank.go index dcd272d..8ecbc06 100644 --- a/assets/imagebank.go +++ b/assets/imagebank.go @@ -23,6 +23,7 @@ const ( TileSet ImgAssetName = "TileSet" Altar ImgAssetName = "Altar" Weapon ImgAssetName = "Weapon" + Worm ImgAssetName = "Worm" ) var ( @@ -48,6 +49,8 @@ var ( altar_img []byte //go:embed weapon.png weapon_img []byte + //go:embed worm.png + worm_img []byte ) func LoadImages() { @@ -63,6 +66,7 @@ func LoadImages() { ImageBank[TileSet] = LoadImagesFatal(tileset_img) ImageBank[Altar] = LoadImagesFatal(altar_img) ImageBank[Weapon] = LoadImagesFatal(weapon_img) + ImageBank[Worm] = LoadImagesFatal(worm_img) } diff --git a/elements/boss.go b/elements/boss.go new file mode 100644 index 0000000..fa4efe9 --- /dev/null +++ b/elements/boss.go @@ -0,0 +1,110 @@ +package elements + +import ( + "image" + "image/color" + "mover/assets" + "mover/gamedata" + + "github.com/hajimehoshi/ebiten/v2" +) + +type Boss struct { + Sprite *ebiten.Image + Maks *ebiten.Image + MaskDest *ebiten.Image + Spawned bool + Pos gamedata.Coordinates + Right bool + damage bool + cycle int + Action MoverAction + hitcount int + damageduration int + SplodeInitiated bool +} + +func NewBoss() *Boss { + b := &Boss{ + Sprite: ebiten.NewImage(96, 96), + Spawned: false, + Action: MoverActionDefault, + hitcount: 0, + Maks: ebiten.NewImage(96, 96), + MaskDest: ebiten.NewImage(96, 96), + } + b.Maks.Fill(color.White) + return b +} + +func (b *Boss) Update() { + if b.damage { + b.damageduration++ + if b.damageduration > 30 { + b.damage = false + b.damageduration = 0 + } + } + b.cycle++ +} + +func (b *Boss) Draw() { + b.Sprite.Clear() + b.MaskDest.Clear() + + /* + //b.Sprite.Fill(color.RGBA{R: 0xFF, G: 0xFF, B: 0x00, A: 0xFF}) + vector.DrawFilledCircle(b.Sprite, 48, 48, 48, color.RGBA{R: 0xFF, G: 0xFF, B: 0x00, A: 0xFF}, true) + */ + + idx := (b.cycle / 8) % 4 + x0 := 96 * idx + x1 := x0 + 96 + y0 := 0 + y1 := 96 + + op := &ebiten.DrawImageOptions{} + if b.Right { + op.GeoM.Scale(-1, 1) + op.GeoM.Translate(MOVER_WIDTH*2, 0) + } + + switch b.Action { + case MoverActionDefault: + 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) + 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) + } + case MoverActionExploding: + op.GeoM.Scale(2, 2) + //op.GeoM.Translate(-48, -48) + b.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeDying].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) + if idx == 3 { + b.Action++ + } + } + +} + +func (b *Boss) SetHit() { + b.hitcount++ + b.damage = true + if b.hitcount > 10 { + b.Action = MoverActionExploding + b.cycle = 0 + } +} + +func (b *Boss) Reset() { + b.hitcount = 0 + b.damage = false + b.damageduration = 0 + b.Action = MoverActionDefault + b.Spawned = false +} diff --git a/screenmanager/manager.go b/screenmanager/manager.go index b3439d2..610346c 100644 --- a/screenmanager/manager.go +++ b/screenmanager/manager.go @@ -26,7 +26,7 @@ func NewManager() Manager { return Manager{ Info: gamedata.GameInfo{ Name: "survive", - Version: "0.22", + Version: "0.26", Dimensions: gamedata.Area{ Width: defaultWidth, Height: defaultHeight, diff --git a/screens/game.go b/screens/game.go index 6674885..f33c8af 100644 --- a/screens/game.go +++ b/screens/game.go @@ -49,6 +49,7 @@ type Game struct { counter int timer int targets []*elements.Mover + boss *elements.Boss } var ( @@ -59,6 +60,7 @@ func NewGame() *Game { g := &Game{ events: make(map[ScreenManagerEvent]func()), musicInitialized: false, + boss: elements.NewBoss(), } return g } @@ -102,6 +104,8 @@ func (g *Game) Initialize() { g.timer = 0 g.runtime = 0. + g.boss.Reset() + g.projectiles = make(map[int]*elements.Projectile) g.initialized = true g.reset = false @@ -158,7 +162,7 @@ func (g *Game) Draw(screen *ebiten.Image) { } */ - //draw shadows + //draw shadows-------------------------------------------------------------- for _, target := range g.targets { if target.Action < elements.MoverActionExploding { op := &ebiten.DrawImageOptions{} @@ -167,6 +171,14 @@ func (g *Game) Draw(screen *ebiten.Image) { } } + if g.boss.Spawned && g.boss.Action < elements.MoverActionExploding { + op := &ebiten.DrawImageOptions{} + op.GeoM.Scale(4, 2) + op.GeoM.Translate(g.boss.Pos.X-96/2, g.boss.Pos.Y+96/2-10) + screen.DrawImage(assets.ImageBank[assets.FlyEyeShadow], op) + } + + //draw enemies-------------------------------------------------------------- for _, target := range g.targets { target.Draw() @@ -177,6 +189,14 @@ func (g *Game) Draw(screen *ebiten.Image) { screen.DrawImage(target.Sprite, op) } + if g.boss.Spawned { + g.boss.Draw() + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(-MOVER_WIDTH, -MOVER_HEIGHT) + op.GeoM.Translate(g.boss.Pos.X, g.boss.Pos.Y) + screen.DrawImage(g.boss.Sprite, op) + } + g.projectileMask.Clear() for _, p := range g.projectiles { @@ -185,6 +205,12 @@ func (g *Game) Draw(screen *ebiten.Image) { screen.DrawImage(g.projectileMask, nil) + /* + op.GeoM.Reset() + op.GeoM.Scale(0.25, 0.25) + screen.DrawImage(g.collisionMask, op) + */ + vector.StrokeCircle(screen, float32(g.explosion.Origin.X), float32(g.explosion.Origin.Y), float32(g.explosion.Radius), 3, color.White, true) s := fmt.Sprintf("%02.3f", g.runtime) @@ -243,16 +269,27 @@ func (g *Game) StepGame() { g.explosion.Update() g.UpdateTargets() + if g.boss.Spawned { + g.UpdateBoss() + } + g.UpdateProjectiles() if !g.gameover { g.UpdateHeroPosition() //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() + } + } g.CleanupTargets() @@ -342,30 +379,51 @@ func (g *Game) UpdateProjectiles() { op.GeoM.Translate(target.Pos.X-MOVER_WIDTH/2, target.Pos.Y-MOVER_HEIGHT/2) g.collisionMask.DrawImage(target.Sprite, op) - //var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4) - var pixels []byte = make([]byte, g.dimensions.Width*g.dimensions.Height*4) - g.collisionMask.ReadPixels(pixels) - for i := 0; i < len(pixels); i = i + 4 { - if pixels[i+3] != 0 { - //fmt.Println("pixel collision") - delete(g.projectiles, k) - //target.ToggleColor() - target.SetHit() - //target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480}) - target.Hit = true + if g.HasCollided(g.collisionMask, g.dimensions.Width*g.dimensions.Height*4) { + //fmt.Println("pixel collision") + delete(g.projectiles, k) + //target.ToggleColor() + target.SetHit() + //target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480}) + target.Hit = true - //var err error - //player, err := audioContext.NewPlayer(assets.SoundBank[assets.Explode]) - - player := audioContext.NewPlayerFromBytes(assets.TargetHit) - player.Play() - - break - } + player := audioContext.NewPlayerFromBytes(assets.TargetHit) + player.Play() } } } + + //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 { + //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 + //there's definitely room for optimization here + g.collisionMask.Clear() + g.collisionMask.DrawImage(g.projectileMask, nil) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Reset() + op.Blend = ebiten.BlendSourceIn + op.GeoM.Translate(g.boss.Pos.X-MOVER_WIDTH/2, g.boss.Pos.Y-MOVER_HEIGHT/2) + g.collisionMask.DrawImage(g.boss.Sprite, op) + + if g.HasCollided(g.collisionMask, g.dimensions.Width*g.dimensions.Height*4) { + //fmt.Println("pixel collision") + delete(g.projectiles, k) + //target.ToggleColor() + g.boss.SetHit() + //target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480}) + //g.boss.Hit = true + + player := audioContext.NewPlayerFromBytes(assets.TargetHit) + player.Play() + } + } } + } func (g *Game) UpdateTargets() { @@ -401,19 +459,12 @@ func (g *Game) UpdateTargets() { 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.SetHit() - g.gameover = true + if g.HasCollided(g.heroCollisionMask, MOVER_HEIGHT*MOVER_HEIGHT*4) { + g.hero.SetHit() + g.gameover = true - player := audioContext.NewPlayerFromBytes(assets.HeroDeath) - player.Play() - break - } + player := audioContext.NewPlayerFromBytes(assets.HeroDeath) + player.Play() } } @@ -494,6 +545,21 @@ func (g *Game) UpdateHeroPosition() { //handle gamepad input 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 + } + if inpx >= 0.15 || inpx <= -0.15 { g.hero.Left = inpx < 0 g.hero.Pos.X += inpx * 5 @@ -502,6 +568,7 @@ func (g *Game) UpdateHeroPosition() { if inpy >= 0.15 || inpy <= -0.15 { g.hero.Pos.Y += inpy * 5 } + } func (g *Game) ConstructBackground() { @@ -542,3 +609,91 @@ func (g *Game) SetDimensions(a gamedata.Area) { func (g *Game) SetEventHandler(e ScreenManagerEvent, f func()) { g.events[e] = f } + +func (g *Game) SpawnBoss() { + x0 := rand.Float64() * 640 + y0 := rand.Float64() * 480 + quadrant := rand.IntN(3) + + switch quadrant { + case 0: + g.boss.Pos = gamedata.Coordinates{X: x0, Y: -(MOVER_HEIGHT * 2)} + case 1: + g.boss.Pos = gamedata.Coordinates{X: x0, Y: float64(g.dimensions.Height) + (MOVER_HEIGHT * 2)} + case 2: + g.boss.Pos = gamedata.Coordinates{X: -(MOVER_HEIGHT * 2), Y: y0} + case 3: + g.boss.Pos = gamedata.Coordinates{X: float64(g.dimensions.Width) + x0, Y: y0} + default: + g.boss.Pos = gamedata.Coordinates{X: x0, Y: y0} + fmt.Println("WTF " + string(quadrant)) + } + //g.boss.Pos = gamedata.Coordinates{X: 640 / 2, Y: 480 / 2} + g.boss.Spawned = true +} + +func (g *Game) UpdateBoss() { + g.boss.Update() + + if g.boss.Action == elements.MoverActionExploding && !g.boss.SplodeInitiated { + player := audioContext.NewPlayerFromBytes(assets.Splode) + player.Play() + g.boss.SplodeInitiated = true + } + /* + if g.boss.Action >= elements.MoverActionDying { + g.boss.Spawned = false + }*/ + + if g.boss.Action >= elements.MoverActionDead { + g.boss.Pos = gamedata.Coordinates{X: -96, Y: -96} + } + + if g.boss.Action < elements.MoverActionDying { + dx := g.hero.Pos.X - g.boss.Pos.X + dy := g.hero.Pos.Y - g.boss.Pos.Y + + g.boss.Right = dx/48 > 0 + + g.boss.Pos = gamedata.Coordinates{ + X: g.boss.Pos.X + dx/48, + Y: g.boss.Pos.Y + dy/48, + } + } + + //compute collision with hero + if g.hero.Pos.X >= g.boss.Pos.X-MOVER_WIDTH && g.hero.Pos.X <= g.boss.Pos.X+MOVER_WIDTH && + g.hero.Pos.Y >= g.boss.Pos.Y-MOVER_HEIGHT && g.hero.Pos.Y <= g.boss.Pos.Y+MOVER_HEIGHT && + g.boss.Action < elements.MoverActionDying && g.hero.Action < elements.HeroActionDying { + 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-g.boss.Pos.X)-MOVER_WIDTH, (g.hero.Pos.Y-g.boss.Pos.Y)-MOVER_HEIGHT) + g.heroCollisionMask.DrawImage(g.boss.Sprite, op) + + if g.HasCollided(g.heroCollisionMask, MOVER_HEIGHT*MOVER_HEIGHT*4) { + g.hero.SetHit() + g.gameover = true + + player := audioContext.NewPlayerFromBytes(assets.HeroDeath) + player.Play() + } + + } +} + +func (g *Game) 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 +} diff --git a/screens/start.go b/screens/start.go index 3e43736..732fbc3 100644 --- a/screens/start.go +++ b/screens/start.go @@ -6,6 +6,7 @@ import ( "mover/assets" "mover/fonts" "mover/gamedata" + "mover/touch" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/audio" @@ -38,8 +39,20 @@ func NewStartScreen() *StartScreen { func (s *StartScreen) Update() error { + touch.UpdateTouchIDs() + + ids := touch.GetTouchIDs() + var touched bool = false + for _, id := range ids { + touched = touch.IsTouchJustPressed(id) + if touched { + break + } + } + if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || - ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonCenterRight) { + ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonCenterRight) || + touched { s.eHandler[EventCompleted]() if s.audioplayer.IsPlaying() { s.audioplayer.Close() diff --git a/touch/touch.go b/touch/touch.go new file mode 100644 index 0000000..49098d0 --- /dev/null +++ b/touch/touch.go @@ -0,0 +1,51 @@ +package touch + +import ( + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "golang.org/x/exp/maps" +) + +var ( + allTouchIDs []ebiten.TouchID + currentTouchIDs map[ebiten.TouchID]bool + justPressedTouchIDs map[ebiten.TouchID]bool + justReleasedTouchIDs map[ebiten.TouchID]bool +) + +func UpdateTouchIDs() { + newPressedTouchIDs := []ebiten.TouchID{} + newPressedTouchIDs = inpututil.AppendJustPressedTouchIDs(newPressedTouchIDs) + justPressedTouchIDs = map[ebiten.TouchID]bool{} + for i := 0; i < len(newPressedTouchIDs); i++ { + justPressedTouchIDs[newPressedTouchIDs[i]] = true + currentTouchIDs[newPressedTouchIDs[i]] = true + } + justReleasedTouchIDs = map[ebiten.TouchID]bool{} + allTouchIDs = maps.Keys(currentTouchIDs) + newReleasedTouchIDs := []ebiten.TouchID{} + newReleasedTouchIDs = inpututil.AppendJustReleasedTouchIDs(newReleasedTouchIDs) + for i := 0; i < len(newReleasedTouchIDs); i++ { + justReleasedTouchIDs[newReleasedTouchIDs[i]] = true + delete(currentTouchIDs, newReleasedTouchIDs[i]) + } +} + +func GetTouchIDs() []ebiten.TouchID { + return allTouchIDs +} + +func IsTouchJustPressed(touchID ebiten.TouchID) bool { + return justPressedTouchIDs[touchID] +} + +func IsTouchJustReleased(touchID ebiten.TouchID) bool { + return justReleasedTouchIDs[touchID] +} + +func init() { + allTouchIDs = []ebiten.TouchID{} + currentTouchIDs = map[ebiten.TouchID]bool{} + justPressedTouchIDs = map[ebiten.TouchID]bool{} + justReleasedTouchIDs = map[ebiten.TouchID]bool{} +}