package screens import ( "fmt" "image" "image/color" "log" "math" "math/rand/v2" "mover/assets" "mover/elements" "mover/fonts" "mover/gamedata" _ "embed" "github.com/hajimehoshi/ebiten/v2" "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 ) type Game struct { 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 runtime float64 hero *elements.Hero projectiles map[int]*elements.Projectile explosion *elements.Explosion score int counter int timer int targets []*elements.Mover gamepadIDsBuf []ebiten.GamepadID gamepadIDs map[ebiten.GamepadID]struct{} //axes map[ebiten.GamepadID][]string //pressedButtons map[ebiten.GamepadID][]string } func (g *Game) Initialize() { 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.projectiles = make(map[int]*elements.Projectile) g.initialized = true g.reset = false } func (g *Game) Update() error { if g.gamepadIDs == nil { g.gamepadIDs = map[ebiten.GamepadID]struct{}{} } g.gamepadIDsBuf = inpututil.AppendJustConnectedGamepadIDs(g.gamepadIDsBuf[:0]) for _, id := range g.gamepadIDsBuf { log.Printf("gamepad connected: id: %d, SDL ID: %s", id, ebiten.GamepadSDLID(id)) g.gamepadIDs[id] = struct{}{} } for id := range g.gamepadIDs { if inpututil.IsGamepadJustDisconnected(id) { log.Printf("gamepad disconnected: id: %d", id) delete(g.gamepadIDs, id) } } if !g.initialized || g.reset { g.Initialize() } else { g.StepGame() } g.timer++ return nil } func (g *Game) Draw(screen *ebiten.Image) { screen.Clear() screen.DrawImage(g.background, nil) g.hero.Draw() op := &ebiten.DrawImageOptions{} if !g.gameover { g.runtime = float64(g.counter) / 60. } 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) } } op.GeoM.Translate(-MOVER_WIDTH/2, -MOVER_HEIGHT/2) //op.GeoM.Rotate(g.hero.Angle) 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) }*/ 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) } g.projectileMask.Clear() //ebitenutil.DrawCircle() for _, p := range g.projectiles { //vector.DrawFilledCircle(screen, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true) vector.DrawFilledCircle(g.projectileMask, float32(p.Pos.X), float32(p.Pos.Y), 3, color.White, true) } screen.DrawImage(g.projectileMask, nil) vector.StrokeCircle(screen, float32(g.explosion.Origin.X), float32(g.explosion.Origin.Y), float32(g.explosion.Radius), 3, color.White, true) /*for _, gamepad ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickHorizontal), ebiten.StandardGamepadAxisValue(id, ebiten.StandardGamepadAxisRightStickVertical))*/ } 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 { g.hero.Update() g.explosion.Update() g.UpdateTargets() 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() } g.CleanupTargets() g.counter++ } } 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 { //for i := 0; i < len(g.projectiles); i++ { // g.projectiles[i].Update() p.Update() //if g.projectiles[i].Pos.X < 5 || g.projectiles[i].Pos.X > 635 || g.projectiles[i].Pos.Y < 5 || g.projectiles[i].Pos.Y > 475 { if p.Pos.X < 5 || p.Pos.X > 635 || p.Pos.Y < 5 || p.Pos.Y > 475 { 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) //var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4) var pixels []byte = make([]byte, g.dimensions.Width*g.dimensions.Height*4) g.collisionMask.ReadPixels(pixels) for i := 0; i < len(pixels); i = i + 4 { if pixels[i+3] != 0 { //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 break } } } } } } func (g *Game) UpdateTargets() { for _, target := range g.targets { 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) //var pixels []byte = make([]byte, MOVER_WIDTH*MOVER_HEIGHT*4) var pixels []byte = make([]byte, MOVER_HEIGHT*MOVER_HEIGHT*4) g.heroCollisionMask.ReadPixels(pixels) for i := 0; i < len(pixels); i = i + 4 { if pixels[i+3] != 0 { //fmt.Println("pixel death") g.hero.SetHit() g.gameover = true break } } } 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.) } } } 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() } } if inpututil.IsStandardGamepadButtonJustPressed(0, ebiten.StandardGamepadButtonCenterRight) { if g.gameover { g.reset = true } else { g.Paused = !g.Paused } } //account for controller sensitivity if !g.gameover { 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) } } } func (g *Game) UpdateHeroPosition() { //handle gamepad input inpx := ebiten.GamepadAxisValue(0, 0) inpy := ebiten.GamepadAxisValue(0, 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/16; i++ { for j := 0; j < 480/16; 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)*16, float64(j)*16) 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()) { }