2024-11-15 16:11:45 -05:00
|
|
|
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)
|
2024-11-15 16:46:20 -05:00
|
|
|
|
|
|
|
|
if e.GetEnemyState() == gamedata.EnemyStateExploding && !e.ExplosionInitiated() {
|
|
|
|
|
if c.eventmap[gamedata.GameEventExplosion] != nil {
|
|
|
|
|
c.eventmap[gamedata.GameEventExplosion]()
|
|
|
|
|
}
|
|
|
|
|
e.SetExplosionInitiated()
|
|
|
|
|
}
|
2024-11-15 16:11:45 -05:00
|
|
|
} 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]
|
|
|
|
|
}
|