Add pathfinding

This commit is contained in:
Miroslav Vasilev 2024-05-12 23:22:39 +03:00
parent 3b9923a713
commit 28cf513b6d
20 changed files with 920 additions and 132 deletions

40
engine/path.go Normal file
View file

@ -0,0 +1,40 @@
package engine
type Path struct {
from Position
to Position
path []Position
currentPos int
}
func CreatePath(from, to Position, path []Position) *Path {
return &Path{
from: from,
to: to,
path: path,
currentPos: 0,
}
}
func (p *Path) From() Position {
return p.from
}
func (p *Path) To() Position {
return p.to
}
func (p *Path) CurrentPosition() Position {
return p.path[p.currentPos]
}
func (p *Path) Next() (current Position, hasNext bool) {
if p.currentPos+1 >= len(p.path) {
return p.CurrentPosition(), false
}
p.currentPos++
return p.CurrentPosition(), true
}

131
engine/pathfinding.go Normal file
View file

@ -0,0 +1,131 @@
package engine
import (
"math"
"slices"
)
type pathNode struct {
pos Position
parent *pathNode
g float64 // distance between current node and start node
h float64 // heuristic - squared distance from current node to end node
f float64 // total cost of this node
}
func FindPath(from Position, to Position, maxDistance int, isPassable func(x, y int) bool) *Path {
maxDistanceSquared := math.Pow(float64(maxDistance), 2)
var openList = make([]*pathNode, 0)
var closedList = make([]*pathNode, 0)
openList = append(openList, &pathNode{
pos: from,
parent: nil,
g: 0,
h: 0,
f: 0,
})
var lastNode *pathNode
for {
if len(openList) == 0 {
break
}
// find node in open list with lowest f value, remove it from open and move it to closed
currentIndex := 0
currentNode := openList[currentIndex]
for i, node := range openList {
if node.f < currentNode.f {
currentNode = node
currentIndex = i
}
}
if currentNode.pos.Equals(to) {
lastNode = currentNode
break // We have reached the goal
}
openList = slices.Delete(openList, currentIndex, currentIndex+1)
closedList = append(closedList, currentNode)
// use adjacent nodes as children
children := []*pathNode{
{
pos: currentNode.pos.WithOffset(1, 0),
parent: currentNode,
},
{
pos: currentNode.pos.WithOffset(-1, 0),
parent: currentNode,
},
{
pos: currentNode.pos.WithOffset(0, 1),
parent: currentNode,
},
{
pos: currentNode.pos.WithOffset(0, -1),
parent: currentNode,
},
}
for _, child := range children {
// If the child is impassable, skip it
if !isPassable(child.pos.XY()) {
continue
}
// If child is already contained in closedList, skip it
if slices.ContainsFunc(closedList, func(el *pathNode) bool { return el.pos.Equals(child.pos) }) {
continue
}
child.g = currentNode.g + 1
if child.g > maxDistanceSquared {
continue
}
child.h = to.DistanceSquared(child.pos)
child.f = float64(child.g) + child.f
// If child is already contained in openList, and has lower g
if slices.ContainsFunc(openList, func(el *pathNode) bool { return el.pos.Equals(child.pos) && child.g > el.g }) {
continue
}
openList = append(openList, child)
}
}
if lastNode == nil {
return nil
}
node := lastNode
path := make([]Position, 0)
for {
path = append(path, node.pos)
if node.parent == nil {
break
}
node = node.parent
}
slices.Reverse(path)
return CreatePath(
from, to,
path,
)
}

View file

@ -1,6 +1,9 @@
package engine package engine
import "math/rand" import (
"math"
"math/rand"
)
type Positioned struct { type Positioned struct {
pos Position pos Position
@ -16,6 +19,10 @@ func (wp *Positioned) Position() Position {
return wp.pos return wp.pos
} }
func (wp *Positioned) SetPosition(pos Position) {
wp.pos = pos
}
type Position struct { type Position struct {
x int x int
y int y int
@ -37,6 +44,18 @@ func (p Position) XY() (int, int) {
return p.x, p.y return p.x, p.y
} }
func (p Position) DistanceSquared(pos Position) float64 {
return float64((pos.x-p.x)*(pos.x-p.x) + (pos.y-p.y)*(pos.y-p.y))
}
func (p Position) Distance(pos Position) float64 {
return math.Sqrt(p.DistanceSquared(pos))
}
func (p Position) Equals(other Position) bool {
return p.x == other.x && p.y == other.y
}
func (p Position) WithOffset(xOffset int, yOffset int) Position { func (p Position) WithOffset(xOffset int, yOffset int) Position {
p.x = p.x + xOffset p.x = p.x + xOffset
p.y = p.y + yOffset p.y = p.y + yOffset
@ -91,6 +110,10 @@ func (s Size) AsArrayIndex(x, y int) int {
return y*s.width + x return y*s.width + x
} }
func (s Size) Contains(x, y int) bool {
return 0 <= x && x < s.width && 0 <= y && y < s.height
}
func LimitIncrement(i int, limit int) int { func LimitIncrement(i int, limit int) int {
if (i + 1) > limit { if (i + 1) > limit {
return i return i
@ -110,3 +133,13 @@ func LimitDecrement(i int, limit int) int {
func RandInt(min, max int) int { func RandInt(min, max int) int {
return min + rand.Intn(max-min) return min + rand.Intn(max-min)
} }
func MapSlice[S ~[]E, E any, R any](slice S, mappingFunc func(e E) R) []R {
newSlice := make([]R, 0, len(slice))
for _, el := range slice {
newSlice = append(newSlice, mappingFunc(el))
}
return newSlice
}

View file

@ -1,4 +1,4 @@
package model package logic
import ( import (
"mvvasilev/last_light/game/model" "mvvasilev/last_light/game/model"

39
game/model/npc.go Normal file
View file

@ -0,0 +1,39 @@
package model
import (
"mvvasilev/last_light/engine"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
)
type NPC struct {
id uuid.UUID
engine.Positioned
}
func CreateNPC(pos engine.Position) *NPC {
return &NPC{
id: uuid.New(),
Positioned: engine.WithPosition(pos),
}
}
func (c *NPC) Position() engine.Position {
return c.Positioned.Position()
}
func (c *NPC) MoveTo(newPosition engine.Position) {
c.Positioned.SetPosition(newPosition)
}
func (c *NPC) UniqueId() uuid.UUID {
return c.id
}
func (c *NPC) Input(e *tcell.EventKey) {
}
func (c *NPC) Tick(dt int64) {
}

View file

@ -0,0 +1,52 @@
package state
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2"
)
type DialogState struct {
prevState GameState
dialog *ui.UIDialog
selectDialog bool
returnToPreviousState bool
}
func CreateDialogState(dialog *ui.UIDialog, prevState GameState) *DialogState {
return &DialogState{
prevState: prevState,
dialog: dialog,
returnToPreviousState: false,
}
}
func (ds *DialogState) OnInput(e *tcell.EventKey) {
if e.Key() == tcell.KeyEnter {
ds.selectDialog = true
return
}
ds.dialog.Input(e)
}
func (ds *DialogState) OnTick(dt int64) GameState {
if ds.selectDialog {
ds.selectDialog = false
ds.returnToPreviousState = true
ds.dialog.Select()
}
if ds.returnToPreviousState {
return ds.prevState
}
return ds
}
func (ds *DialogState) CollectDrawables() []engine.Drawable {
return append(ds.prevState.CollectDrawables(), ds.dialog)
}

View file

@ -3,6 +3,7 @@ package state
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model" "mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/ui"
"mvvasilev/last_light/game/world" "mvvasilev/last_light/game/world"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -11,8 +12,9 @@ import (
type PlayingState struct { type PlayingState struct {
player *model.Player player *model.Player
entityMap *world.EntityMap someNPC *model.NPC
level *world.MultilevelMap
dungeon *world.Dungeon
viewport *engine.Viewport viewport *engine.Viewport
@ -20,6 +22,10 @@ type PlayingState struct {
pauseGame bool pauseGame bool
openInventory bool openInventory bool
pickUpUnderPlayer bool pickUpUnderPlayer bool
interact bool
moveEntities bool
nextGameState GameState
} }
func BeginPlayingState() *PlayingState { func BeginPlayingState() *PlayingState {
@ -27,46 +33,24 @@ func BeginPlayingState() *PlayingState {
mapSize := engine.SizeOf(128, 128) mapSize := engine.SizeOf(128, 128)
dungeonLevel := world.CreateBSPDungeonMap(mapSize.Width(), mapSize.Height(), 4) s.dungeon = world.CreateDungeon(mapSize.Width(), mapSize.Height(), 1)
genTable := make(map[float32]*model.ItemType, 0) s.player = model.CreatePlayer(s.dungeon.CurrentLevel().PlayerSpawnPoint().XY())
genTable[0.2] = model.ItemTypeFish() s.someNPC = model.CreateNPC(s.dungeon.CurrentLevel().NextLevelStaircase())
genTable[0.05] = model.ItemTypeBow()
genTable[0.051] = model.ItemTypeLongsword()
genTable[0.052] = model.ItemTypeKey()
itemTiles := world.SpawnItems(dungeonLevel.Rooms(), 0.01, genTable) s.dungeon.CurrentLevel().AddEntity(s.player, '@', tcell.StyleDefault)
s.dungeon.CurrentLevel().AddEntity(s.someNPC, 'N', tcell.StyleDefault)
itemLevel := world.CreateEmptyDungeonLevel(mapSize.Width(), mapSize.Height())
for _, it := range itemTiles {
if !dungeonLevel.TileAt(it.Position().XY()).Passable() {
continue
}
itemLevel.SetTileAt(it.Position().X(), it.Position().Y(), it)
}
s.player = model.CreatePlayer(dungeonLevel.PlayerSpawnPoint().XY())
s.entityMap = world.CreateEntityMap(mapSize.WH())
s.level = world.CreateMultilevelMap(
dungeonLevel,
itemLevel,
s.entityMap,
)
s.entityMap.AddEntity(s.player, '@', tcell.StyleDefault)
s.viewport = engine.CreateViewport( s.viewport = engine.CreateViewport(
engine.PositionAt(0, 0), engine.PositionAt(0, 0),
dungeonLevel.PlayerSpawnPoint(), s.dungeon.CurrentLevel().PlayerSpawnPoint(),
engine.SizeOf(80, 24), engine.SizeOf(80, 24),
tcell.StyleDefault, tcell.StyleDefault,
) )
s.nextGameState = s
return s return s
} }
@ -88,36 +72,143 @@ func (ps *PlayingState) MovePlayer() {
} }
newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(ps.movePlayerDirection)) newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(ps.movePlayerDirection))
tileAtMovePos := ps.level.TileAt(newPlayerPos.XY())
if tileAtMovePos.Passable() { if ps.dungeon.CurrentLevel().IsTilePassable(newPlayerPos.XY()) {
dx, dy := model.MovementDirectionOffset(ps.movePlayerDirection) dx, dy := model.MovementDirectionOffset(ps.movePlayerDirection)
ps.entityMap.MoveEntity(ps.player.UniqueId(), dx, dy) ps.dungeon.CurrentLevel().MoveEntity(ps.player.UniqueId(), dx, dy)
ps.viewport.SetCenter(ps.player.Position()) ps.viewport.SetCenter(ps.player.Position())
} }
ps.movePlayerDirection = model.DirectionNone ps.movePlayerDirection = model.DirectionNone
} }
func (ps *PlayingState) InteractBelowPlayer() {
playerPos := ps.player.Position()
if playerPos == ps.dungeon.CurrentLevel().NextLevelStaircase() {
ps.SwitchToNextLevel()
return
}
if playerPos == ps.dungeon.CurrentLevel().PreviousLevelStaircase() {
ps.SwitchToPreviousLevel()
return
}
}
func (ps *PlayingState) SwitchToNextLevel() {
if !ps.dungeon.HasNextLevel() {
ps.nextGameState = CreateDialogState(
ui.CreateOkDialog(
"The Unknown Depths",
"The staircases descent down to the lower levels is seemingly blocked by multiple large boulders. They appear immovable.",
"Continue",
40,
func() {
ps.nextGameState = ps
},
),
ps,
)
return
}
ps.dungeon.CurrentLevel().DropEntity(ps.player.UniqueId())
ps.dungeon.MoveToNextLevel()
ps.player.MoveTo(ps.dungeon.CurrentLevel().PlayerSpawnPoint())
ps.viewport = engine.CreateViewport(
engine.PositionAt(0, 0),
ps.dungeon.CurrentLevel().PlayerSpawnPoint(),
engine.SizeOf(80, 24),
tcell.StyleDefault,
)
ps.dungeon.CurrentLevel().AddEntity(ps.player, '@', tcell.StyleDefault)
}
func (ps *PlayingState) SwitchToPreviousLevel() {
if !ps.dungeon.HasPreviousLevel() {
ps.nextGameState = CreateDialogState(
ui.CreateOkDialog(
"The Surface",
"You feel the gentle, yet chilling breeze of the surface make its way through the weaving cavern tunnels, the very same you had to make your way through to get where you are. There is nothing above that you need. Find the last light, or die trying.",
"Continue",
40,
func() {
ps.nextGameState = ps
},
),
ps,
)
return
}
ps.dungeon.CurrentLevel().DropEntity(ps.player.UniqueId())
ps.dungeon.MoveToPreviousLevel()
ps.player.MoveTo(ps.dungeon.CurrentLevel().NextLevelStaircase())
ps.viewport = engine.CreateViewport(
engine.PositionAt(0, 0),
ps.dungeon.CurrentLevel().NextLevelStaircase(),
engine.SizeOf(80, 24),
tcell.StyleDefault,
)
ps.dungeon.CurrentLevel().AddEntity(ps.player, '@', tcell.StyleDefault)
}
func (ps *PlayingState) PickUpItemUnderPlayer() { func (ps *PlayingState) PickUpItemUnderPlayer() {
pos := ps.player.Position() pos := ps.player.Position()
tile := ps.level.TileAtHeight(pos.X(), pos.Y(), 1) item := ps.dungeon.CurrentLevel().RemoveItemAt(pos.XY())
itemTile, ok := tile.(*world.ItemTile) if item == nil {
if !ok {
return return
} }
item := model.CreateItem(itemTile.Type(), itemTile.Quantity()) success := ps.player.Inventory().Push(*item)
success := ps.player.Inventory().Push(item)
if !success { if !success {
ps.dungeon.CurrentLevel().SetItemAt(pos.X(), pos.Y(), *item)
}
}
func (ps *PlayingState) CalcPathToPlayerAndMove() {
distanceToPlayer := ps.someNPC.Position().Distance(ps.player.Position())
if distanceToPlayer > 16 {
return return
} }
ps.level.SetTileAtHeight(pos.X(), pos.Y(), 1, nil) pathToPlayer := engine.FindPath(
ps.someNPC.Position(),
ps.player.Position(),
16,
func(x, y int) bool {
if x == ps.player.Position().X() && y == ps.player.Position().Y() {
return true
}
return ps.dungeon.CurrentLevel().IsTilePassable(x, y)
},
)
nextPos, hasNext := pathToPlayer.Next()
if !hasNext {
return
}
if nextPos.Equals(ps.player.Position()) {
return
}
ps.dungeon.CurrentLevel().MoveEntityTo(ps.someNPC.UniqueId(), nextPos.X(), nextPos.Y())
} }
func (ps *PlayingState) OnInput(e *tcell.EventKey) { func (ps *PlayingState) OnInput(e *tcell.EventKey) {
@ -138,15 +229,24 @@ func (ps *PlayingState) OnInput(e *tcell.EventKey) {
return return
} }
if e.Key() == tcell.KeyRune && e.Rune() == 'e' {
ps.interact = true
return
}
switch e.Key() { switch e.Key() {
case tcell.KeyUp: case tcell.KeyUp:
ps.movePlayerDirection = model.DirectionUp ps.movePlayerDirection = model.DirectionUp
ps.moveEntities = true
case tcell.KeyDown: case tcell.KeyDown:
ps.movePlayerDirection = model.DirectionDown ps.movePlayerDirection = model.DirectionDown
ps.moveEntities = true
case tcell.KeyLeft: case tcell.KeyLeft:
ps.movePlayerDirection = model.DirectionLeft ps.movePlayerDirection = model.DirectionLeft
ps.moveEntities = true
case tcell.KeyRight: case tcell.KeyRight:
ps.movePlayerDirection = model.DirectionRight ps.movePlayerDirection = model.DirectionRight
ps.moveEntities = true
} }
} }
@ -171,13 +271,23 @@ func (ps *PlayingState) OnTick(dt int64) GameState {
ps.PickUpItemUnderPlayer() ps.PickUpItemUnderPlayer()
} }
return ps if ps.interact {
ps.interact = false
ps.InteractBelowPlayer()
}
if ps.moveEntities {
ps.moveEntities = false
ps.CalcPathToPlayerAndMove()
}
return ps.nextGameState
} }
func (ps *PlayingState) CollectDrawables() []engine.Drawable { func (ps *PlayingState) CollectDrawables() []engine.Drawable {
return engine.Multidraw(engine.CreateDrawingInstructions(func(v views.View) { return engine.Multidraw(engine.CreateDrawingInstructions(func(v views.View) {
ps.viewport.DrawFromProvider(v, func(x, y int) (rune, tcell.Style) { ps.viewport.DrawFromProvider(v, func(x, y int) (rune, tcell.Style) {
tile := ps.level.TileAt(x, y) tile := ps.dungeon.CurrentLevel().TileAt(x, y)
if tile != nil { if tile != nil {
return tile.Presentation() return tile.Presentation()

View file

@ -1,61 +0,0 @@
package ui
import (
"mvvasilev/last_light/engine"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
type UIBorderedButton struct {
id uuid.UUID
text engine.Text
border engine.Rectangle
isSelected bool
unselectedStyle tcell.Style
selectedStyle tcell.Style
}
func (b *UIBorderedButton) IsSelected() bool {
return b.isSelected
}
func (b *UIBorderedButton) Select() {
b.isSelected = true
}
func (b *UIBorderedButton) Deselect() {
b.isSelected = false
}
func (b *UIBorderedButton) SetSelected(selected bool) {
b.isSelected = selected
}
func (b *UIBorderedButton) UniqueId() uuid.UUID {
return b.id
}
func (b *UIBorderedButton) MoveTo(x int, y int) {
panic("not implemented") // TODO: Implement
}
func (b *UIBorderedButton) Position() engine.Position {
panic("not implemented") // TODO: Implement
}
func (b *UIBorderedButton) Size() engine.Size {
panic("not implemented") // TODO: Implement
}
func (b *UIBorderedButton) Draw(v views.View) {
panic("not implemented") // TODO: Implement
}
func (b *UIBorderedButton) Input(e *tcell.EventKey) {
panic("not implemented") // TODO: Implement
}

123
game/ui/dialog.go Normal file
View file

@ -0,0 +1,123 @@
package ui
import (
"mvvasilev/last_light/engine"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
type UIDialog struct {
window *UIWindow
prompt *UILabel
yesBtn *UISimpleButton
noBtn *UISimpleButton
}
func CreateYesNoDialog(title string, prompt string, yesText string, noText string, lineWidth int, yesAction func(), noAction func()) *UIDialog {
d := new(UIDialog)
numLines := len(prompt) / lineWidth
winWidth := lineWidth + 2
winHeight := numLines + 2
winX := engine.TERMINAL_SIZE_WIDTH - winWidth/2
winY := engine.TERMINAL_SIZE_HEIGHT - winHeight/2
d.window = CreateWindow(winX, winY, winWidth, winHeight, title, tcell.StyleDefault)
d.prompt = CreateUILabel(winX+1, winY+1, lineWidth, numLines, prompt, tcell.StyleDefault)
yesBtnLength := len(yesText) + 4
noBtnLength := len(noText) + 4
yesBtnPosX := winX + winWidth/4 - yesBtnLength/2
d.yesBtn = CreateSimpleButton(yesBtnPosX, winY+winHeight-1, yesText, tcell.StyleDefault, tcell.StyleDefault.Attributes(tcell.AttrBold), yesAction)
noBtnPosX := winX + 3*winWidth/4 - noBtnLength/2
d.noBtn = CreateSimpleButton(noBtnPosX, winY+winHeight-2, noText, tcell.StyleDefault, tcell.StyleDefault.Attributes(tcell.AttrBold), noAction)
d.yesBtn.Highlight()
return d
}
func CreateOkDialog(title string, prompt string, okText string, lineWidth int, okAction func()) *UIDialog {
d := new(UIDialog)
numLines := len(prompt) / lineWidth
winWidth := lineWidth + 2
winHeight := numLines + 5
winX := engine.TERMINAL_SIZE_WIDTH/2 - winWidth/2
winY := engine.TERMINAL_SIZE_HEIGHT/2 - winHeight/2
d.window = CreateWindow(winX, winY, winWidth, winHeight, title, tcell.StyleDefault)
d.prompt = CreateUILabel(winX+1, winY+1, lineWidth, numLines, prompt, tcell.StyleDefault)
yesBtnLength := len(okText) + 4
yesBtnPosX := winX + winWidth/2 - yesBtnLength/2
d.yesBtn = CreateSimpleButton(yesBtnPosX, winY+winHeight-2, okText, tcell.StyleDefault, tcell.StyleDefault.Attributes(tcell.AttrBold), okAction)
d.yesBtn.Highlight()
return d
}
func (d *UIDialog) Select() {
if d.yesBtn.IsHighlighted() {
d.yesBtn.Select()
} else if d.noBtn != nil && d.noBtn.IsHighlighted() {
d.noBtn.Select()
}
}
func (d *UIDialog) OnSelect(f func()) {
d.yesBtn.OnSelect(f)
}
func (d *UIDialog) MoveTo(x int, y int) {
}
func (d *UIDialog) Position() engine.Position {
return d.window.Position()
}
func (d *UIDialog) Size() engine.Size {
return d.window.Size()
}
func (d *UIDialog) Input(e *tcell.EventKey) {
if e.Key() == tcell.KeyLeft {
if !d.yesBtn.IsHighlighted() {
d.noBtn.Unhighlight()
d.yesBtn.Highlight()
}
} else if e.Key() == tcell.KeyRight {
if d.noBtn == nil {
return
}
if !d.noBtn.IsHighlighted() {
d.noBtn.Highlight()
d.yesBtn.Unhighlight()
}
}
}
func (d *UIDialog) UniqueId() uuid.UUID {
return d.window.UniqueId()
}
func (d *UIDialog) Draw(v views.View) {
d.window.Draw(v)
d.prompt.Draw(v)
d.yesBtn.Draw(v)
if d.noBtn != nil {
d.noBtn.Draw(v)
}
}

View file

@ -26,5 +26,5 @@ type UIHighlightableElement interface {
type UISelectableElement interface { type UISelectableElement interface {
Select() Select()
OnSelect(func()) OnSelect(func())
UIHighlightableElement UIElement
} }

View file

@ -8,6 +8,7 @@ type BSPDungeonMap struct {
level *BasicMap level *BasicMap
playerSpawnPoint engine.Position playerSpawnPoint engine.Position
nextLevelStaircase engine.Position
rooms []engine.BoundingBox rooms []engine.BoundingBox
} }
@ -15,12 +16,16 @@ func (bsp *BSPDungeonMap) PlayerSpawnPoint() engine.Position {
return bsp.playerSpawnPoint return bsp.playerSpawnPoint
} }
func (bsp *BSPDungeonMap) NextLevelStaircasePosition() engine.Position {
return bsp.nextLevelStaircase
}
func (bsp *BSPDungeonMap) Size() engine.Size { func (bsp *BSPDungeonMap) Size() engine.Size {
return bsp.level.Size() return bsp.level.Size()
} }
func (bsp *BSPDungeonMap) SetTileAt(x int, y int, t Tile) { func (bsp *BSPDungeonMap) SetTileAt(x int, y int, t Tile) Tile {
bsp.level.SetTileAt(x, y, t) return bsp.level.SetTileAt(x, y, t)
} }
func (bsp *BSPDungeonMap) TileAt(x int, y int) Tile { func (bsp *BSPDungeonMap) TileAt(x int, y int) Tile {
@ -33,3 +38,7 @@ func (bsp *BSPDungeonMap) Tick(dt int64) {
func (bsp *BSPDungeonMap) Rooms() []engine.BoundingBox { func (bsp *BSPDungeonMap) Rooms() []engine.BoundingBox {
return bsp.rooms return bsp.rooms
} }
func (bsp *BSPDungeonMap) PreviousLevelStaircasePosition() engine.Position {
return bsp.playerSpawnPoint
}

View file

@ -1,11 +1,231 @@
package world package world
type dungeonLevel struct { import (
groundLevel *Map "math/rand"
entityLevel *EntityMap "mvvasilev/last_light/engine"
itemLevel *Map "mvvasilev/last_light/game/model"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
)
type DungeonType int
const (
DungeonTypeBSP DungeonType = iota
DungeonTypeCaverns
DungeonTypeMine
DungeonTypeUndercity
)
func randomDungeonType() DungeonType {
return DungeonType(rand.Intn(4))
} }
type Dungeon struct { type Dungeon struct {
levels []*dungeonLevel levels []*DungeonLevel
current int
}
func CreateDungeon(width, height int, depth int) *Dungeon {
levels := make([]*DungeonLevel, 0, depth)
for range depth {
levels = append(levels, CreateDungeonLevel(width, height, randomDungeonType()))
}
return &Dungeon{
levels: levels,
current: 0,
}
}
func (d *Dungeon) CurrentLevel() *DungeonLevel {
return d.levels[d.current]
}
func (d *Dungeon) MoveToNextLevel() (moved bool) {
if !d.HasNextLevel() {
return false
}
d.current++
return true
}
func (d *Dungeon) MoveToPreviousLevel() (moved bool) {
if !d.HasPreviousLevel() {
return false
}
d.current--
return true
}
func (d *Dungeon) NextLevel() *DungeonLevel {
if !d.HasNextLevel() {
return nil
}
return d.levels[d.current+1]
}
func (d *Dungeon) PreviousLevel() *DungeonLevel {
if !d.HasPreviousLevel() {
return nil
}
return d.levels[d.current-1]
}
func (d *Dungeon) HasPreviousLevel() bool {
return d.current-1 >= 0
}
func (d *Dungeon) HasNextLevel() bool {
return d.current+1 < len(d.levels)
}
type DungeonLevel struct {
groundLevel interface {
Map
WithPlayerSpawnPoint
WithNextLevelStaircasePosition
WithPreviousLevelStaircasePosition
}
entityLevel *EntityMap
itemLevel Map
multilevel Map
}
func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLevel {
genTable := make(map[float32]*model.ItemType, 0)
genTable[0.2] = model.ItemTypeFish()
genTable[0.05] = model.ItemTypeBow()
genTable[0.051] = model.ItemTypeLongsword()
genTable[0.052] = model.ItemTypeKey()
var groundLevel interface {
Map
WithRooms
WithPlayerSpawnPoint
WithNextLevelStaircasePosition
WithPreviousLevelStaircasePosition
}
switch dungeonType {
case DungeonTypeBSP:
groundLevel = CreateBSPDungeonMap(width, height, 4)
default:
groundLevel = CreateBSPDungeonMap(width, height, 4)
}
items := SpawnItems(groundLevel.Rooms(), 0.01, genTable, []engine.Position{
groundLevel.NextLevelStaircasePosition(),
groundLevel.PlayerSpawnPoint(),
groundLevel.PreviousLevelStaircasePosition(),
})
itemLevel := CreateEmptyDungeonLevel(width, height)
for _, it := range items {
if !groundLevel.TileAt(it.Position().XY()).Passable() {
continue
}
itemLevel.SetTileAt(it.Position().X(), it.Position().Y(), it)
}
d := &DungeonLevel{
groundLevel: groundLevel,
entityLevel: CreateEntityMap(width, height),
itemLevel: itemLevel,
}
d.multilevel = CreateMultilevelMap(
d.groundLevel,
d.itemLevel,
d.entityLevel,
)
return d
}
func (d *DungeonLevel) PlayerSpawnPoint() engine.Position {
return d.groundLevel.PlayerSpawnPoint()
}
func (d *DungeonLevel) NextLevelStaircase() engine.Position {
return d.groundLevel.NextLevelStaircasePosition()
}
func (d *DungeonLevel) PreviousLevelStaircase() engine.Position {
return d.groundLevel.PreviousLevelStaircasePosition()
}
func (d *DungeonLevel) DropEntity(uuid uuid.UUID) {
d.entityLevel.DropEntity(uuid)
}
func (d *DungeonLevel) AddEntity(entity model.MovableEntity, presentation rune, style tcell.Style) {
d.entityLevel.AddEntity(entity, presentation, style)
}
func (d *DungeonLevel) MoveEntity(uuid uuid.UUID, dx, dy int) {
d.entityLevel.MoveEntity(uuid, dx, dy)
}
func (d *DungeonLevel) MoveEntityTo(uuid uuid.UUID, x, y int) {
d.entityLevel.MoveEntityTo(uuid, x, y)
}
func (d *DungeonLevel) RemoveItemAt(x, y int) *model.Item {
if !d.groundLevel.Size().Contains(x, y) {
return nil
}
tile := d.itemLevel.TileAt(x, y)
itemTile, ok := tile.(*ItemTile)
if !ok {
return nil
}
d.itemLevel.SetTileAt(x, y, nil)
item := model.CreateItem(itemTile.Type(), itemTile.Quantity())
return &item
}
func (d *DungeonLevel) SetItemAt(x, y int, it model.Item) (success bool) {
if !d.TileAt(x, y).Passable() {
return false
}
d.itemLevel.SetTileAt(x, y, CreateItemTile(engine.PositionAt(x, y), it.Type(), it.Quantity()))
return true
}
func (d *DungeonLevel) TileAt(x, y int) Tile {
return d.multilevel.TileAt(x, y)
}
func (d *DungeonLevel) IsTilePassable(x, y int) bool {
if !d.groundLevel.Size().Contains(x, y) {
return false
}
return d.TileAt(x, y).Passable()
}
func (d *DungeonLevel) Flatten() Map {
return d.multilevel
} }

View file

@ -10,8 +10,8 @@ func (edl *EmptyDungeonMap) Size() engine.Size {
return edl.level.Size() return edl.level.Size()
} }
func (edl *EmptyDungeonMap) SetTileAt(x int, y int, t Tile) { func (edl *EmptyDungeonMap) SetTileAt(x int, y int, t Tile) Tile {
edl.level.SetTileAt(x, y, t) return edl.level.SetTileAt(x, y, t)
} }
func (edl *EmptyDungeonMap) TileAt(x int, y int) Tile { func (edl *EmptyDungeonMap) TileAt(x int, y int) Tile {
@ -21,3 +21,26 @@ func (edl *EmptyDungeonMap) TileAt(x int, y int) Tile {
func (edl *EmptyDungeonMap) Tick(dt int64) { func (edl *EmptyDungeonMap) Tick(dt int64) {
} }
func (edl *EmptyDungeonMap) Rooms() []engine.BoundingBox {
rooms := make([]engine.BoundingBox, 1)
rooms = append(rooms, engine.BoundingBox{
Sized: engine.WithSize(edl.Size()),
Positioned: engine.WithPosition(engine.PositionAt(0, 0)),
})
return rooms
}
func (edl *EmptyDungeonMap) PlayerSpawnPoint() engine.Position {
return engine.PositionAt(edl.Size().Width()/2, edl.Size().Height()/2)
}
func (edl *EmptyDungeonMap) NextLevelStaircasePosition() engine.Position {
return engine.PositionAt(edl.Size().Width()/3, edl.Size().Height()/3)
}
func (bsp *EmptyDungeonMap) PreviousLevelStaircasePosition() engine.Position {
return bsp.PlayerSpawnPoint()
}

View file

@ -22,7 +22,8 @@ func CreateEntityMap(width, height int) *EntityMap {
} }
} }
func (em *EntityMap) SetTileAt(x int, y int, t Tile) { func (em *EntityMap) SetTileAt(x int, y int, t Tile) Tile {
return nil
// if !em.FitsWithin(x, y) { // if !em.FitsWithin(x, y) {
// return // return
// } // }
@ -80,6 +81,26 @@ func (em *EntityMap) MoveEntity(uuid uuid.UUID, dx, dy int) {
em.entities[newKey] = e em.entities[newKey] = e
} }
func (em *EntityMap) MoveEntityTo(uuid uuid.UUID, x, y int) {
oldKey, e := em.FindEntityByUuid(uuid)
if e == nil {
return
}
if !em.FitsWithin(x, y) {
return
}
delete(em.entities, oldKey)
e.Entity().MoveTo(engine.PositionAt(x, y))
newKey := em.Size().AsArrayIndex(e.Entity().Position().XY())
em.entities[newKey] = e
}
func (em *EntityMap) TileAt(x int, y int) Tile { func (em *EntityMap) TileAt(x int, y int) Tile {
if !em.FitsWithin(x, y) { if !em.FitsWithin(x, y) {
return CreateStaticTile(x, y, TileTypeVoid()) return CreateStaticTile(x, y, TileTypeVoid())

View file

@ -39,7 +39,7 @@ func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap {
tiles[h] = make([]Tile, width) tiles[h] = make([]Tile, width)
} }
rooms := make([]engine.BoundingBox, 0, 2^numSplits) rooms := make([]engine.BoundingBox, 0, numSplits*numSplits)
iterateBspLeaves(root, func(leaf *bspNode) { iterateBspLeaves(root, func(leaf *bspNode) {
x := engine.RandInt(leaf.origin.X(), leaf.origin.X()+leaf.size.Width()/4) x := engine.RandInt(leaf.origin.X(), leaf.origin.X()+leaf.size.Width()/4)
@ -89,14 +89,24 @@ func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap {
bsp := new(BSPDungeonMap) bsp := new(BSPDungeonMap)
spawnRoom := findRoom(root.left) spawnRoom := findRoom(root.left)
staircaseRoom := findRoom(root.right)
bsp.rooms = rooms bsp.rooms = rooms
bsp.level = CreateBasicMap(tiles) bsp.level = CreateBasicMap(tiles)
bsp.playerSpawnPoint = engine.PositionAt( bsp.playerSpawnPoint = engine.PositionAt(
spawnRoom.Position().X()+spawnRoom.Size().Width()/2, spawnRoom.Position().X()+spawnRoom.Size().Width()/2,
spawnRoom.Position().Y()+spawnRoom.Size().Height()/2, spawnRoom.Position().Y()+spawnRoom.Size().Height()/2,
) )
bsp.nextLevelStaircase = engine.PositionAt(
staircaseRoom.Position().X()+staircaseRoom.Size().Width()/2,
staircaseRoom.Position().Y()+staircaseRoom.Size().Height()/2,
)
bsp.level.SetTileAt(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), CreateStaticTile(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), TileTypeStaircaseDown()))
bsp.level.SetTileAt(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), CreateStaticTile(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), TileTypeStaircaseUp()))
return bsp return bsp
} }

View file

@ -11,5 +11,8 @@ func CreateEmptyDungeonLevel(width, height int) *EmptyDungeonMap {
m.level = CreateBasicMap(tiles) m.level = CreateBasicMap(tiles)
//m.level.SetTileAt(width/2, height/2, CreateStaticTile(width/2, height/2, TileTypeStaircaseDown()))
//m.level.SetTileAt(width/3, height/3, CreateStaticTile(width/3, height/3, TileTypeStaircaseUp()))
return m return m
} }

View file

@ -4,9 +4,10 @@ import (
"math/rand" "math/rand"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model" "mvvasilev/last_light/game/model"
"slices"
) )
func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTable map[float32]*model.ItemType) []Tile { func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTable map[float32]*model.ItemType, forbiddenPositions []engine.Position) []Tile {
rooms := spawnableAreas rooms := spawnableAreas
itemTiles := make([]Tile, 0, 10) itemTiles := make([]Tile, 0, 10)
@ -32,6 +33,10 @@ func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTa
engine.RandInt(r.Position().Y()+1, r.Position().Y()+r.Size().Height()-1), engine.RandInt(r.Position().Y()+1, r.Position().Y()+r.Size().Height()-1),
) )
if slices.Contains(forbiddenPositions, pos) {
continue
}
itemTiles = append(itemTiles, CreateItemTile(pos, itemType, 1)) itemTiles = append(itemTiles, CreateItemTile(pos, itemType, 1))
} }
} }

View file

@ -6,19 +6,25 @@ import (
type Map interface { type Map interface {
Size() engine.Size Size() engine.Size
SetTileAt(x, y int, t Tile) SetTileAt(x, y int, t Tile) Tile
TileAt(x, y int) Tile TileAt(x, y int) Tile
Tick(dt int64) Tick(dt int64)
} }
type WithPlayerSpawnPoint interface { type WithPlayerSpawnPoint interface {
PlayerSpawnPoint() engine.Position PlayerSpawnPoint() engine.Position
Map
} }
type WithRooms interface { type WithRooms interface {
Rooms() []engine.BoundingBox Rooms() []engine.BoundingBox
Map }
type WithNextLevelStaircasePosition interface {
NextLevelStaircasePosition() engine.Position
}
type WithPreviousLevelStaircasePosition interface {
PreviousLevelStaircasePosition() engine.Position
} }
type BasicMap struct { type BasicMap struct {
@ -40,16 +46,18 @@ func (bm *BasicMap) Size() engine.Size {
return engine.SizeOf(len(bm.tiles[0]), len(bm.tiles)) return engine.SizeOf(len(bm.tiles[0]), len(bm.tiles))
} }
func (bm *BasicMap) SetTileAt(x int, y int, t Tile) { func (bm *BasicMap) SetTileAt(x int, y int, t Tile) Tile {
if len(bm.tiles) <= y || len(bm.tiles[0]) <= x { if len(bm.tiles) <= y || len(bm.tiles[0]) <= x {
return return CreateStaticTile(x, y, TileTypeVoid())
} }
if x < 0 || y < 0 { if x < 0 || y < 0 {
return return CreateStaticTile(x, y, TileTypeVoid())
} }
bm.tiles[y][x] = t bm.tiles[y][x] = t
return bm.tiles[y][x]
} }
func (bm *BasicMap) TileAt(x int, y int) Tile { func (bm *BasicMap) TileAt(x int, y int) Tile {

View file

@ -22,8 +22,8 @@ func (mm *MultilevelMap) Size() engine.Size {
return mm.layers[0].Size() return mm.layers[0].Size()
} }
func (mm *MultilevelMap) SetTileAt(x, y int, t Tile) { func (mm *MultilevelMap) SetTileAt(x, y int, t Tile) Tile {
mm.layers[0].SetTileAt(x, y, t) return mm.layers[0].SetTileAt(x, y, t)
} }
func (mm *MultilevelMap) UnsetTileAtHeight(x, y, height int) { func (mm *MultilevelMap) UnsetTileAtHeight(x, y, height int) {

View file

@ -17,6 +17,8 @@ const (
MaterialVoid MaterialVoid
MaterialClosedDoor MaterialClosedDoor
MaterialOpenDoor MaterialOpenDoor
MaterialStaircaseDown
MaterialStaircaseUp
) )
type TileType struct { type TileType struct {
@ -97,6 +99,26 @@ func TileTypeOpenDoor() TileType {
} }
} }
func TileTypeStaircaseDown() TileType {
return TileType{
Material: MaterialStaircaseDown,
Passable: true,
Transparent: false,
Presentation: '≡',
Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold),
}
}
func TileTypeStaircaseUp() TileType {
return TileType{
Material: MaterialStaircaseUp,
Passable: true,
Transparent: false,
Presentation: '^',
Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold),
}
}
type Tile interface { type Tile interface {
Position() engine.Position Position() engine.Position
Presentation() (rune, tcell.Style) Presentation() (rune, tcell.Style)