Added new enemy character. WIP.

This commit is contained in:
2024-11-13 07:44:56 -05:00
parent 8a1194eca3
commit 478ba994d6
6 changed files with 367 additions and 34 deletions

View File

@@ -23,6 +23,7 @@ const (
TileSet ImgAssetName = "TileSet" TileSet ImgAssetName = "TileSet"
Altar ImgAssetName = "Altar" Altar ImgAssetName = "Altar"
Weapon ImgAssetName = "Weapon" Weapon ImgAssetName = "Weapon"
Worm ImgAssetName = "Worm"
) )
var ( var (
@@ -48,6 +49,8 @@ var (
altar_img []byte altar_img []byte
//go:embed weapon.png //go:embed weapon.png
weapon_img []byte weapon_img []byte
//go:embed worm.png
worm_img []byte
) )
func LoadImages() { func LoadImages() {
@@ -63,6 +66,7 @@ func LoadImages() {
ImageBank[TileSet] = LoadImagesFatal(tileset_img) ImageBank[TileSet] = LoadImagesFatal(tileset_img)
ImageBank[Altar] = LoadImagesFatal(altar_img) ImageBank[Altar] = LoadImagesFatal(altar_img)
ImageBank[Weapon] = LoadImagesFatal(weapon_img) ImageBank[Weapon] = LoadImagesFatal(weapon_img)
ImageBank[Worm] = LoadImagesFatal(worm_img)
} }

110
elements/boss.go Normal file
View File

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

View File

@@ -26,7 +26,7 @@ func NewManager() Manager {
return Manager{ return Manager{
Info: gamedata.GameInfo{ Info: gamedata.GameInfo{
Name: "survive", Name: "survive",
Version: "0.22", Version: "0.26",
Dimensions: gamedata.Area{ Dimensions: gamedata.Area{
Width: defaultWidth, Width: defaultWidth,
Height: defaultHeight, Height: defaultHeight,

View File

@@ -49,6 +49,7 @@ type Game struct {
counter int counter int
timer int timer int
targets []*elements.Mover targets []*elements.Mover
boss *elements.Boss
} }
var ( var (
@@ -59,6 +60,7 @@ func NewGame() *Game {
g := &Game{ g := &Game{
events: make(map[ScreenManagerEvent]func()), events: make(map[ScreenManagerEvent]func()),
musicInitialized: false, musicInitialized: false,
boss: elements.NewBoss(),
} }
return g return g
} }
@@ -102,6 +104,8 @@ func (g *Game) Initialize() {
g.timer = 0 g.timer = 0
g.runtime = 0. g.runtime = 0.
g.boss.Reset()
g.projectiles = make(map[int]*elements.Projectile) g.projectiles = make(map[int]*elements.Projectile)
g.initialized = true g.initialized = true
g.reset = false g.reset = false
@@ -158,7 +162,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
} }
*/ */
//draw shadows //draw shadows--------------------------------------------------------------
for _, target := range g.targets { for _, target := range g.targets {
if target.Action < elements.MoverActionExploding { if target.Action < elements.MoverActionExploding {
op := &ebiten.DrawImageOptions{} 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 { for _, target := range g.targets {
target.Draw() target.Draw()
@@ -177,6 +189,14 @@ func (g *Game) Draw(screen *ebiten.Image) {
screen.DrawImage(target.Sprite, op) 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() g.projectileMask.Clear()
for _, p := range g.projectiles { for _, p := range g.projectiles {
@@ -185,6 +205,12 @@ func (g *Game) Draw(screen *ebiten.Image) {
screen.DrawImage(g.projectileMask, nil) 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) 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) s := fmt.Sprintf("%02.3f", g.runtime)
@@ -243,16 +269,27 @@ func (g *Game) StepGame() {
g.explosion.Update() g.explosion.Update()
g.UpdateTargets() g.UpdateTargets()
if g.boss.Spawned {
g.UpdateBoss()
}
g.UpdateProjectiles() g.UpdateProjectiles()
if !g.gameover { if !g.gameover {
g.UpdateHeroPosition() g.UpdateHeroPosition()
//append new projectiles //append new projectiles
g.AppendProjectiles() g.AppendProjectiles()
//add new target with increasing frequency //add new target with increasing frequency
g.SpawnEnemies() g.SpawnEnemies()
//handle pulsewave updates //handle pulsewave updates
g.HandlePulseWaveUpdate() g.HandlePulseWaveUpdate()
if !g.boss.Spawned && g.counter > 600 {
g.SpawnBoss()
}
} }
g.CleanupTargets() 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) op.GeoM.Translate(target.Pos.X-MOVER_WIDTH/2, target.Pos.Y-MOVER_HEIGHT/2)
g.collisionMask.DrawImage(target.Sprite, op) g.collisionMask.DrawImage(target.Sprite, op)
//var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4) if g.HasCollided(g.collisionMask, g.dimensions.Width*g.dimensions.Height*4) {
var pixels []byte = make([]byte, g.dimensions.Width*g.dimensions.Height*4) //fmt.Println("pixel collision")
g.collisionMask.ReadPixels(pixels) delete(g.projectiles, k)
for i := 0; i < len(pixels); i = i + 4 { //target.ToggleColor()
if pixels[i+3] != 0 { target.SetHit()
//fmt.Println("pixel collision") //target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480})
delete(g.projectiles, k) target.Hit = true
//target.ToggleColor()
target.SetHit()
//target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480})
target.Hit = true
//var err error player := audioContext.NewPlayerFromBytes(assets.TargetHit)
//player, err := audioContext.NewPlayer(assets.SoundBank[assets.Explode]) player.Play()
player := audioContext.NewPlayerFromBytes(assets.TargetHit)
player.Play()
break
}
} }
} }
} }
//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() { 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) 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) g.heroCollisionMask.DrawImage(target.Sprite, op)
//var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4) if g.HasCollided(g.heroCollisionMask, MOVER_HEIGHT*MOVER_HEIGHT*4) {
var pixels []byte = make([]byte, MOVER_HEIGHT*MOVER_HEIGHT*4) g.hero.SetHit()
g.heroCollisionMask.ReadPixels(pixels) g.gameover = true
for i := 0; i < len(pixels); i = i + 4 {
if pixels[i+3] != 0 {
//fmt.Println("pixel death")
g.hero.SetHit()
g.gameover = true
player := audioContext.NewPlayerFromBytes(assets.HeroDeath) player := audioContext.NewPlayerFromBytes(assets.HeroDeath)
player.Play() player.Play()
break
}
} }
} }
@@ -494,6 +545,21 @@ func (g *Game) UpdateHeroPosition() {
//handle gamepad input //handle gamepad input
inpx := ebiten.GamepadAxisValue(0, 0) inpx := ebiten.GamepadAxisValue(0, 0)
inpy := ebiten.GamepadAxisValue(0, 1) 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 { if inpx >= 0.15 || inpx <= -0.15 {
g.hero.Left = inpx < 0 g.hero.Left = inpx < 0
g.hero.Pos.X += inpx * 5 g.hero.Pos.X += inpx * 5
@@ -502,6 +568,7 @@ func (g *Game) UpdateHeroPosition() {
if inpy >= 0.15 || inpy <= -0.15 { if inpy >= 0.15 || inpy <= -0.15 {
g.hero.Pos.Y += inpy * 5 g.hero.Pos.Y += inpy * 5
} }
} }
func (g *Game) ConstructBackground() { func (g *Game) ConstructBackground() {
@@ -542,3 +609,91 @@ func (g *Game) SetDimensions(a gamedata.Area) {
func (g *Game) SetEventHandler(e ScreenManagerEvent, f func()) { func (g *Game) SetEventHandler(e ScreenManagerEvent, f func()) {
g.events[e] = f 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
}

View File

@@ -6,6 +6,7 @@ import (
"mover/assets" "mover/assets"
"mover/fonts" "mover/fonts"
"mover/gamedata" "mover/gamedata"
"mover/touch"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio" "github.com/hajimehoshi/ebiten/v2/audio"
@@ -38,8 +39,20 @@ func NewStartScreen() *StartScreen {
func (s *StartScreen) Update() error { 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) || if inpututil.IsKeyJustPressed(ebiten.KeyEnter) ||
ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonCenterRight) { ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonCenterRight) ||
touched {
s.eHandler[EventCompleted]() s.eHandler[EventCompleted]()
if s.audioplayer.IsPlaying() { if s.audioplayer.IsPlaying() {
s.audioplayer.Close() s.audioplayer.Close()

51
touch/touch.go Normal file
View File

@@ -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{}
}