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 goblinspawned bool goblindead bool lastInputs gamedata.GameInputs runtime float64 counter int score int hero *elements.Hero charge *elements.Explosion goblin *elements.FlyGoblin 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(48, 48), heroCollisionCpy: ebiten.NewImage(48, 48), hero: elements.NewHero(), charge: elements.NewExplosion(), initialized: false, gameover: false, goblinspawned: false, goblindead: false, score: 0, runtime: 0., counter: 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 _, es := range c.enemies { if es.GetEnemyState() < gamedata.EnemyStateExploding { dx := float64(assets.ImageBank[assets.FlyEyeShadow].Bounds().Dx()) / 2 dy := float64(assets.ImageBank[assets.FlyEyeShadow].Bounds().Dy()) / 2 sx := float64(es.GetSprite().Bounds().Dx()) / 48 sy := float64(es.GetSprite().Bounds().Dy()) / 48 op := &ebiten.DrawImageOptions{} op.GeoM.Translate(-dx, -dy) op.GeoM.Scale(sx, sy) op.GeoM.Translate(es.GetPosition().X, es.GetPosition().Y+float64(es.GetSprite().Bounds().Dx())/2) c.Sprite.DrawImage(assets.ImageBank[assets.FlyEyeShadow], op) } } for _, e := range c.enemies { e.Draw() xshift := float64(e.GetSprite().Bounds().Dx() / 2) yshift := float64(e.GetSprite().Bounds().Dy() / 2) op := &ebiten.DrawImageOptions{} op.GeoM.Translate(-xshift, -yshift) op.GeoM.Rotate(e.GetAngle()) op.GeoM.Translate(e.GetPosition().X, e.GetPosition().Y) //op := &ebiten.DrawImageOptions{} //op.GeoM.Translate(e.GetPosition().X-float64(e.GetSprite().Bounds().Dx())/2, e.GetPosition().Y-float64(e.GetSprite().Bounds().Dy())/2) c.Sprite.DrawImage(e.GetSprite(), op) //do we need a health bar for this enemy? if e.Health() > 0 { x0 := e.GetPosition().X - float64(e.GetSprite().Bounds().Dx()) y0 := e.GetPosition().Y - 2/3.*float64(e.GetSprite().Bounds().Dy()) vector.DrawFilledRect(c.Sprite, float32(x0), float32(y0), float32(e.MaxHealth())*2+4, 12, color.Black, true) vector.DrawFilledRect(c.Sprite, float32(x0+2), float32(y0+2), float32(e.Health())*2, 8, color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}, true) } } 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. c.goblinspawned = false c.goblindead = false //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-float64(e.GetSprite().Bounds().Dx())/2 && 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.hero.Pos.Y <= e.GetPosition().Y+float64(e.GetSprite().Bounds().Dy())/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, 48*48*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) if e.GetEnemyState() == gamedata.EnemyStateExploding && !e.ExplosionInitiated() { if c.eventmap[gamedata.GameEventExplosion] != nil { c.eventmap[gamedata.GameEventExplosion]() } e.SetExplosionInitiated() } } else { e.SetTarget(e.GetPosition()) } e.Update() } if !c.gameover { if !c.goblinspawned || c.goblindead { c.SpawnFlyEyes() } if !c.goblinspawned { //&& c.counter > 1200 && !c.goblindead { c.SpawnGoblin() } } } func (c *Canvas) SpawnFlyEyes() { //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) SpawnGoblin() { newfg := elements.NewFlyGoblin() newfg.SetDeathEvent(c.GoblinDeathEvent) newfg.SetFireballCallback(c.GoblinFireballEvent) x0 := rand.Float64() * 640 y0 := rand.Float64() * 480 quadrant := rand.IntN(3) switch quadrant { case 0: newfg.SetPosition(gamedata.Coordinates{X: x0, Y: -96}) case 1: newfg.SetPosition(gamedata.Coordinates{X: x0, Y: 480 + 48}) case 2: newfg.SetPosition(gamedata.Coordinates{X: -96, Y: y0}) case 3: newfg.SetPosition(gamedata.Coordinates{X: 640 + x0, Y: y0}) } c.goblin = newfg c.enemies = append(c.enemies, newfg) c.goblinspawned = true } 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] } func (c *Canvas) GoblinDeathEvent() { c.goblindead = true c.goblinspawned = false c.score += 10 } func (c *Canvas) GoblinFireballEvent() { if !c.gameover { velocity := 8. dx := c.hero.Pos.X - c.goblin.GetPosition().X dy := c.hero.Pos.Y - c.goblin.GetPosition().Y angle := math.Atan2(dy, dx) //add some randomness to the angle arand := rand.Float64() * math.Pi / 3 newfb := elements.NewFireBall(angle+arand, velocity) newfb.SetPosition(c.goblin.GetPosition()) c.enemies = append(c.enemies, newfb) } }