Add holy water

This commit is contained in:
Trevor Slocum 2021-10-19 18:46:11 -07:00
parent 45799afc2f
commit 05cda8379b
9 changed files with 297 additions and 129 deletions

View File

@ -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).

BIN
assets/items/holywater.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

View File

@ -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()

277
game.go
View File

@ -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

6
go.mod
View File

@ -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
)

12
go.sum
View File

@ -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=

10
item.go
View File

@ -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
}
}

View File

@ -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 {

View File

@ -18,7 +18,10 @@ type gamePlayer struct {
health int
repelUntil time.Time
holyWaters int
garlicUntil time.Time
holyWaterUntil time.Time
}
func NewPlayer() (*gamePlayer, error) {