diff --git a/README.md b/README.md index 11b6a88..b8933be 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,26 @@ Run the following command to build a `carotidartillery` executable: Run `~/go/bin/carotidartillery` to play. +## Gameplay + +### Items + +#### Garlic + +Eat garlic to repel creeps for 7 seconds. + +#### Holy water + +Break a vial of holy water to ward off death for 1 second. + +### Creeps + +#### Vampire + +Cursed creature with an insatiable thirst for blood. + +Some vampires take the appearance of a bat. + ## Support Please share issues and suggestions [here](https://code.rocketnine.space/tslocum/carotidartillery/issues). diff --git a/assets/items/holywater.png b/assets/items/holywater.png new file mode 100644 index 0000000..7b28c95 Binary files /dev/null and b/assets/items/holywater.png differ diff --git a/creep.go b/creep.go index e1f1fcd..c4f550e 100644 --- a/creep.go +++ b/creep.go @@ -44,14 +44,14 @@ func (c *gameCreep) queueNextAction() { c.nextAction = 288 + rand.Intn(288) return } - c.nextAction = 288 + rand.Intn(720) + c.nextAction = 288 + rand.Intn(432) } func (c *gameCreep) runAway() { c.queueNextAction() - randMovementA := (rand.Float64() - 0.5) / 7 - randMovementB := (rand.Float64() - 0.5) / 7 + randMovementA := (rand.Float64() - 0.5) / 8 + randMovementB := (rand.Float64() - 0.5) / 8 c.moveX = c.x - c.player.x if c.moveX < 0 { @@ -67,37 +67,42 @@ func (c *gameCreep) runAway() { } } +func (c *gameCreep) seekPlayer() { + maxSpeed := 0.5 / 9 + minSpeed := 0.1 / 9 + + a := angle(c.x, c.y, c.player.x, c.player.y) + c.moveX = -math.Cos(a) + c.moveY = -math.Sin(a) + for { + if (c.moveX < -minSpeed || c.moveX > minSpeed) || (c.moveY < -minSpeed || c.moveY > minSpeed) { + break + } + + c.moveX *= 1.1 + c.moveY *= 1.1 + } + for { + if c.moveX >= -maxSpeed && c.moveX <= maxSpeed && c.moveY >= -maxSpeed && c.moveY <= maxSpeed { + break + } + + c.moveX *= 0.9 + c.moveY *= 0.9 + } + + c.nextAction = 1440 +} + func (c *gameCreep) doNextAction() { c.queueNextAction() - randMovementA := (rand.Float64() - 0.5) / 7 - randMovementB := (rand.Float64() - 0.5) / 7 + randMovementA := (rand.Float64() - 0.5) / 12 + randMovementB := (rand.Float64() - 0.5) / 12 - dx, dy := deltaXY(c.x, c.y, c.player.x, c.player.y) - seekDistance := 5.0 - maxSpeed := 0.5 / 7 - minSpeed := 0.1 / 7 - if (dx < seekDistance && dy < seekDistance) || rand.Intn(66) == 0 { - // Seek player. - a := angle(c.x, c.y, c.player.x, c.player.y) - c.moveX = -math.Cos(a) - c.moveY = -math.Sin(a) - for { - if (c.moveX < -minSpeed || c.moveX > minSpeed) || (c.moveY < -minSpeed || c.moveY > minSpeed) { - break - } - - c.moveX *= 1.1 - c.moveY *= 1.1 - } - for { - if c.moveX >= -maxSpeed && c.moveX <= maxSpeed && c.moveY >= -maxSpeed && c.moveY <= maxSpeed { - break - } - - c.moveX *= 0.9 - c.moveY *= 0.9 - } + repelled := c.repelled() + if !repelled && rand.Intn(13) == 0 { + c.seekPlayer() } else { c.moveX = randMovementA c.moveY = randMovementB @@ -115,6 +120,14 @@ func (c *gameCreep) doNextAction() { } } +func (c *gameCreep) repelled() bool { + repelled := !c.player.garlicUntil.IsZero() && c.player.garlicUntil.Sub(time.Now()) > 0 + if !c.player.holyWaterUntil.IsZero() && c.player.holyWaterUntil.Sub(time.Now()) > 0 { + repelled = true + } + return repelled +} + func (c *gameCreep) Update() { c.Lock() defer c.Unlock() @@ -124,7 +137,15 @@ func (c *gameCreep) Update() { } c.tick++ - if c.tick >= c.nextAction { + + repelled := c.repelled() + + dx, dy := deltaXY(c.x, c.y, c.player.x, c.player.y) + seekDistance := 2.0 + if !repelled && dx < seekDistance && dy < seekDistance { + c.queueNextAction() + c.seekPlayer() + } else if c.tick >= c.nextAction { c.doNextAction() c.tick = 0 } @@ -137,7 +158,7 @@ func (c *gameCreep) Update() { return } - if !c.player.repelUntil.IsZero() && c.player.repelUntil.Sub(time.Now()) > 0 { + if repelled { dx, dy := deltaXY(c.x, c.y, c.player.x, c.player.y) if dx <= 3 && dy <= 3 { c.runAway() diff --git a/game.go b/game.go index b57cc02..9bb390b 100644 --- a/game.go +++ b/game.go @@ -36,10 +36,11 @@ const ( playerDieVolume = 1.6 munchVolume = 0.8 - spawnVampire = 1000 - spawnGarlic = 13 + spawnVampire = 777 + spawnGarlic = 6 - garlicActiveTime = 7 * time.Second + garlicActiveTime = 7 * time.Second + holyWaterActiveTime = time.Second ) var startButtons = []ebiten.StandardGamepadButton{ @@ -91,11 +92,12 @@ type game struct { ojasSS *PlayerSpriteSheet - heartImg *ebiten.Image - vampireImage1 *ebiten.Image - vampireImage2 *ebiten.Image - vampireImage3 *ebiten.Image - garlicImage *ebiten.Image + heartImg *ebiten.Image + vampireImage1 *ebiten.Image + vampireImage2 *ebiten.Image + vampireImage3 *ebiten.Image + garlicImage *ebiten.Image + holyWaterImage *ebiten.Image overlayImg *ebiten.Image op *ebiten.DrawImageOptions @@ -114,7 +116,11 @@ type game struct { tick int + flashMessageText string + flashMessageUntil time.Time + godMode bool + noclipMode bool debugMode bool cpuProfile *os.File } @@ -157,6 +163,11 @@ func NewGame() (*game, error) { return g, nil } +func (g *game) flashMessage(message string) { + g.flashMessageText = message + g.flashMessageUntil = time.Now().Add(3 * time.Second) +} + func (g *game) loadAssets() error { var err error // Load SpriteSheets. @@ -178,7 +189,6 @@ func (g *game) loadAssets() error { if err != nil { return err } - bulletImage = ebiten.NewImageFromImage(img) f, err = assetsFS.Open("assets/weapons/flash.png") @@ -189,7 +199,6 @@ func (g *game) loadAssets() error { if err != nil { return err } - flashImage = ebiten.NewImageFromImage(img) g.soundBuffer[SoundGunshot] = make([]*audio.Player, 4) @@ -282,9 +291,18 @@ func (g *game) loadAssets() error { if err != nil { return err } - g.garlicImage = ebiten.NewImageFromImage(img) + f, err = assetsFS.Open("assets/items/holywater.png") + if err != nil { + return err + } + img, _, err = image.Decode(f) + if err != nil { + return err + } + g.holyWaterImage = ebiten.NewImageFromImage(img) + f, err = assetsFS.Open("assets/ui/heart.png") if err != nil { return err @@ -293,13 +311,16 @@ func (g *game) loadAssets() error { if err != nil { return err } - g.heartImg = ebiten.NewImageFromImage(img) + return nil } func (g *game) newItem(itemType int) *gameItem { sprite := g.garlicImage + if itemType == itemTypeHolyWater { + sprite = g.holyWaterImage + } x, y := g.level.newSpawnLocation() return &gameItem{ itemType: itemType, @@ -351,6 +372,8 @@ func (g *game) newCreep(creepType int) *gameCreep { } func (g *game) reset() error { + log.Println("Starting a new game") + g.tick = 0 var err error @@ -390,9 +413,25 @@ func (g *game) reset() error { g.level.items = append(g.level.items, c) added[addedItem] = true } + // Spawn starting garlic. + garlicOffsetA := 8 - float64(rand.Intn(16)) + garlicOffsetB := 8 - float64(rand.Intn(16)) + startingGarlicX := g.player.x + 2 + garlicOffsetA + startingGarlicY := g.player.y + 2 + garlicOffsetB + clampX, clampY := g.level.Clamp(startingGarlicX, startingGarlicY) + if clampX != startingGarlicX { + startingGarlicX = g.player.x - 2 - garlicOffsetA + } + if clampY != startingGarlicY { + startingGarlicY = g.player.y - 2 - garlicOffsetB + } + item := g.newItem(itemTypeGarlic) + item.x = startingGarlicX + item.y = startingGarlicY + g.level.items = append(g.level.items, item) // Spawn creeps. - g.level.creeps = make([]*gameCreep, 1000) + g.level.creeps = make([]*gameCreep, spawnVampire) for i := 0; i < spawnVampire; i++ { creepType := TypeVampire c := g.newCreep(creepType) @@ -487,6 +526,7 @@ func (g *game) Update() error { } biteThreshold := 0.75 + liveCreeps := 0 for _, c := range g.level.creeps { if c.health == 0 { continue @@ -497,27 +537,35 @@ func (g *game) Update() error { cx, cy := c.Position() dx, dy := deltaXY(g.player.x, g.player.y, cx, cy) if dx <= biteThreshold && dy <= biteThreshold { - if !g.godMode { - g.player.health-- - } + if !g.godMode && g.player.garlicUntil.Sub(time.Now()) <= 0 && g.player.holyWaterUntil.Sub(time.Now()) <= 0 { + if g.player.holyWaters > 0 { + // TODO g.playSound(SoundItemUseHolyWater, useholyWaterVolume) + g.player.holyWaterUntil = time.Now().Add(holyWaterActiveTime) + g.player.holyWaters-- + } else { + err := g.hurtCreep(c, -1) + if err != nil { + // TODO + panic(err) + } - err := g.hurtCreep(c, -1) - if err != nil { - // TODO - panic(err) - } + g.player.health-- - if g.player.health == 2 { - g.playSound(SoundPlayerHurt, playerHurtVolume/2) - } else if g.player.health == 1 { - g.playSound(SoundPlayerHurt, playerHurtVolume) - } + if g.player.health == 2 { + g.playSound(SoundPlayerHurt, playerHurtVolume/2) + } else if g.player.health == 1 { + g.playSound(SoundPlayerHurt, playerHurtVolume) + } - g.addBloodSplatter(g.player.x, g.player.y) + g.addBloodSplatter(g.player.x, g.player.y) + } + } if g.player.health == 0 { ebiten.SetCursorShape(ebiten.CursorShapeDefault) + g.player.holyWaters = 0 + g.gameOverTime = time.Now() // Play die sound. @@ -531,23 +579,12 @@ func (g *game) Update() error { g.playSound(SoundBat, batDieVolume) g.lastBatSound = time.Now() } - } - // Update target zoom level. - var scrollY float64 - if ebiten.IsKeyPressed(ebiten.KeyC) || ebiten.IsKeyPressed(ebiten.KeyPageDown) { - scrollY = -0.25 - } else if ebiten.IsKeyPressed(ebiten.KeyE) || ebiten.IsKeyPressed(ebiten.KeyPageUp) { - scrollY = .25 - } else { - _, scrollY = ebiten.Wheel() - if scrollY < -1 { - scrollY = -1 - } else if scrollY > 1 { - scrollY = 1 + if c.health > 0 { + liveCreeps++ } } - g.camScaleTo += scrollY * (g.camScaleTo / 7) + g.level.liveCreeps = liveCreeps // Clamp target zoom level. if g.camScaleTo < 2 { @@ -575,9 +612,8 @@ func (g *game) Update() error { g.player.y += v * pan } } else { - // TODO debug only if ebiten.IsKeyPressed(ebiten.KeyShift) { - pan *= 5 + pan /= 2 } if ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA) { @@ -595,7 +631,9 @@ func (g *game) Update() error { } // Clamp camera position. - g.player.x, g.player.y = g.level.Clamp(g.player.x, g.player.y) + if !g.noclipMode { + g.player.x, g.player.y = g.level.Clamp(g.player.x, g.player.y) + } for _, item := range g.level.items { if item.health == 0 { @@ -605,9 +643,15 @@ func (g *game) Update() error { dx, dy := deltaXY(g.player.x, g.player.y, item.x, item.y) if dx <= 1 && dy <= 1 { item.health = 0 - g.playSound(SoundMunch, munchVolume) - g.player.repelUntil = time.Now().Add(garlicActiveTime) g.player.score += item.useScore() + + if item.itemType == itemTypeGarlic { + g.playSound(SoundMunch, munchVolume) + g.player.garlicUntil = time.Now().Add(garlicActiveTime) + } else if item.itemType == itemTypeHolyWater { + // TODO g.playSound(SoundItemPickup, munchVolume) + g.player.holyWaters++ + } } } @@ -701,25 +745,42 @@ UPDATEPROJECTILES: continue } - // Remove projectile + // Remove creep. g.level.creeps = append(g.level.creeps[:i-removed], g.level.creeps[i-removed+1:]...) removed++ } } // Spawn garlic. - if g.tick%144*20 == 0 { + if g.tick%(144*45) == 0 { item := g.newItem(itemTypeGarlic) g.level.items = append(g.level.items, item) + + if g.debugMode { + log.Println("SPAWN GARLIC") + } + } + + // Spawn holy water. + if g.tick%(144*120) == 0 || rand.Intn(660) == 0 { // TODO + item := g.newItem(itemTypeHolyWater) + g.level.items = append(g.level.items, item) + + if g.debugMode { + log.Println("SPAWN HOLY WATER") + } } // Spawn vampires. - if g.tick > 144*5 && g.tick%288 == 0 { - for i := 0; i < g.tick/1440; i++ { - if rand.Intn(2) == 0 { - continue - } - + if g.tick%144 == 0 { + spawnAmount := rand.Intn(26 + (g.tick / (144 * 3))) + if len(g.level.creeps) < 500 { + spawnAmount *= 4 + } + if g.debugMode && spawnAmount > 0 { + log.Printf("SPAWN %d VAMPIRES", spawnAmount) + } + for i := 0; i < spawnAmount; i++ { creepType := TypeVampire c := g.newCreep(creepType) @@ -728,12 +789,18 @@ UPDATEPROJECTILES: } // Spawn bats. - if g.tick > 144*5 && g.tick%144 == 0 { - for i := 0; i < g.tick/1440; i++ { - if rand.Intn(6) == 0 { - continue - } - + if g.tick%144 == 0 { + spawnAmount := g.tick / 288 + if spawnAmount < 1 { + spawnAmount = 1 + } else if spawnAmount > 12 { + spawnAmount = 12 + } + spawnAmount = rand.Intn(spawnAmount) + if g.debugMode && spawnAmount > 0 { + log.Printf("SPAWN %d BATS", spawnAmount) + } + for i := 0; i < spawnAmount; i++ { creepType := TypeBat c := g.newCreep(creepType) @@ -741,16 +808,38 @@ UPDATEPROJECTILES: } } - // TODO debug only - if inpututil.IsKeyJustPressed(ebiten.KeyV) { - g.debugMode = !g.debugMode - } if inpututil.IsKeyJustPressed(ebiten.KeyG) { g.godMode = !g.godMode + if g.godMode { + g.flashMessage("GOD MODE ACTIVATED") + } else { + g.flashMessage("GOD MODE DEACTIVATED") + } + } + if inpututil.IsKeyJustPressed(ebiten.KeyN) { + g.noclipMode = !g.noclipMode + if g.noclipMode { + g.flashMessage("NOCLIP MODE ACTIVATED") + } else { + g.flashMessage("NOCLIP MODE DEACTIVATED") + } + } + if inpututil.IsKeyJustPressed(ebiten.KeyH) { + g.player.holyWaters++ + g.flashMessage("+ HOLY WATER") + } + if inpututil.IsKeyJustPressed(ebiten.KeyV) { + g.debugMode = !g.debugMode + if g.debugMode { + g.flashMessage("DEBUG MODE ACTIVATED") + } else { + g.flashMessage("DEBUG MODE DEACTIVATED") + } } if inpututil.IsKeyJustPressed(ebiten.KeyP) { if g.cpuProfile == nil { log.Println("CPU profiling started...") + g.flashMessage("CPU PROFILING STARTED") homeDir, err := os.UserHomeDir() if err != nil { @@ -765,6 +854,7 @@ UPDATEPROJECTILES: } } else { log.Println("Profiling stopped") + g.flashMessage("CPU PROFILING STOPPED") pprof.StopCPUProfile() g.cpuProfile.Close() @@ -776,13 +866,15 @@ UPDATEPROJECTILES: return nil } -func (g *game) drawText(target *ebiten.Image, y float64, scale float64, text string) { +func (g *game) drawText(target *ebiten.Image, y float64, scale float64, alpha float64, text string) { g.overlayImg.Clear() ebitenutil.DebugPrint(g.overlayImg, text) g.op.GeoM.Reset() g.op.GeoM.Scale(scale, scale) g.op.GeoM.Translate(float64(g.w/2)-(float64(len(text))*3*scale), y) + g.op.ColorM.Scale(1, 1, 1, alpha) target.DrawImage(g.overlayImg, g.op) + g.op.ColorM.Reset() } // Draw draws the game on the screen. @@ -790,14 +882,14 @@ func (g *game) Draw(screen *ebiten.Image) { if g.gameStartTime.IsZero() { screen.Fill(colorBlood) - g.drawText(screen, float64(g.h/2)-350, 16, "CAROTID") - g.drawText(screen, float64(g.h/2)-100, 16, "ARTILLERY") + g.drawText(screen, float64(g.h/2)-350, 16, 1.0, "CAROTID") + g.drawText(screen, float64(g.h/2)-100, 16, 1.0, "ARTILLERY") - g.drawText(screen, float64(g.h-210), 4, "KEYBOARD WASD") - g.drawText(screen, float64(g.h-145), 4, "GAMEPAD RECOMMENDED") + g.drawText(screen, float64(g.h-210), 4, 1.0, "WASD + MOUSE = OK") + g.drawText(screen, float64(g.h-145), 4, 1.0, "FULLSCREEN + GAMEPAD = BEST") if time.Now().UnixMilli()%2000 < 1500 { - g.drawText(screen, float64(g.h-80), 4, "PRESS ANY KEY OR BUTTON TO START") + g.drawText(screen, float64(g.h-80), 4, 1.0, "PRESS ANY KEY OR BUTTON TO START") } return @@ -812,27 +904,39 @@ func (g *game) Draw(screen *ebiten.Image) { // Game over. screen.Fill(colorBlood) - g.drawText(screen, float64(g.h/2)-150, 16, "GAME OVER") + g.drawText(screen, float64(g.h/2)-150, 16, 1.0, "GAME OVER") if time.Since(g.gameOverTime).Milliseconds()%2000 < 1500 { - g.drawText(screen, 8, 4, "PRESS ENTER OR START TO PLAY AGAIN") + g.drawText(screen, 8, 4, 1.0, "PRESS ENTER OR START TO PLAY AGAIN") } } - heartSpace := 64 - heartX := (g.w / 2) - ((heartSpace * g.player.health) / 2) + 16 + heartSpace := 32 + heartX := (g.w / 2) - ((heartSpace * g.player.health) / 2) + 8 for i := 0; i < g.player.health; i++ { g.op.GeoM.Reset() g.op.GeoM.Translate(float64(heartX+(i*heartSpace)), 32) screen.DrawImage(g.heartImg, g.op) } - scoreLabel := numberPrinter.Sprintf("%d", g.player.score) - g.drawText(screen, float64(g.h-150), 8, scoreLabel) + holyWaterSpace := 16 + holyWaterX := (g.w / 2) - ((holyWaterSpace * g.player.holyWaters) / 2) + for i := 0; i < g.player.holyWaters; i++ { + g.op.GeoM.Reset() + g.op.GeoM.Translate(float64(holyWaterX+(i*holyWaterSpace)), 76) + screen.DrawImage(g.holyWaterImage, g.op) + } - if g.godMode { - // Draw God mode indicator. - g.drawText(screen, float64(g.h-40), 2, " GOD") + scoreLabel := numberPrinter.Sprintf("%d", g.player.score) + g.drawText(screen, float64(g.h-150), 8, 1.0, scoreLabel) + + flashTime := g.flashMessageUntil.Sub(time.Now()) + if flashTime > 0 { + alpha := flashTime.Seconds() * 4 + if alpha > 1 { + alpha = 1 + } + g.drawText(screen, float64(g.h-40), 2, alpha, g.flashMessageText) } if !g.debugMode { @@ -841,7 +945,7 @@ func (g *game) Draw(screen *ebiten.Image) { // Print game info. g.overlayImg.Clear() - ebitenutil.DebugPrint(g.overlayImg, fmt.Sprintf("SPR %d\nTPS %0.0f\nFPS %0.0f", drawn, ebiten.CurrentTPS(), ebiten.CurrentFPS())) + ebitenutil.DebugPrint(g.overlayImg, fmt.Sprintf("CRP %d\nSPR %d\nTPS %0.0f\nFPS %0.0f", g.level.liveCreeps, drawn, ebiten.CurrentTPS(), ebiten.CurrentFPS())) g.op.GeoM.Reset() g.op.GeoM.Translate(3, 0) g.op.GeoM.Scale(2, 2) @@ -934,7 +1038,7 @@ func (g *game) renderLevel(screen *ebiten.Image) int { drawn += g.renderSprite(p.x, p.y, 0, 0, p.angle, 1.0, 1.0, bulletImage, screen) } - repelTime := g.player.repelUntil.Sub(time.Now()) + repelTime := g.player.garlicUntil.Sub(time.Now()) if repelTime > 0 && repelTime < 7*time.Second { scale := repelTime.Seconds() + 1 offset := 12 * scale @@ -945,6 +1049,17 @@ func (g *game) renderLevel(screen *ebiten.Image) int { drawn += g.renderSprite(g.player.x+0.25, g.player.y+0.25, -offset, -offset, 0, scale, alpha, g.garlicImage, screen) } + holyWaterTime := g.player.holyWaterUntil.Sub(time.Now()) + if holyWaterTime > 0 && holyWaterTime < time.Second { + scale := (holyWaterTime.Seconds() + 1) * 2 + offset := 16 * scale + alpha := 0.25 + if holyWaterTime.Seconds() < 3 { + alpha = holyWaterTime.Seconds() / 2 + } + drawn += g.renderSprite(g.player.x+0.25, g.player.y+0.25, -offset, -offset, 0, scale, alpha, g.holyWaterImage, screen) + } + playerSprite := g.ojasSS.Frame1 playerAngle := g.player.angle weaponSprite := g.player.weapon.spriteFlipped diff --git a/go.mod b/go.mod index eeed40d..13ca707 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module code.rocketnine.space/tslocum/carotidartillery go 1.17 require ( - github.com/hajimehoshi/ebiten/v2 v2.2.0 + github.com/hajimehoshi/ebiten/v2 v2.2.1 golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d golang.org/x/text v0.3.7 ) @@ -13,8 +13,8 @@ require ( github.com/hajimehoshi/go-mp3 v0.3.2 // indirect github.com/hajimehoshi/oto/v2 v2.1.0-alpha.2 // indirect github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect - golang.org/x/exp v0.0.0-20211011213208-1d87cf485e27 // indirect + golang.org/x/exp v0.0.0-20211012155715-ffe10e552389 // indirect golang.org/x/mobile v0.0.0-20210924032853-1c027f395ef7 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect + golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect ) diff --git a/go.sum b/go.sum index 515d417..ec42f80 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs= -github.com/hajimehoshi/ebiten/v2 v2.2.0 h1:2mP9HrLLqiH9X3MajElYZEjVZU/CGh22iFkjatxhT4w= -github.com/hajimehoshi/ebiten/v2 v2.2.0/go.mod h1:olKl/qqhMBBAm2oI7Zy292nCtE+nitlmYKNF3UpbFn0= +github.com/hajimehoshi/ebiten/v2 v2.2.1 h1:YhITMaBQmnwb4kzAXCCfSkSZmeQX7pfQ7BnC32cnPjc= +github.com/hajimehoshi/ebiten/v2 v2.2.1/go.mod h1:olKl/qqhMBBAm2oI7Zy292nCtE+nitlmYKNF3UpbFn0= github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= github.com/hajimehoshi/go-mp3 v0.3.2 h1:xSYNE2F3lxtOu9BRjCWHHceg7S91IHfXfXp5+LYQI7s= github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= @@ -288,8 +288,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20211011213208-1d87cf485e27 h1:xUVDmMyOkmVK1aEyyBO+z9LzF5UUQJrV12rZ70CvXnU= -golang.org/x/exp v0.0.0-20211011213208-1d87cf485e27/go.mod h1:a3o/VtDNHN+dCVLEpzjjUHOzR+Ln3DHX056ZPzoZGGA= +golang.org/x/exp v0.0.0-20211012155715-ffe10e552389 h1:qFfBYVpJAdBCk6Nmd7ZbcyhGmKmv8fps+OyoOfpjvu8= +golang.org/x/exp v0.0.0-20211012155715-ffe10e552389/go.mod h1:a3o/VtDNHN+dCVLEpzjjUHOzR+Ln3DHX056ZPzoZGGA= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -369,8 +369,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/item.go b/item.go index 177d8cc..e12d20e 100644 --- a/item.go +++ b/item.go @@ -8,6 +8,7 @@ import ( const ( itemTypeGarlic = iota + itemTypeHolyWater ) type gameItem struct { @@ -26,5 +27,12 @@ type gameItem struct { } func (item *gameItem) useScore() int { - return 275 + switch item.itemType { + case itemTypeGarlic: + return 275 + case itemTypeHolyWater: + return 150 + default: + return 0 + } } diff --git a/level.go b/level.go index 951dd28..4a04675 100644 --- a/level.go +++ b/level.go @@ -16,7 +16,8 @@ type Level struct { items []*gameItem - creeps []*gameCreep + creeps []*gameCreep + liveCreeps int player *gamePlayer } @@ -51,17 +52,17 @@ func (l *Level) Clamp(x, y float64) (float64, float64) { func (l *Level) newSpawnLocation() (float64, float64) { SPAWNLOCATION: for { - x := float64(rand.Intn(108)) - y := float64(rand.Intn(108)) + x := float64(1 + rand.Intn(l.w-2)) + y := float64(1 + rand.Intn(l.h-2)) // Too close to player. - playerSafeSpace := 7.0 + playerSafeSpace := 18.0 dx, dy := deltaXY(x, y, l.player.x, l.player.y) if dx <= playerSafeSpace && dy <= playerSafeSpace { continue } - // Too close to garlic. + // Too close to garlic or holy water. garlicSafeSpace := 2.0 for _, item := range l.items { if item.health == 0 { diff --git a/player.go b/player.go index 79f2601..803dbde 100644 --- a/player.go +++ b/player.go @@ -18,7 +18,10 @@ type gamePlayer struct { health int - repelUntil time.Time + holyWaters int + + garlicUntil time.Time + holyWaterUntil time.Time } func NewPlayer() (*gamePlayer, error) {