mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-19 12:49:52 +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
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
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
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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()),
|
WithInventory(CreateEquippedInventory()),
|
||||||
WithStats(playerBaseStats),
|
WithStats(playerBaseStats),
|
||||||
WithHealthData(0, 0, false),
|
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
|
// 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -16,7 +17,7 @@ type PlayingState struct {
|
||||||
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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
4
main.go
4
main.go
|
@ -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()
|
||||||
|
|
Loading…
Add table
Reference in a new issue