diff --git a/engine/engine_pathfinding.go b/engine/engine_pathfinding.go index 647a236..c629609 100644 --- a/engine/engine_pathfinding.go +++ b/engine/engine_pathfinding.go @@ -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 } diff --git a/engine/engine_pathfinding_test.go b/engine/engine_pathfinding_test.go index d0bd772..329fcc7 100644 --- a/engine/engine_pathfinding_test.go +++ b/engine/engine_pathfinding_test.go @@ -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 diff --git a/engine/engine_profiler.go b/engine/engine_profiler.go new file mode 100644 index 0000000..ce796b1 --- /dev/null +++ b/engine/engine_profiler.go @@ -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() +} diff --git a/game/game_context.go b/game/game_context.go index 53dbb45..2ca334f 100644 --- a/game/game_context.go +++ b/game/game_context.go @@ -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) } } diff --git a/game/model/entity.go b/game/model/entity.go index 2c38b6c..3f79cf9 100644 --- a/game/model/entity.go +++ b/game/model/entity.go @@ -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, + } + } +} diff --git a/game/model/entity_generator.go b/game/model/entity_generator.go new file mode 100644 index 0000000..c5c31bd --- /dev/null +++ b/game/model/entity_generator.go @@ -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) +} diff --git a/game/model/entity_npcs.go b/game/model/entity_npcs.go new file mode 100644 index 0000000..719def0 --- /dev/null +++ b/game/model/entity_npcs.go @@ -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 + }), + )), + )), + ) +} diff --git a/game/model/entity_player.go b/game/model/entity_player.go index 79b2244..40875e4 100644 --- a/game/model/entity_player.go +++ b/game/model/entity_player.go @@ -19,6 +19,7 @@ func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player { WithInventory(CreateEquippedInventory()), WithStats(playerBaseStats), WithHealthData(0, 0, false), + WithSpeed(10), ), } diff --git a/game/model/inventory.go b/game/model/inventory.go index 881af90..eafda82 100644 --- a/game/model/inventory.go +++ b/game/model/inventory.go @@ -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 } diff --git a/game/model/inventory_equipped.go b/game/model/inventory_equipped.go index d35a347..1e2c80f 100644 --- a/game/model/inventory_equipped.go +++ b/game/model/inventory_equipped.go @@ -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: diff --git a/game/state/playing_state.go b/game/state/playing_state.go index 6f4bc22..289071e 100644 --- a/game/state/playing_state.go +++ b/game/state/playing_state.go @@ -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) { diff --git a/game/ui/menu/menu_player_inventory_menu.go b/game/ui/menu/menu_player_inventory_menu.go index 2423296..0e4a285 100644 --- a/game/ui/menu/menu_player_inventory_menu.go +++ b/game/ui/menu/menu_player_inventory_menu.go @@ -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) } diff --git a/main.go b/main.go index 8ccb7fc..5c7dc23 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main -import "mvvasilev/last_light/game" +import ( + "mvvasilev/last_light/game" +) func main() { gc := game.CreateGameContext()