mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-11 17:25:01 +03:00
Add profiling, add better entities, add entity generator
This commit is contained in:
parent
82bc886b88
commit
d3477074c7
13 changed files with 373 additions and 87 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
24
engine/engine_profiler.go
Normal 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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
25
game/model/entity_generator.go
Normal file
25
game/model/entity_generator.go
Normal 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
41
game/model/entity_npcs.go
Normal 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
|
||||
}),
|
||||
)),
|
||||
)),
|
||||
)
|
||||
}
|
|
@ -19,6 +19,7 @@ func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player {
|
|||
WithInventory(CreateEquippedInventory()),
|
||||
WithStats(playerBaseStats),
|
||||
WithHealthData(0, 0, false),
|
||||
WithSpeed(10),
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
4
main.go
4
main.go
|
@ -1,6 +1,8 @@
|
|||
package main
|
||||
|
||||
import "mvvasilev/last_light/game"
|
||||
import (
|
||||
"mvvasilev/last_light/game"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gc := game.CreateGameContext()
|
||||
|
|
Loading…
Add table
Reference in a new issue