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