Add profiling, add better entities, add entity generator

This commit is contained in:
Miroslav Vasilev 2024-06-07 23:02:17 +03:00
parent 82bc886b88
commit d3477074c7
13 changed files with 373 additions and 87 deletions

View file

@ -14,7 +14,7 @@ type pathNode struct {
f int // total cost of this node f int // total cost of this node
} }
func FindPath(from Position, to Position, isPassable func(x, y int) bool) *Path { func FindPath(from Position, to Position, maxDistance int, isPassable func(x, y int) bool) *Path {
var openList = make([]*pathNode, 0) var openList = make([]*pathNode, 0)
var closedList = make([]*pathNode, 0) var closedList = make([]*pathNode, 0)
@ -28,8 +28,16 @@ func FindPath(from Position, to Position, isPassable func(x, y int) bool) *Path
var lastNode *pathNode var lastNode *pathNode
iteration := 0
for { for {
iteration++
if iteration >= maxDistance {
return nil
}
if len(openList) == 0 { if len(openList) == 0 {
break break
} }

View file

@ -9,6 +9,7 @@ func BenchmarkPathfinding(b *testing.B) {
path := FindPath( path := FindPath(
PositionAt(0, 0), PositionAt(0, 0),
PositionAt(16, 16), PositionAt(16, 16),
20,
func(x, y int) bool { func(x, y int) bool {
if x > 6 && x <= 16 && y == 10 { if x > 6 && x <= 16 && y == 10 {
return false return false

24
engine/engine_profiler.go Normal file
View file

@ -0,0 +1,24 @@
package engine
import (
"fmt"
"os"
"runtime/pprof"
)
func Profile(profileName string, what func()) {
// Create a CPU profile file
f, err := os.Create(fmt.Sprintf("%s.prof", profileName))
if err != nil {
panic(err)
}
defer f.Close()
// Start CPU profiling
if err := pprof.StartCPUProfile(f); err != nil {
panic(err)
}
defer pprof.StopCPUProfile()
what()
}

View file

@ -1,13 +1,15 @@
package game package game
import ( import (
"fmt"
"log" "log"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"os"
"time" "time"
"github.com/gdamore/tcell/v2"
) )
const TICK_RATE int64 = 50 // tick every 50ms ( 20 ticks per second ) const TickRate int64 = 1 // tick every 50ms ( 20 ticks per second )
type GameContext struct { type GameContext struct {
renderContext *engine.RenderContext renderContext *engine.RenderContext
@ -34,6 +36,8 @@ func (gc *GameContext) Run() {
lastLoop := time.Now() lastLoop := time.Now()
lastTick := time.Now() lastTick := time.Now()
tickRateText := engine.CreateText(0, 1, 16, 1, "0ms", tcell.StyleDefault)
for { for {
deltaTime := 1 + time.Since(lastLoop).Microseconds() deltaTime := 1 + time.Since(lastLoop).Microseconds()
lastLoop = time.Now() lastLoop = time.Now()
@ -42,12 +46,14 @@ func (gc *GameContext) Run() {
gc.game.Input(e) gc.game.Input(e)
} }
if time.Since(lastTick).Milliseconds() >= TICK_RATE { deltaTickTime := time.Since(lastTick).Milliseconds()
stop := !gc.game.Tick(deltaTime) if deltaTickTime >= TickRate {
tickRateText = engine.CreateText(0, 1, 16, 1, fmt.Sprintf("%vms", deltaTickTime), tcell.StyleDefault)
stop := !gc.game.Tick(deltaTickTime)
if stop { if stop {
gc.renderContext.Stop() gc.renderContext.Stop()
os.Exit(0)
break break
} }
@ -55,6 +61,8 @@ func (gc *GameContext) Run() {
} }
drawables := gc.game.CollectDrawables() drawables := gc.game.CollectDrawables()
drawables = append(drawables, tickRateText)
gc.renderContext.Draw(deltaTime, drawables) gc.renderContext.Draw(deltaTime, drawables)
} }
} }

View file

@ -73,6 +73,10 @@ type Entity_StatsHolderComponent struct {
// StatModifiers []StatModifier // StatModifiers []StatModifier
} }
type Entity_SpeedComponent struct {
Speed int
}
type Entity_HealthComponent struct { type Entity_HealthComponent struct {
Health int Health int
MaxHealth int MaxHealth int
@ -82,6 +86,7 @@ type Entity_HealthComponent struct {
type Entity interface { type Entity interface {
UniqueId() uuid.UUID UniqueId() uuid.UUID
Speed() *Entity_SpeedComponent
Named() *Entity_NamedComponent Named() *Entity_NamedComponent
Described() *Entity_DescribedComponent Described() *Entity_DescribedComponent
Presentable() *Entity_PresentableComponent Presentable() *Entity_PresentableComponent
@ -94,6 +99,7 @@ type Entity interface {
type BaseEntity struct { type BaseEntity struct {
id uuid.UUID id uuid.UUID
speed *Entity_SpeedComponent
named *Entity_NamedComponent named *Entity_NamedComponent
described *Entity_DescribedComponent described *Entity_DescribedComponent
presentable *Entity_PresentableComponent presentable *Entity_PresentableComponent
@ -135,6 +141,10 @@ func (be *BaseEntity) HealthData() *Entity_HealthComponent {
return be.damageable return be.damageable
} }
func (be *BaseEntity) Speed() *Entity_SpeedComponent {
return be.speed
}
func CreateEntity(components ...func(*BaseEntity)) *BaseEntity { func CreateEntity(components ...func(*BaseEntity)) *BaseEntity {
e := &BaseEntity{ e := &BaseEntity{
id: uuid.New(), id: uuid.New(),
@ -206,3 +216,11 @@ func WithHealthData(health, maxHealth int, isDead bool) func(e *BaseEntity) {
} }
} }
} }
func WithSpeed(speed int) func(e *BaseEntity) {
return func(e *BaseEntity) {
e.speed = &Entity_SpeedComponent{
Speed: speed,
}
}
}

View file

@ -0,0 +1,25 @@
package model
import "math/rand"
type EntitySupplier func(x, y int) Entity
type EntityTable struct {
table []EntitySupplier
}
func CreateEntityTable() *EntityTable {
return &EntityTable{
table: make([]EntitySupplier, 0),
}
}
func (igt *EntityTable) Add(weight int, createItemFunction EntitySupplier) {
for range weight {
igt.table = append(igt.table, createItemFunction)
}
}
func (igt *EntityTable) Generate(x, y int) Entity {
return igt.table[rand.Intn(len(igt.table))](x, y)
}

41
game/model/entity_npcs.go Normal file
View file

@ -0,0 +1,41 @@
package model
import (
"mvvasilev/last_light/engine"
"github.com/gdamore/tcell/v2"
)
type specialItemType ItemType
const (
ImpClaws specialItemType = 100_000 + iota
)
func Entity_Imp(x, y int) Entity {
return CreateEntity(
WithName("Imp"),
WithDescription("A fiery little creature"),
WithHealthData(15, 15, false),
WithPosition(engine.PositionAt(x, y)),
WithPresentation('i', tcell.StyleDefault.Foreground(tcell.ColorDarkRed)),
WithSpeed(11),
WithStats(map[Stat]int{
Stat_Attributes_Constitution: 5,
Stat_Attributes_Dexterity: 10,
Stat_Attributes_Strength: 5,
Stat_Attributes_Intelligence: 7,
}),
WithInventory(BuildInventory(
Inv_WithDominantHand(createBaseItem(
ItemType(ImpClaws),
'v', "|||",
tcell.StyleDefault,
item_WithName("Claws", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) {
return RollD4(1), DamageType_Physical_Slashing
}),
)),
)),
)
}

View file

@ -19,6 +19,7 @@ func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player {
WithInventory(CreateEquippedInventory()), WithInventory(CreateEquippedInventory()),
WithStats(playerBaseStats), WithStats(playerBaseStats),
WithHealthData(0, 0, false), WithHealthData(0, 0, false),
WithSpeed(10),
), ),
} }

View file

@ -43,34 +43,44 @@ func (inv *BasicInventory) Push(i Item) (success bool) {
// Try to first find a matching item with capacity // Try to first find a matching item with capacity
for index, existingItem := range inv.contents { for index, existingItem := range inv.contents {
if existingItem != nil && existingItem.Type() == itemType && existingItem.Quantifiable() != nil && i.Quantifiable() != nil { if existingItem == nil {
continue
}
itemsAreSame := existingItem.Type() == itemType
bothItemsAreQuantifiable := existingItem.Quantifiable() != nil && i.Quantifiable() != nil
if itemsAreSame && bothItemsAreQuantifiable {
existingCurrent := existingItem.Quantifiable().CurrentQuantity
incomingCurrent := i.Quantifiable().CurrentQuantity
existingMax := existingItem.Quantifiable().MaxQuantity
// Cannot add even a single more item to this stack, skip it // Cannot add even a single more item to this stack, skip it
if existingItem.Quantifiable().CurrentQuantity+1 > existingItem.Quantifiable().MaxQuantity { if existingItem.Quantifiable().CurrentQuantity+1 > existingItem.Quantifiable().MaxQuantity {
continue continue
} }
// Item has capacity, but is less than total new item stack. Split between existing, and a new stack. total := existingCurrent + incomingCurrent
if existingItem.Quantifiable().CurrentQuantity+i.Quantifiable().CurrentQuantity > existingItem.Quantifiable().MaxQuantity { leftOver := engine.AbsInt(existingMax - total)
// get difference in quantities
diff := existingItem.Quantifiable().MaxQuantity - existingItem.Quantifiable().CurrentQuantity
// set existing item quantity to max // Existing item is filled, and remained is turned into new stack
existingItem.Quantifiable().CurrentQuantity = existingItem.Quantifiable().MaxQuantity if leftOver > 0 {
// If we have don't have enough free slots, just say we can't push it
// set new item quantity to its current - diff
i.Quantifiable().CurrentQuantity -= i.Quantifiable().CurrentQuantity - diff
// Cannot pick up item, doing so would overflow the inventory
if index+1 >= inv.shape.Area() { if index+1 >= inv.shape.Area() {
return false return false
} }
existingItem.Quantifiable().CurrentQuantity = existingMax
i.Quantifiable().CurrentQuantity = leftOver
inv.contents[index+1] = i inv.contents[index+1] = i
return true return true
} }
inv.contents[index] = i // Otherwise, just set the existing item quantity to the total
existingItem.Quantifiable().CurrentQuantity = total
return true return true
} }

View file

@ -35,6 +35,60 @@ func CreateEquippedInventory() *EquippedInventory {
} }
} }
func BuildInventory(manips ...func(*EquippedInventory)) *EquippedInventory {
ei := CreateEquippedInventory()
for _, m := range manips {
m(ei)
}
return ei
}
func Inv_WithOffHand(item Item) func(*EquippedInventory) {
return func(ei *EquippedInventory) {
ei.offHand = item
}
}
func Inv_WithDominantHand(item Item) func(*EquippedInventory) {
return func(ei *EquippedInventory) {
ei.dominantHand = item
}
}
func Inv_WithHead(item Item) func(*EquippedInventory) {
return func(ei *EquippedInventory) {
ei.head = item
}
}
func Inv_WithChest(item Item) func(*EquippedInventory) {
return func(ei *EquippedInventory) {
ei.chestplate = item
}
}
func Inv_WithLegs(item Item) func(*EquippedInventory) {
return func(ei *EquippedInventory) {
ei.leggings = item
}
}
func Inv_WithShoes(item Item) func(*EquippedInventory) {
return func(ei *EquippedInventory) {
ei.shoes = item
}
}
func Inv_WithContents(items ...Item) func(*EquippedInventory) {
return func(ei *EquippedInventory) {
for _, i := range items {
ei.Push(i)
}
}
}
func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item { func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item {
switch slot { switch slot {
case EquippedSlotOffhand: case EquippedSlotOffhand:

View file

@ -2,6 +2,7 @@ package state
import ( import (
"fmt" "fmt"
"math/rand"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model" "mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/systems" "mvvasilev/last_light/game/systems"
@ -15,8 +16,8 @@ type PlayingState struct {
turnSystem *systems.TurnSystem turnSystem *systems.TurnSystem
inputSystem *systems.InputSystem inputSystem *systems.InputSystem
player *model.Player player *model.Player
someNPC model.Entity npcs []model.Entity
eventLog *engine.GameEventLog eventLog *engine.GameEventLog
uiEventLog *ui.UIEventLog uiEventLog *ui.UIEventLog
@ -64,44 +65,38 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
case systems.InputAction_OpenInventory: case systems.InputAction_OpenInventory:
s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s) s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s)
case systems.InputAction_PickUpItem: case systems.InputAction_PickUpItem:
s.PickUpItemUnderPlayer() complete = PickUpItemUnderPlayer(s.eventLog, s.dungeon, s.player)
complete = true
case systems.InputAction_Interact: case systems.InputAction_Interact:
s.InteractBelowPlayer() complete = s.InteractBelowPlayer()
complete = true
case systems.InputAction_OpenLogs: case systems.InputAction_OpenLogs:
s.viewShortLogs = !s.viewShortLogs s.viewShortLogs = !s.viewShortLogs
case systems.InputAction_MovePlayer_East: case systems.InputAction_MovePlayer_East:
s.MovePlayer(model.East) complete = s.MovePlayer(model.East)
complete = true
case systems.InputAction_MovePlayer_West: case systems.InputAction_MovePlayer_West:
s.MovePlayer(model.West) complete = s.MovePlayer(model.West)
complete = true
case systems.InputAction_MovePlayer_North: case systems.InputAction_MovePlayer_North:
s.MovePlayer(model.North) complete = s.MovePlayer(model.North)
complete = true
case systems.InputAction_MovePlayer_South: case systems.InputAction_MovePlayer_South:
s.MovePlayer(model.South) complete = s.MovePlayer(model.South)
complete = true
default: default:
} }
return return
}) })
s.someNPC = model.CreateEntity( // s.someNPC = model.CreateEntity(
model.WithPosition(s.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position), // model.WithPosition(s.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position),
model.WithName("NPC"), // model.WithName("NPC"),
model.WithPresentation('n', tcell.StyleDefault), // model.WithPresentation('n', tcell.StyleDefault),
model.WithStats(model.RandomStats(21, 1, 20, []model.Stat{model.Stat_Attributes_Strength, model.Stat_Attributes_Constitution, model.Stat_Attributes_Intelligence, model.Stat_Attributes_Dexterity})), // model.WithStats(model.RandomStats(21, 1, 20, []model.Stat{model.Stat_Attributes_Strength, model.Stat_Attributes_Constitution, model.Stat_Attributes_Intelligence, model.Stat_Attributes_Dexterity})),
model.WithHealthData(20, 20, false), // model.WithHealthData(20, 20, false),
) // )
s.turnSystem.Schedule(20, func() (complete bool, requeue bool) { // s.turnSystem.Schedule(20, func() (complete bool, requeue bool) {
s.CalcPathToPlayerAndMove() // s.CalcPathToPlayerAndMove()
return true, true // return true, true
}) // })
s.eventLog = engine.CreateGameEventLog(100) s.eventLog = engine.CreateGameEventLog(100)
@ -109,7 +104,28 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
s.healthBar = ui.CreateHealthBar(68, 0, 12, 3, s.player, tcell.StyleDefault) s.healthBar = ui.CreateHealthBar(68, 0, 12, 3, s.player, tcell.StyleDefault)
s.dungeon.CurrentLevel().AddEntity(s.player) s.dungeon.CurrentLevel().AddEntity(s.player)
s.dungeon.CurrentLevel().AddEntity(s.someNPC)
entityTable := model.CreateEntityTable()
entityTable.Add(1, func(x, y int) model.Entity { return model.Entity_Imp(x, y) })
s.npcs = SpawnNPCs(s.dungeon, 7, entityTable)
for _, npc := range s.npcs {
speed := 10
if npc.Speed() != nil {
speed = npc.Speed().Speed
}
s.turnSystem.Schedule(speed, func() (complete bool, requeue bool) {
CalcPathToPlayerAndMove(25, s.eventLog, s.dungeon, npc, s.player)
return true, true
})
}
// s.dungeon.CurrentLevel().AddEntity(s.someNPC)
s.viewport = engine.CreateViewport( s.viewport = engine.CreateViewport(
engine.PositionAt(0, 0), engine.PositionAt(0, 0),
@ -127,9 +143,29 @@ func (s *PlayingState) InputContext() systems.InputContext {
return systems.InputContext_Play return systems.InputContext_Play
} }
func (ps *PlayingState) MovePlayer(direction model.Direction) { func SpawnNPCs(dungeon *model.Dungeon, number int, genTable *model.EntityTable) []model.Entity {
rooms := dungeon.CurrentLevel().Ground().Rooms().Rooms
entities := make([]model.Entity, 0, number)
for range number {
r := rooms[rand.Intn(len(rooms))]
x, y := engine.RandInt(r.Position().X()+1, r.Position().X()+r.Size().Width()-1), engine.RandInt(r.Position().Y()+1, r.Position().Y()+r.Size().Height()-1)
entity := genTable.Generate(x, y)
entities = append(entities, entity)
dungeon.CurrentLevel().AddEntity(entity)
}
return entities
}
func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
if direction == model.DirectionNone { if direction == model.DirectionNone {
return return true
} }
newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction)) newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction))
@ -140,12 +176,12 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) {
if ent != nil && ent.HealthData() != nil { if ent != nil && ent.HealthData() != nil {
if ent.HealthData().IsDead { if ent.HealthData().IsDead {
// TODO: If the entity is dead, the player should be able to move through it. // TODO: If the entity is dead, the player should be able to move through it.
return return false
} }
ExecuteAttack(ps.eventLog, ps.player, ent) ExecuteAttack(ps.eventLog, ps.player, ent)
return return true
} }
if ps.dungeon.CurrentLevel().IsTilePassable(newPlayerPos.XY()) { if ps.dungeon.CurrentLevel().IsTilePassable(newPlayerPos.XY()) {
@ -153,6 +189,12 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) {
ps.viewport.SetCenter(ps.player.Position()) ps.viewport.SetCenter(ps.player.Position())
ps.eventLog.Log("You moved " + model.DirectionName(direction)) ps.eventLog.Log("You moved " + model.DirectionName(direction))
return true
} else {
ps.eventLog.Log("You bump into an impassable object" + model.DirectionName(direction))
return false
} }
} }
@ -197,18 +239,22 @@ func CalculateAttack(attacker, victim model.Entity) (hit bool, precisionRoll, ev
} }
} }
func (ps *PlayingState) InteractBelowPlayer() { func (ps *PlayingState) InteractBelowPlayer() (success bool) {
playerPos := ps.player.Position() playerPos := ps.player.Position()
if playerPos == ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position { if playerPos == ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position {
ps.SwitchToNextLevel() ps.SwitchToNextLevel()
return return true
} }
if playerPos == ps.dungeon.CurrentLevel().Ground().PreviousLevelStaircase().Position { if playerPos == ps.dungeon.CurrentLevel().Ground().PreviousLevelStaircase().Position {
ps.SwitchToPreviousLevel() ps.SwitchToPreviousLevel()
return return true
} }
ps.eventLog.Log("There is nothing to interact with here")
return false
} }
func (ps *PlayingState) SwitchToNextLevel() { func (ps *PlayingState) SwitchToNextLevel() {
@ -283,34 +329,38 @@ func (ps *PlayingState) SwitchToPreviousLevel() {
ps.dungeon.CurrentLevel().AddEntity(ps.player) ps.dungeon.CurrentLevel().AddEntity(ps.player)
} }
func (ps *PlayingState) PickUpItemUnderPlayer() { func PickUpItemUnderPlayer(eventLog *engine.GameEventLog, dungeon *model.Dungeon, player *model.Player) (success bool) {
pos := ps.player.Position() pos := player.Position()
item := ps.dungeon.CurrentLevel().RemoveItemAt(pos.XY()) item := dungeon.CurrentLevel().RemoveItemAt(pos.XY())
if item == nil { if item == nil {
return eventLog.Log("There is no item to pick up here")
return false
} }
success := ps.player.Inventory().Push(item) success = player.Inventory().Push(item)
if !success { if !success {
ps.dungeon.CurrentLevel().SetItemAt(pos.X(), pos.Y(), item) eventLog.Log("Unable to pick up item")
dungeon.CurrentLevel().SetItemAt(pos.X(), pos.Y(), item)
return return
} }
if item.Named() != nil { if item.Named() != nil {
itemName := item.Named().Name itemName := item.Named().Name
ps.eventLog.Log("You picked up " + itemName) eventLog.Log("You picked up " + itemName)
} else { } else {
ps.eventLog.Log("You picked up an item") eventLog.Log("You picked up an item")
} }
return true
} }
func (ps *PlayingState) HasLineOfSight(start, end engine.Position) bool { func HasLineOfSight(dungeon *model.Dungeon, start, end engine.Position) bool {
positions := engine.CastRay(start, end) positions := engine.CastRay(start, end)
for _, p := range positions { for _, p := range positions {
if ps.dungeon.CurrentLevel().IsGroundTileOpaque(p.XY()) { if dungeon.CurrentLevel().IsGroundTileOpaque(p.XY()) {
return false return false
} }
} }
@ -318,26 +368,30 @@ func (ps *PlayingState) HasLineOfSight(start, end engine.Position) bool {
return true return true
} }
func (ps *PlayingState) PlayerWithinHitRange(pos engine.Position) bool { func WithinHitRange(pos engine.Position, otherPos engine.Position) bool {
return pos.WithOffset(-1, 0) == ps.player.Position() || pos.WithOffset(+1, 0) == ps.player.Position() || pos.WithOffset(0, -1) == ps.player.Position() || pos.WithOffset(0, +1) == ps.player.Position() return pos.WithOffset(-1, 0) == otherPos || pos.WithOffset(+1, 0) == otherPos || pos.WithOffset(0, -1) == otherPos || pos.WithOffset(0, +1) == otherPos
} }
func (ps *PlayingState) CalcPathToPlayerAndMove() { func CalcPathToPlayerAndMove(simulationDistance int, eventLog *engine.GameEventLog, dungeon *model.Dungeon, npc model.Entity, player *model.Player) {
if ps.someNPC.HealthData().IsDead { if npc.Positioned().Position.DistanceSquared(player.Position()) > simulationDistance*simulationDistance {
ps.dungeon.CurrentLevel().DropEntity(ps.someNPC.UniqueId()) return
}
if npc.HealthData().IsDead {
dungeon.CurrentLevel().DropEntity(npc.UniqueId())
return return
} }
playerVisibleAndInRange := false playerVisibleAndInRange := false
if ps.someNPC.Positioned().Position.Distance(ps.player.Position()) < 20 && ps.HasLineOfSight(ps.someNPC.Positioned().Position, ps.player.Position()) { if npc.Positioned().Position.DistanceSquared(player.Position()) < 144 && HasLineOfSight(dungeon, npc.Positioned().Position, player.Position()) {
playerVisibleAndInRange = true playerVisibleAndInRange = true
} }
if !playerVisibleAndInRange { if !playerVisibleAndInRange {
randomMove := model.Direction(engine.RandInt(int(model.DirectionNone), int(model.East))) randomMove := model.Direction(engine.RandInt(int(model.DirectionNone), int(model.East)))
nextPos := ps.someNPC.Positioned().Position nextPos := npc.Positioned().Position
switch randomMove { switch randomMove {
case model.North: case model.North:
@ -352,9 +406,9 @@ func (ps *PlayingState) CalcPathToPlayerAndMove() {
return return
} }
if ps.dungeon.CurrentLevel().IsTilePassable(nextPos.XY()) { if dungeon.CurrentLevel().IsTilePassable(nextPos.XY()) {
ps.dungeon.CurrentLevel().MoveEntityTo( dungeon.CurrentLevel().MoveEntityTo(
ps.someNPC.UniqueId(), npc.UniqueId(),
nextPos.X(), nextPos.X(),
nextPos.Y(), nextPos.Y(),
) )
@ -363,33 +417,38 @@ func (ps *PlayingState) CalcPathToPlayerAndMove() {
return return
} }
if ps.PlayerWithinHitRange(ps.someNPC.Positioned().Position) { if WithinHitRange(npc.Positioned().Position, player.Position()) {
ExecuteAttack(ps.eventLog, ps.someNPC, ps.player) ExecuteAttack(eventLog, npc, player)
} }
pathToPlayer := engine.FindPath( pathToPlayer := engine.FindPath(
ps.someNPC.Positioned().Position, npc.Positioned().Position,
ps.player.Position(), player.Position(),
12,
func(x, y int) bool { func(x, y int) bool {
if x == ps.player.Position().X() && y == ps.player.Position().Y() { if x == player.Position().X() && y == player.Position().Y() {
return true return true
} }
return ps.dungeon.CurrentLevel().IsTilePassable(x, y) return dungeon.CurrentLevel().IsTilePassable(x, y)
}, },
) )
if pathToPlayer == nil {
return
}
nextPos, hasNext := pathToPlayer.Next() nextPos, hasNext := pathToPlayer.Next()
if !hasNext { if !hasNext {
return return
} }
if nextPos.Equals(ps.player.Position()) { if nextPos.Equals(player.Position()) {
return return
} }
ps.dungeon.CurrentLevel().MoveEntityTo(ps.someNPC.UniqueId(), nextPos.X(), nextPos.Y()) dungeon.CurrentLevel().MoveEntityTo(npc.UniqueId(), nextPos.X(), nextPos.Y())
} }
func (ps *PlayingState) OnTick(dt int64) (nextState GameState) { func (ps *PlayingState) OnTick(dt int64) (nextState GameState) {

View file

@ -26,7 +26,11 @@ type PlayerInventoryMenu struct {
playerItems *engine.ArbitraryDrawable playerItems *engine.ArbitraryDrawable
selectedItem *engine.ArbitraryDrawable selectedItem *engine.ArbitraryDrawable
equipmentSlots *engine.ArbitraryDrawable
selectedInventorySlot engine.Position selectedInventorySlot engine.Position
highlightStyle tcell.Style
} }
func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventory, style tcell.Style, highlightStyle tcell.Style) *PlayerInventoryMenu { func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventory, style tcell.Style, highlightStyle tcell.Style) *PlayerInventoryMenu {
@ -72,10 +76,12 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
x+2, y+5, 3, 1, 8, 4, '┌', '─', '┬', '┐', '│', ' ', '│', '│', '├', '─', '┼', '┤', '└', '─', '┴', '┘', style, highlightStyle, x+2, y+5, 3, 1, 8, 4, '┌', '─', '┬', '┐', '│', ' ', '│', '│', '├', '─', '┼', '┤', '└', '─', '┴', '┘', style, highlightStyle,
) )
menu.highlightStyle = highlightStyle
menu.playerItems = engine.CreateDrawingInstructions(func(v views.View) { menu.playerItems = engine.CreateDrawingInstructions(func(v views.View) {
for y := range playerInventory.Shape().Height() { for y := range menu.inventory.Shape().Height() {
for x := range playerInventory.Shape().Width() { for x := range menu.inventory.Shape().Width() {
item := playerInventory.ItemAt(x, y) item := menu.inventory.ItemAt(x, y)
isHighlighted := x == menu.selectedInventorySlot.X() && y == menu.selectedInventorySlot.Y() isHighlighted := x == menu.selectedInventorySlot.X() && y == menu.selectedInventorySlot.Y()
if item == nil { if item == nil {
@ -85,7 +91,7 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
menu.inventoryGrid.Position().X()+1+x*4, menu.inventoryGrid.Position().X()+1+x*4,
menu.inventoryGrid.Position().Y()+1+y*2, menu.inventoryGrid.Position().Y()+1+y*2,
" ", " ",
highlightStyle, menu.highlightStyle,
).Draw(v) ).Draw(v)
} }
@ -95,7 +101,7 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
style := item.Style() style := item.Style()
if isHighlighted { if isHighlighted {
style = highlightStyle style = menu.highlightStyle
} }
menu.drawItemSlot( menu.drawItemSlot(
@ -110,7 +116,7 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
}) })
menu.selectedItem = engine.CreateDrawingInstructions(func(v views.View) { menu.selectedItem = engine.CreateDrawingInstructions(func(v views.View) {
item := playerInventory.ItemAt(menu.selectedInventorySlot.XY()) item := menu.inventory.ItemAt(menu.selectedInventorySlot.XY())
if item == nil { if item == nil {
return return
@ -119,9 +125,37 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
ui.CreateUIItem(x+2, y+14, item, style).Draw(v) ui.CreateUIItem(x+2, y+14, item, style).Draw(v)
}) })
menu.equipmentSlots = engine.CreateDrawingInstructions(func(v views.View) {
drawEquipmentSlot(menu.rightHandBox.Position().X()+1, menu.rightHandBox.Position().Y(), menu.inventory.AtSlot(model.EquippedSlotDominantHand), false, menu.highlightStyle, v)
drawEquipmentSlot(menu.leftHandBox.Position().X()+1, menu.leftHandBox.Position().Y(), menu.inventory.AtSlot(model.EquippedSlotOffhand), false, menu.highlightStyle, v)
drawEquipmentSlot(x+10+1, y+3, menu.inventory.AtSlot(model.EquippedSlotHead), false, menu.highlightStyle, v)
drawEquipmentSlot(x+10+4, y+3, menu.inventory.AtSlot(model.EquippedSlotChestplate), false, menu.highlightStyle, v)
drawEquipmentSlot(x+10+7, y+3, menu.inventory.AtSlot(model.EquippedSlotLeggings), false, menu.highlightStyle, v)
drawEquipmentSlot(x+10+10, y+3, menu.inventory.AtSlot(model.EquippedSlotShoes), false, menu.highlightStyle, v)
})
return menu return menu
} }
func drawEquipmentSlot(screenX, screenY int, item model.Item, highlighted bool, highlightStyle tcell.Style, v views.View) {
if item == nil {
return
}
style := item.Style()
if highlighted {
style = highlightStyle
}
ui.CreateSingleLineUILabel(
screenX,
screenY+1,
item.Icon(),
style,
).Draw(v)
}
func (pim *PlayerInventoryMenu) drawItemSlot(screenX, screenY int, item model.Item, style tcell.Style, v views.View) { func (pim *PlayerInventoryMenu) drawItemSlot(screenX, screenY int, item model.Item, style tcell.Style, v views.View) {
if item.Quantifiable() != nil { if item.Quantifiable() != nil {
ui.CreateSingleLineUILabel( ui.CreateSingleLineUILabel(
@ -181,4 +215,5 @@ func (pim *PlayerInventoryMenu) Draw(v views.View) {
pim.inventoryGrid.Draw(v) pim.inventoryGrid.Draw(v)
pim.playerItems.Draw(v) pim.playerItems.Draw(v)
pim.selectedItem.Draw(v) pim.selectedItem.Draw(v)
pim.equipmentSlots.Draw(v)
} }

View file

@ -1,6 +1,8 @@
package main package main
import "mvvasilev/last_light/game" import (
"mvvasilev/last_light/game"
)
func main() { func main() {
gc := game.CreateGameContext() gc := game.CreateGameContext()