diff --git a/creep.go b/creep.go index c4f550e..59a1b68 100644 --- a/creep.go +++ b/creep.go @@ -151,12 +151,11 @@ func (c *gameCreep) Update() { } x, y := c.x+c.moveX, c.y+c.moveY - clampX, clampY := c.level.Clamp(x, y) - c.x, c.y = clampX, clampY - if clampX != x || clampY != y { + if !c.level.isFloor(x, y, false) { c.nextAction = 0 return } + c.x, c.y = x, y if repelled { dx, dy := deltaXY(c.x, c.y, c.player.x, c.player.y) diff --git a/game.go b/game.go index 9bb390b..cab7689 100644 --- a/game.go +++ b/game.go @@ -41,6 +41,8 @@ const ( garlicActiveTime = 7 * time.Second holyWaterActiveTime = time.Second + + maxCreeps = 3000 // TODO optimize and raise ) var startButtons = []ebiten.StandardGamepadButton{ @@ -390,8 +392,14 @@ func (g *game) reset() error { g.player.health = 3 // Position player. - g.player.x = float64(rand.Intn(108)) - g.player.y = float64(rand.Intn(108)) + for { + g.player.x = float64(rand.Intn(108)) + g.player.y = float64(rand.Intn(108)) + + if g.level.isFloor(g.player.x, g.player.y, false) { + break + } + } // Remove projectiles. g.projectiles = nil @@ -414,20 +422,19 @@ func (g *game) reset() error { 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 + for { + garlicOffsetA := 8 - float64(rand.Intn(16)) + garlicOffsetB := 8 - float64(rand.Intn(16)) + startingGarlicX := g.player.x + 2 + garlicOffsetA + startingGarlicY := g.player.y + 2 + garlicOffsetB + + if g.level.isFloor(startingGarlicX, startingGarlicY, false) { + item.x = startingGarlicX + item.y = startingGarlicY + break + } + } g.level.items = append(g.level.items, item) // Spawn creeps. @@ -587,11 +594,11 @@ func (g *game) Update() error { g.level.liveCreeps = liveCreeps // Clamp target zoom level. - if g.camScaleTo < 2 { + /*if g.camScaleTo < 2 { g.camScaleTo = 2 } else if g.camScaleTo > 4 { g.camScaleTo = 4 - } + } TODO */ // Smooth zoom transition. div := 10.0 @@ -604,12 +611,13 @@ func (g *game) Update() error { pan := 0.05 // Pan camera. + px, py := g.player.x, g.player.y if g.activeGamepad != -1 { h := ebiten.StandardGamepadAxisValue(g.activeGamepad, ebiten.StandardGamepadAxisLeftStickHorizontal) v := ebiten.StandardGamepadAxisValue(g.activeGamepad, ebiten.StandardGamepadAxisLeftStickVertical) if v < -gamepadDeadZone || v > gamepadDeadZone || h < -gamepadDeadZone || h > gamepadDeadZone { - g.player.x += h * pan - g.player.y += v * pan + px += h * pan + py += v * pan } } else { if ebiten.IsKeyPressed(ebiten.KeyShift) { @@ -617,22 +625,25 @@ func (g *game) Update() error { } if ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA) { - g.player.x -= pan + px -= pan } if ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyD) { - g.player.x += pan + px += pan } if ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.IsKeyPressed(ebiten.KeyS) { - g.player.y += pan + py += pan } if ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.IsKeyPressed(ebiten.KeyW) { - g.player.y -= pan + py -= pan } } - // Clamp camera position. - if !g.noclipMode { - g.player.x, g.player.y = g.level.Clamp(g.player.x, g.player.y) + if g.noclipMode || g.level.isFloor(px, py, false) { + g.player.x, g.player.y = px, py + } else if g.level.isFloor(px, g.player.y, false) { + g.player.x = px + } else if g.level.isFloor(g.player.x, py, false) { + g.player.y = py } for _, item := range g.level.items { @@ -709,11 +720,11 @@ UPDATEPROJECTILES: continue UPDATEPROJECTILES } - clampX, clampY := g.level.Clamp(p.x, p.y) - if clampX != p.x || clampY != p.y { + if !g.level.isFloor(p.x, p.y, true) { // Remove projectile g.projectiles = append(g.projectiles[:i-removed], g.projectiles[i-removed+1:]...) removed++ + // TODO Add bullet hole } } @@ -771,40 +782,42 @@ UPDATEPROJECTILES: } } - // Spawn vampires. - 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) + if len(g.level.creeps) < maxCreeps { + // Spawn vampires. + 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) - g.level.creeps = append(g.level.creeps, c) + g.level.creeps = append(g.level.creeps, c) + } } - } - // Spawn bats. - 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) + // Spawn bats. + 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) - g.level.creeps = append(g.level.creeps, c) + g.level.creeps = append(g.level.creeps, c) + } } } diff --git a/go.mod b/go.mod index 13ca707..6b9e1ec 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module code.rocketnine.space/tslocum/carotidartillery go 1.17 require ( + github.com/Meshiest/go-dungeon v0.0.0-20160809210039-1d1d1e7596b8 github.com/hajimehoshi/ebiten/v2 v2.2.1 golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d golang.org/x/text v0.3.7 diff --git a/go.sum b/go.sum index ec42f80..e422cfd 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Meshiest/go-dungeon v0.0.0-20160809210039-1d1d1e7596b8 h1:nL4v6/vh9uBtMk4mZWhbdUOgisnpODxFjT2/h951GGU= +github.com/Meshiest/go-dungeon v0.0.0-20160809210039-1d1d1e7596b8/go.mod h1:mQT0Ddn2ir5O4dIO3KS8EUKZZjiFO66nIF46cl9vrLs= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= diff --git a/level.go b/level.go index 4a04675..898f941 100644 --- a/level.go +++ b/level.go @@ -4,9 +4,12 @@ import ( "fmt" "math" "math/rand" - "time" + + "github.com/Meshiest/go-dungeon/dungeon" ) +const dungeonScale = 4 + // Level represents a game level. type Level struct { w, h int @@ -35,18 +38,27 @@ func (l *Level) Size() (width, height int) { return l.w, l.h } -func (l *Level) Clamp(x, y float64) (float64, float64) { - if x < 0.3 { - x = 0.3 - } else if x > float64(l.w)-1.3 { - x = float64(l.w) - 1.3 +func (l *Level) isFloor(x float64, y float64, exact bool) bool { + offsetA := .25 + offsetB := .75 + if exact { + offsetA = 0 + offsetB = 0 } - if y < 0.4 { - y = 0.4 - } else if y > float64(l.h)-1.8 { - y = float64(l.h) - 1.8 + + t := l.Tile(int(x+offsetA), int(y+offsetA)) + if t == nil { + return false } - return x, y + if !t.floor { + return false + } + + t = l.Tile(int(x+offsetB), int(y+offsetB)) + if t == nil { + return false + } + return t.floor } func (l *Level) newSpawnLocation() (float64, float64) { @@ -55,6 +67,10 @@ SPAWNLOCATION: x := float64(1 + rand.Intn(l.w-2)) y := float64(1 + rand.Intn(l.h-2)) + if !l.isFloor(x, y, false) { + continue + } + // Too close to player. playerSafeSpace := 18.0 dx, dy := deltaXY(x, y, l.player.x, l.player.y) @@ -82,10 +98,10 @@ SPAWNLOCATION: // NewLevel returns a new randomly generated Level. func NewLevel() (*Level, error) { - // Create a 108x108 Level. + // Create a 216x216 Level. l := &Level{ - w: 108, - h: 108, + w: 216, + h: 216, tileSize: 32, } @@ -94,50 +110,111 @@ func NewLevel() (*Level, error) { return nil, fmt.Errorf("failed to load embedded spritesheet: %s", err) } - _ = sandstoneSS - - // Generate a unique permutation each time. - r := rand.New(rand.NewSource(time.Now().UTC().UnixNano())) - - // Fill each tile with one or more sprites randomly. + dungeon := dungeon.NewDungeon(l.w/dungeonScale, 13) + dungeonFloor := 1 l.tiles = make([][]*Tile, l.h) for y := 0; y < l.h; y++ { l.tiles[y] = make([]*Tile, l.w) for x := 0; x < l.w; x++ { t := &Tile{} - t.AddSprite(sandstoneSS.FloorA) - - val := r.Intn(1000) - switch { - case x == 0 && y == 0: - t.AddSprite(sandstoneSS.WallTopLeft) - case x == l.w-1 && y == 0: - t.AddSprite(sandstoneSS.WallTopRight) - case x == 0 && y == l.h-1: - t.AddSprite(sandstoneSS.WallBottom) - case x == l.w-1 && y == l.h-1: - t.AddSprite(sandstoneSS.WallBottom) - case y == 0: - if x%(l.w/7) == 1 { - t.AddSprite(sandstoneSS.WallPillar) + if y < l.h-1 && dungeon.Grid[x/dungeonScale][y/dungeonScale] == dungeonFloor { + if rand.Intn(13) == 0 { + t.AddSprite(sandstoneSS.FloorC) } else { - t.AddSprite(sandstoneSS.WallTop) + t.AddSprite(sandstoneSS.FloorA) } - case y == l.h-1: - t.AddSprite(sandstoneSS.WallBottom) - case x == 0: - t.AddSprite(sandstoneSS.WallLeft) - case x == l.w-1: - t.AddSprite(sandstoneSS.WallRight) - case val < 275: - //t.AddSprite(sandstoneSS.FloorB) - case val < 500: - t.AddSprite(sandstoneSS.FloorC) + t.floor = true } l.tiles[y][x] = t } } + neighbors := func(x, y int) [][2]int { + return [][2]int{ + {x - 1, y - 1}, + {x, y - 1}, + {x + 1, y - 1}, + {x + 1, y}, + {x + 1, y + 1}, + {x, y + 1}, + {x - 1, y + 1}, + {x - 1, y}, + } + } + + floorTile := func(x, y int) bool { + t := l.Tile(x, y) + if t == nil { + return false + } + return t.floor + } + + // Add walls. + for x := 0; x < l.w; x++ { + for y := 0; y < l.h; y++ { + t := l.Tile(x, y) + if t == nil { + continue + } + if !t.floor { + continue + } + for _, n := range neighbors(x, y) { + nx, ny := n[0], n[1] + neighbor := l.Tile(nx, ny) + if neighbor == nil || neighbor.floor || neighbor.wall { + continue + } + neighbor.wall = true + + // From perspective of neighbor tile. + bottom := floorTile(nx, ny+1) + top := floorTile(nx, ny-1) + right := floorTile(nx+1, ny) + left := floorTile(nx-1, ny) + topLeft := floorTile(nx-1, ny-1) + topRight := floorTile(nx+1, ny-1) + bottomLeft := floorTile(nx-1, ny+1) + bottomRight := floorTile(nx+1, ny+1) + + // Determine which wall sprite to belongs here. + spriteTop := !top && bottom + spriteLeft := (left || bottomLeft) && !right && !bottomRight && !bottom + spriteRight := (right || bottomRight) && !left && !bottomLeft && !bottom + spriteBottomRight := !topLeft && !top && topRight && !bottomLeft && !bottom && !bottomRight + spriteBottomLeft := topLeft && !top && !topRight && !bottomLeft && !bottom && !bottomRight + spriteBottom := top && !bottom + + // Add wall sprite. + switch { + case spriteTop: + if !bottomLeft || !bottomRight || left || right { + neighbor.AddSprite(sandstoneSS.WallPillar) + } else { + neighbor.AddSprite(sandstoneSS.WallTop) + } + case spriteLeft: + if spriteBottom { + neighbor.AddSprite(sandstoneSS.WallBottom) + } + neighbor.AddSprite(sandstoneSS.WallLeft) + case spriteRight: + if spriteBottom { + neighbor.AddSprite(sandstoneSS.WallBottom) + } + neighbor.AddSprite(sandstoneSS.WallRight) + case spriteBottomLeft: + neighbor.AddSprite(sandstoneSS.WallBottomLeft) + case spriteBottomRight: + neighbor.AddSprite(sandstoneSS.WallBottomRight) + case spriteBottom: + neighbor.AddSprite(sandstoneSS.WallBottom) + } + } + } + } + return l, nil } diff --git a/main.go b/main.go index 1015489..c4529d9 100644 --- a/main.go +++ b/main.go @@ -3,13 +3,12 @@ package main import ( "log" "math/rand" + _ "net/http/pprof" "os" "os/signal" "syscall" "time" - _ "net/http/pprof" - "github.com/hajimehoshi/ebiten/v2" ) diff --git a/tile.go b/tile.go index bc28efe..e128438 100644 --- a/tile.go +++ b/tile.go @@ -8,6 +8,8 @@ import ( // sprites may be added to a Tile. type Tile struct { sprites []*ebiten.Image + floor bool + wall bool } // AddSprite adds a sprite to the Tile.