Files
survive/gameelement/canvas.go

404 lines
10 KiB
Go

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