Files
survive/screens/game.go
2024-11-13 07:44:56 -05:00

700 lines
18 KiB
Go

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
}