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
}
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 closedList = make([]*pathNode, 0)
@ -28,8 +28,16 @@ func FindPath(from Position, to Position, isPassable func(x, y int) bool) *Path
var lastNode *pathNode
iteration := 0
for {
iteration++
if iteration >= maxDistance {
return nil
}
if len(openList) == 0 {
break
}

View file

@ -9,6 +9,7 @@ func BenchmarkPathfinding(b *testing.B) {
path := FindPath(
PositionAt(0, 0),
PositionAt(16, 16),
20,
func(x, y int) bool {
if x > 6 && x <= 16 && y == 10 {
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
import (
"fmt"
"log"
"mvvasilev/last_light/engine"
"os"
"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 {
renderContext *engine.RenderContext
@ -34,6 +36,8 @@ func (gc *GameContext) Run() {
lastLoop := time.Now()
lastTick := time.Now()
tickRateText := engine.CreateText(0, 1, 16, 1, "0ms", tcell.StyleDefault)
for {
deltaTime := 1 + time.Since(lastLoop).Microseconds()
lastLoop = time.Now()
@ -42,12 +46,14 @@ func (gc *GameContext) Run() {
gc.game.Input(e)
}
if time.Since(lastTick).Milliseconds() >= TICK_RATE {
stop := !gc.game.Tick(deltaTime)
deltaTickTime := time.Since(lastTick).Milliseconds()
if deltaTickTime >= TickRate {
tickRateText = engine.CreateText(0, 1, 16, 1, fmt.Sprintf("%vms", deltaTickTime), tcell.StyleDefault)
stop := !gc.game.Tick(deltaTickTime)
if stop {
gc.renderContext.Stop()
os.Exit(0)
break
}
@ -55,6 +61,8 @@ func (gc *GameContext) Run() {
}
drawables := gc.game.CollectDrawables()
drawables = append(drawables, tickRateText)
gc.renderContext.Draw(deltaTime, drawables)
}
}

View file

@ -73,6 +73,10 @@ type Entity_StatsHolderComponent struct {
// StatModifiers []StatModifier
}
type Entity_SpeedComponent struct {
Speed int
}
type Entity_HealthComponent struct {
Health int
MaxHealth int
@ -82,6 +86,7 @@ type Entity_HealthComponent struct {
type Entity interface {
UniqueId() uuid.UUID
Speed() *Entity_SpeedComponent
Named() *Entity_NamedComponent
Described() *Entity_DescribedComponent
Presentable() *Entity_PresentableComponent
@ -94,6 +99,7 @@ type Entity interface {
type BaseEntity struct {
id uuid.UUID
speed *Entity_SpeedComponent
named *Entity_NamedComponent
described *Entity_DescribedComponent
presentable *Entity_PresentableComponent
@ -135,6 +141,10 @@ func (be *BaseEntity) HealthData() *Entity_HealthComponent {
return be.damageable
}
func (be *BaseEntity) Speed() *Entity_SpeedComponent {
return be.speed
}
func CreateEntity(components ...func(*BaseEntity)) *BaseEntity {
e := &BaseEntity{
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()),
WithStats(playerBaseStats),
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
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
if existingItem.Quantifiable().CurrentQuantity+1 > existingItem.Quantifiable().MaxQuantity {
continue
}
// Item has capacity, but is less than total new item stack. Split between existing, and a new stack.
if existingItem.Quantifiable().CurrentQuantity+i.Quantifiable().CurrentQuantity > existingItem.Quantifiable().MaxQuantity {
// get difference in quantities
diff := existingItem.Quantifiable().MaxQuantity - existingItem.Quantifiable().CurrentQuantity
total := existingCurrent + incomingCurrent
leftOver := engine.AbsInt(existingMax - total)
// set existing item quantity to max
existingItem.Quantifiable().CurrentQuantity = existingItem.Quantifiable().MaxQuantity
// 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
// Existing item is filled, and remained is turned into new stack
if leftOver > 0 {
// If we have don't have enough free slots, just say we can't push it
if index+1 >= inv.shape.Area() {
return false
}
existingItem.Quantifiable().CurrentQuantity = existingMax
i.Quantifiable().CurrentQuantity = leftOver
inv.contents[index+1] = i
return true
}
inv.contents[index] = i
// Otherwise, just set the existing item quantity to the total
existingItem.Quantifiable().CurrentQuantity = total
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 {
switch slot {
case EquippedSlotOffhand:

View file

@ -2,6 +2,7 @@ package state
import (
"fmt"
"math/rand"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/systems"
@ -15,8 +16,8 @@ type PlayingState struct {
turnSystem *systems.TurnSystem
inputSystem *systems.InputSystem
player *model.Player
someNPC model.Entity
player *model.Player
npcs []model.Entity
eventLog *engine.GameEventLog
uiEventLog *ui.UIEventLog
@ -64,44 +65,38 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
case systems.InputAction_OpenInventory:
s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s)
case systems.InputAction_PickUpItem:
s.PickUpItemUnderPlayer()
complete = true
complete = PickUpItemUnderPlayer(s.eventLog, s.dungeon, s.player)
case systems.InputAction_Interact:
s.InteractBelowPlayer()
complete = true
complete = s.InteractBelowPlayer()
case systems.InputAction_OpenLogs:
s.viewShortLogs = !s.viewShortLogs
case systems.InputAction_MovePlayer_East:
s.MovePlayer(model.East)
complete = true
complete = s.MovePlayer(model.East)
case systems.InputAction_MovePlayer_West:
s.MovePlayer(model.West)
complete = true
complete = s.MovePlayer(model.West)
case systems.InputAction_MovePlayer_North:
s.MovePlayer(model.North)
complete = true
complete = s.MovePlayer(model.North)
case systems.InputAction_MovePlayer_South:
s.MovePlayer(model.South)
complete = true
complete = s.MovePlayer(model.South)
default:
}
return
})
s.someNPC = model.CreateEntity(
model.WithPosition(s.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position),
model.WithName("NPC"),
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.WithHealthData(20, 20, false),
)
// s.someNPC = model.CreateEntity(
// model.WithPosition(s.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position),
// model.WithName("NPC"),
// 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.WithHealthData(20, 20, false),
// )
s.turnSystem.Schedule(20, func() (complete bool, requeue bool) {
s.CalcPathToPlayerAndMove()
// s.turnSystem.Schedule(20, func() (complete bool, requeue bool) {
// s.CalcPathToPlayerAndMove()
return true, true
})
// return true, true
// })
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.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(
engine.PositionAt(0, 0),
@ -127,9 +143,29 @@ func (s *PlayingState) InputContext() systems.InputContext {
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 {
return
return true
}
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.HealthData().IsDead {
// TODO: If the entity is dead, the player should be able to move through it.
return
return false
}
ExecuteAttack(ps.eventLog, ps.player, ent)
return
return true
}
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.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()
if playerPos == ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position {
ps.SwitchToNextLevel()
return
return true
}
if playerPos == ps.dungeon.CurrentLevel().Ground().PreviousLevelStaircase().Position {
ps.SwitchToPreviousLevel()
return
return true
}
ps.eventLog.Log("There is nothing to interact with here")
return false
}
func (ps *PlayingState) SwitchToNextLevel() {
@ -283,34 +329,38 @@ func (ps *PlayingState) SwitchToPreviousLevel() {
ps.dungeon.CurrentLevel().AddEntity(ps.player)
}
func (ps *PlayingState) PickUpItemUnderPlayer() {
pos := ps.player.Position()
item := ps.dungeon.CurrentLevel().RemoveItemAt(pos.XY())
func PickUpItemUnderPlayer(eventLog *engine.GameEventLog, dungeon *model.Dungeon, player *model.Player) (success bool) {
pos := player.Position()
item := dungeon.CurrentLevel().RemoveItemAt(pos.XY())
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 {
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
}
if item.Named() != nil {
itemName := item.Named().Name
ps.eventLog.Log("You picked up " + itemName)
eventLog.Log("You picked up " + itemName)
} 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)
for _, p := range positions {
if ps.dungeon.CurrentLevel().IsGroundTileOpaque(p.XY()) {
if dungeon.CurrentLevel().IsGroundTileOpaque(p.XY()) {
return false
}
}
@ -318,26 +368,30 @@ func (ps *PlayingState) HasLineOfSight(start, end engine.Position) bool {
return true
}
func (ps *PlayingState) PlayerWithinHitRange(pos 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()
func WithinHitRange(pos engine.Position, otherPos engine.Position) bool {
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() {
if ps.someNPC.HealthData().IsDead {
ps.dungeon.CurrentLevel().DropEntity(ps.someNPC.UniqueId())
func CalcPathToPlayerAndMove(simulationDistance int, eventLog *engine.GameEventLog, dungeon *model.Dungeon, npc model.Entity, player *model.Player) {
if npc.Positioned().Position.DistanceSquared(player.Position()) > simulationDistance*simulationDistance {
return
}
if npc.HealthData().IsDead {
dungeon.CurrentLevel().DropEntity(npc.UniqueId())
return
}
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
}
if !playerVisibleAndInRange {
randomMove := model.Direction(engine.RandInt(int(model.DirectionNone), int(model.East)))
nextPos := ps.someNPC.Positioned().Position
nextPos := npc.Positioned().Position
switch randomMove {
case model.North:
@ -352,9 +406,9 @@ func (ps *PlayingState) CalcPathToPlayerAndMove() {
return
}
if ps.dungeon.CurrentLevel().IsTilePassable(nextPos.XY()) {
ps.dungeon.CurrentLevel().MoveEntityTo(
ps.someNPC.UniqueId(),
if dungeon.CurrentLevel().IsTilePassable(nextPos.XY()) {
dungeon.CurrentLevel().MoveEntityTo(
npc.UniqueId(),
nextPos.X(),
nextPos.Y(),
)
@ -363,33 +417,38 @@ func (ps *PlayingState) CalcPathToPlayerAndMove() {
return
}
if ps.PlayerWithinHitRange(ps.someNPC.Positioned().Position) {
ExecuteAttack(ps.eventLog, ps.someNPC, ps.player)
if WithinHitRange(npc.Positioned().Position, player.Position()) {
ExecuteAttack(eventLog, npc, player)
}
pathToPlayer := engine.FindPath(
ps.someNPC.Positioned().Position,
ps.player.Position(),
npc.Positioned().Position,
player.Position(),
12,
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 ps.dungeon.CurrentLevel().IsTilePassable(x, y)
return dungeon.CurrentLevel().IsTilePassable(x, y)
},
)
if pathToPlayer == nil {
return
}
nextPos, hasNext := pathToPlayer.Next()
if !hasNext {
return
}
if nextPos.Equals(ps.player.Position()) {
if nextPos.Equals(player.Position()) {
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) {

View file

@ -26,7 +26,11 @@ type PlayerInventoryMenu struct {
playerItems *engine.ArbitraryDrawable
selectedItem *engine.ArbitraryDrawable
equipmentSlots *engine.ArbitraryDrawable
selectedInventorySlot engine.Position
highlightStyle tcell.Style
}
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,
)
menu.highlightStyle = highlightStyle
menu.playerItems = engine.CreateDrawingInstructions(func(v views.View) {
for y := range playerInventory.Shape().Height() {
for x := range playerInventory.Shape().Width() {
item := playerInventory.ItemAt(x, y)
for y := range menu.inventory.Shape().Height() {
for x := range menu.inventory.Shape().Width() {
item := menu.inventory.ItemAt(x, y)
isHighlighted := x == menu.selectedInventorySlot.X() && y == menu.selectedInventorySlot.Y()
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().Y()+1+y*2,
" ",
highlightStyle,
menu.highlightStyle,
).Draw(v)
}
@ -95,7 +101,7 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
style := item.Style()
if isHighlighted {
style = highlightStyle
style = menu.highlightStyle
}
menu.drawItemSlot(
@ -110,7 +116,7 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
})
menu.selectedItem = engine.CreateDrawingInstructions(func(v views.View) {
item := playerInventory.ItemAt(menu.selectedInventorySlot.XY())
item := menu.inventory.ItemAt(menu.selectedInventorySlot.XY())
if item == nil {
return
@ -119,9 +125,37 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
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
}
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) {
if item.Quantifiable() != nil {
ui.CreateSingleLineUILabel(
@ -181,4 +215,5 @@ func (pim *PlayerInventoryMenu) Draw(v views.View) {
pim.inventoryGrid.Draw(v)
pim.playerItems.Draw(v)
pim.selectedItem.Draw(v)
pim.equipmentSlots.Draw(v)
}

View file

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