package screens import ( "fmt" "image" "image/color" "math" "math/rand/v2" "mover/assets" "mover/elements" "mover/fonts" "mover/gamedata" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/audio" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text" "github.com/hajimehoshi/ebiten/v2/vector" ) const ( MOVER_WIDTH = 48 MOVER_HEIGHT = 48 sampleRate = 44100 ) type Game struct { events map[ScreenManagerEvent]func() background *ebiten.Image collisionMask *ebiten.Image projectileMask *ebiten.Image heroCollisionMask *ebiten.Image heroCollisionCpy *ebiten.Image dimensions gamedata.Area Pos gamedata.Coordinates Paused bool initialized bool gameover bool reset bool musicInitialized bool runtime float64 hero *elements.Hero projectiles map[int]*elements.Projectile explosion *elements.Explosion audioplayer *audio.Player score int counter int timer int targets []*elements.Mover boss *elements.Boss } var ( audioContext = audio.NewContext(sampleRate) ) func NewGame() *Game { g := &Game{ events: make(map[ScreenManagerEvent]func()), musicInitialized: false, boss: elements.NewBoss(), } return g } func (g *Game) Initialize() { if !g.musicInitialized { s := audio.NewInfiniteLoop(assets.SoundBank[assets.MainLoop], assets.SoundBank[assets.MainLoop].Length()) g.audioplayer, _ = audioContext.NewPlayer(s) g.audioplayer.Play() g.musicInitialized = true } origin := gamedata.Coordinates{X: 640 / 2, Y: 480 / 2} g.ConstructBackground() g.hero = elements.NewHero() g.hero.SetOrigin(origin) g.hero.ToggleRotate() g.gameover = false g.collisionMask = ebiten.NewImage(g.dimensions.Width, g.dimensions.Height) g.projectileMask = ebiten.NewImage(g.dimensions.Width, g.dimensions.Height) g.heroCollisionMask = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT) g.heroCollisionCpy = ebiten.NewImage(MOVER_WIDTH, MOVER_HEIGHT) g.explosion = elements.NewExplosion() g.explosion.SetOrigin(origin) g.score = 0 g.reset = false //clean up all targets for j := 0; j < len(g.targets); j++ { g.targets[j] = nil } g.targets = g.targets[:0] g.score = 0 g.counter = 0 g.timer = 0 g.runtime = 0. g.boss.Reset() g.projectiles = make(map[int]*elements.Projectile) g.initialized = true g.reset = false } func (g *Game) Update() error { if !g.initialized || g.reset { g.Initialize() } else { g.StepGame() } g.timer++ return nil } func (g *Game) Draw(screen *ebiten.Image) { screen.Clear() if g.initialized { screen.DrawImage(g.background, nil) g.hero.Draw() op := &ebiten.DrawImageOptions{} if !g.gameover { g.runtime = float64(g.counter) / 60. } op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2) op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y) screen.DrawImage(g.hero.Sprite, op) op.GeoM.Reset() op.GeoM.Translate(0, -16) op.GeoM.Rotate(g.hero.Angle) op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y) screen.DrawImage(assets.ImageBank[assets.Weapon], op) //secondary/upgraded weapon sprite; in testing proves sort of distracting /* if g.hero.Upgrade { op.GeoM.Reset() op.GeoM.Translate(-16, -16) op.GeoM.Scale(0.75, 0.75) op.GeoM.Translate(16, 0) op.GeoM.Rotate(g.hero.Angle + math.Pi) op.GeoM.Translate(g.hero.Pos.X, g.hero.Pos.Y) screen.DrawImage(weaponImage, op) } */ //draw shadows-------------------------------------------------------------- for _, target := range g.targets { if target.Action < elements.MoverActionExploding { op := &ebiten.DrawImageOptions{} op.GeoM.Translate(target.Pos.X-10, target.Pos.Y+10) screen.DrawImage(assets.ImageBank[assets.FlyEyeShadow], op) } } 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 { target.Draw() op.GeoM.Reset() op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2) op.GeoM.Rotate(target.Angle) op.GeoM.Translate(target.Pos.X, target.Pos.Y) 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() for _, p := range g.projectiles { vector.DrawFilledCircle(g.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true) } 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) s := fmt.Sprintf("%02.3f", g.runtime) if !g.gameover { text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) } else { if (g.counter/30)%2 == 0 { text.Draw(screen, "TIME: "+s, fonts.SurviveFont.Arcade, 640/2-250, 25, color.White) text.Draw(screen, fmt.Sprintf("SCORE: %d", g.score*10), fonts.SurviveFont.Arcade, 640/2+100, 25, color.White) } text.Draw(screen, "PRESS START TO TRY AGAIN", fonts.SurviveFont.Arcade, 640/2-150, 480/2, color.White) } } } func (g *Game) Layout(width, height int) (int, int) { return g.dimensions.Width, g.dimensions.Height } func (g *Game) CleanupTargets() { // remove dead targets by 1) iterating over all targets i := 0 for _, target := range g.targets { //moving valid targets to the front of the slice if target.Action < elements.MoverActionDead { g.targets[i] = target i++ } } //then culling the last elements of the slice if len(g.targets)-i > 0 { // fmt.Printf("Removing %d elements\n", len(g.targets)-i) g.score += len(g.targets) - i } for j := i; j < len(g.targets); j++ { g.targets[j] = nil } g.targets = g.targets[:i] } func (g *Game) StepGame() { g.HandleInput() if !g.Paused { if !g.audioplayer.IsPlaying() { g.audioplayer.Play() } g.hero.Update() g.explosion.Update() g.UpdateTargets() if g.boss.Spawned { g.UpdateBoss() } g.UpdateProjectiles() if !g.gameover { g.UpdateHeroPosition() //append new projectiles g.AppendProjectiles() //add new target with increasing frequency g.SpawnEnemies() //handle pulsewave updates g.HandlePulseWaveUpdate() if !g.boss.Spawned && g.counter > 600 { g.SpawnBoss() } } g.CleanupTargets() g.counter++ } else { g.audioplayer.Pause() } } func (g *Game) SpawnEnemies() { f := 40000 / (g.counter + 1) if g.counter%f == 0 { g.targets = append(g.targets, elements.NewMover()) x0 := rand.Float64() * 640 y0 := rand.Float64() * 480 quadrant := rand.IntN(3) switch quadrant { case 0: g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: x0, Y: -MOVER_HEIGHT}) case 1: g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: x0, Y: float64(g.dimensions.Height) + MOVER_HEIGHT}) case 2: g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: -MOVER_WIDTH, Y: y0}) case 3: g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: float64(g.dimensions.Width) + x0, Y: y0}) default: g.targets[len(g.targets)-1].SetOrigin(gamedata.Coordinates{X: x0, Y: y0}) fmt.Println("WTF " + string(quadrant)) } } } func (g *Game) HandlePulseWaveUpdate() { if g.explosion.Active { if g.explosion.Radius > math.Sqrt(640*640+480*480) { g.explosion.ToggleActivate() g.explosion.Reset() g.ResetTargetTouches() } //check collisions for _, target := range g.targets { dx := target.Pos.X - g.hero.Pos.X dy := target.Pos.Y - g.hero.Pos.Y r := math.Sqrt(dx*dx + dy*dy) if r >= g.explosion.Radius-5 && r <= g.explosion.Radius+5 && target.Action <= elements.MoverActionDamaged && !target.Touched { target.ToggleColor() target.Touched = true //target.SetHit() } } } } func (g *Game) UpdateProjectiles() { for k, p := range g.projectiles { p.Update() //cleanup projectiles at boundaries if p.Pos.X < -640/2 || p.Pos.X > 1.5*640 || p.Pos.Y < -480/2 || p.Pos.Y > 1.5*480 { p.Velocity = 0 delete(g.projectiles, k) } //compute projectile collisions for _, target := range g.targets { //first, boundary check if p.Pos.X >= target.Pos.X-MOVER_WIDTH/2 && p.Pos.X <= target.Pos.X+MOVER_WIDTH/2 && p.Pos.Y >= target.Pos.Y-MOVER_HEIGHT/2 && p.Pos.Y <= target.Pos.Y+MOVER_HEIGHT/2 && target.Action == elements.MoverActionDamaged { //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(target.Pos.X-MOVER_WIDTH/2, target.Pos.Y-MOVER_HEIGHT/2) g.collisionMask.DrawImage(target.Sprite, op) if g.HasCollided(g.collisionMask, g.dimensions.Width*g.dimensions.Height*4) { //fmt.Println("pixel collision") delete(g.projectiles, k) //target.ToggleColor() target.SetHit() //target.SetOrigin(gamedata.Coordinates{X: rand.Float64() * 640, Y: rand.Float64() * 480}) target.Hit = true player := audioContext.NewPlayerFromBytes(assets.TargetHit) player.Play() } } } //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() { for _, target := range g.targets { if target.Action == elements.MoverActionExploding && !target.SplodeInitiated { player := audioContext.NewPlayerFromBytes(assets.Splode) player.Play() target.SplodeInitiated = true } if !target.Hit && g.hero.Action < elements.HeroActionDying { dx := g.hero.Pos.X - target.Pos.X dy := g.hero.Pos.Y - target.Pos.Y angle := math.Atan2(dy, dx) //maxspeed := (float64(g.counter) + 1.) / 1000. maxspeed := 2.9 target.Pos.X += maxspeed * math.Cos(angle) target.Pos.Y += maxspeed * math.Sin(angle) } //compute collision with hero if g.hero.Pos.X >= target.Pos.X-MOVER_WIDTH/2 && g.hero.Pos.X <= target.Pos.X+MOVER_WIDTH/2 && g.hero.Pos.Y >= target.Pos.Y-MOVER_HEIGHT/2 && g.hero.Pos.Y <= target.Pos.Y+MOVER_HEIGHT/2 && target.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-target.Pos.X)-MOVER_WIDTH/2, (g.hero.Pos.Y-target.Pos.Y)-MOVER_HEIGHT/2) g.heroCollisionMask.DrawImage(target.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() } } target.Update() } } func (g *Game) ResetTargetTouches() { for _, t := range g.targets { t.Touched = false } } func (g *Game) AppendProjectiles() { if g.counter%14 == 0 && ebiten.IsStandardGamepadButtonPressed(0, ebiten.StandardGamepadButtonFrontBottomRight) { g.projectiles[g.counter] = elements.NewProjectile(gamedata.Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle, 5.) if g.hero.Upgrade { g.projectiles[g.counter+1] = elements.NewProjectile(gamedata.Coordinates{X: g.hero.Pos.X, Y: g.hero.Pos.Y}, g.hero.Angle+math.Pi, 5.) } player := audioContext.NewPlayerFromBytes(assets.Shot) player.Play() } } func (g *Game) HandleInput() { //if len(g.gamepadIDs) > 0 { if inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonRightStick) { if !g.explosion.Active && !g.gameover { g.explosion.SetOrigin(g.hero.Pos) g.explosion.Reset() g.explosion.ToggleActivate() player := audioContext.NewPlayerFromBytes(assets.Magic) player.Play() } } if inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonCenterRight) { if g.gameover { g.reset = true } else { g.Paused = !g.Paused var player *audio.Player if g.Paused { player = audioContext.NewPlayerFromBytes(assets.PauseIn) } else { player = audioContext.NewPlayerFromBytes(assets.PauseOut) } player.Play() } } //account for controller sensitivity if !g.gameover && !g.Paused { xaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickHorizontal) yaxis := ebiten.StandardGamepadAxisValue(0, ebiten.StandardGamepadAxisRightStickVertical) if yaxis <= 0.09 && yaxis >= -0.09 { yaxis = 0 } if xaxis <= 0.09 && xaxis >= -0.09 { xaxis = 0 } inputangle := math.Atan2(yaxis, xaxis) g.hero.SetAngle(inputangle) } if inpututil.IsKeyJustPressed(ebiten.KeyQ) { g.events[EventEndgame]() } //} } func (g *Game) UpdateHeroPosition() { //handle gamepad input inpx := ebiten.GamepadAxisValue(0, 0) 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 { g.hero.Left = inpx < 0 g.hero.Pos.X += inpx * 5 } if inpy >= 0.15 || inpy <= -0.15 { g.hero.Pos.Y += inpy * 5 } } func (g *Game) ConstructBackground() { g.background = ebiten.NewImage(g.dimensions.Width, g.dimensions.Height) BLOCK_SIZE := 32 for i := 0; i < 640/BLOCK_SIZE; i++ { for j := 0; j < 480/BLOCK_SIZE; j++ { //select random tile in x and y from tileset idx_y := rand.IntN(256 / BLOCK_SIZE) idx_x := rand.IntN(256 / BLOCK_SIZE) x0 := BLOCK_SIZE * idx_x y0 := BLOCK_SIZE * idx_y x1 := x0 + BLOCK_SIZE y1 := y0 + BLOCK_SIZE //translate for grid element we're painting op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(i*BLOCK_SIZE), float64(j*BLOCK_SIZE)) g.background.DrawImage(assets.ImageBank[assets.TileSet].SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), op) } } ax := float64(rand.IntN(640/BLOCK_SIZE) * BLOCK_SIZE) ay := float64(rand.IntN(480/BLOCK_SIZE) * BLOCK_SIZE) op := &ebiten.DrawImageOptions{} op.GeoM.Translate(ax, ay) g.background.DrawImage(assets.ImageBank[assets.Altar], op) } func (g *Game) SetDimensions(a gamedata.Area) { g.dimensions = a } func (g *Game) SetEventHandler(e ScreenManagerEvent, f func()) { 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 }