Generate dungeon layout

This commit is contained in:
Trevor Slocum 2021-10-21 18:07:26 -07:00
parent 05cda8379b
commit e4cab469d8
7 changed files with 202 additions and 109 deletions

View File

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

129
game.go
View File

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

1
go.mod
View File

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

2
go.sum
View File

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

169
level.go
View File

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

View File

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

View File

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