mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-19 12:49:52 +03:00
Add pathfinding
This commit is contained in:
parent
3b9923a713
commit
28cf513b6d
20 changed files with 920 additions and 132 deletions
40
engine/path.go
Normal file
40
engine/path.go
Normal 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
131
engine/pathfinding.go
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
39
game/model/npc.go
Normal 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) {
|
||||||
|
}
|
52
game/state/dialog_state.go
Normal file
52
game/state/dialog_state.go
Normal 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)
|
||||||
|
}
|
|
@ -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"
|
||||||
|
@ -10,9 +11,10 @@ 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()
|
||||||
|
|
|
@ -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
123
game/ui/dialog.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,5 +26,5 @@ type UIHighlightableElement interface {
|
||||||
type UISelectableElement interface {
|
type UISelectableElement interface {
|
||||||
Select()
|
Select()
|
||||||
OnSelect(func())
|
OnSelect(func())
|
||||||
UIHighlightableElement
|
UIElement
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,20 +7,25 @@ import (
|
||||||
type BSPDungeonMap struct {
|
type BSPDungeonMap struct {
|
||||||
level *BasicMap
|
level *BasicMap
|
||||||
|
|
||||||
playerSpawnPoint engine.Position
|
playerSpawnPoint engine.Position
|
||||||
rooms []engine.BoundingBox
|
nextLevelStaircase engine.Position
|
||||||
|
rooms []engine.BoundingBox
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bsp *BSPDungeonMap) PlayerSpawnPoint() engine.Position {
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Reference in a new issue