Multiple entities per tile, look state, entity behavior moved

This commit is contained in:
Miroslav Vasilev 2024-06-08 15:33:18 +03:00
parent fb50992ce4
commit e8f3c6ca9e
14 changed files with 685 additions and 248 deletions

View file

@ -70,11 +70,11 @@ type Entity_EquippedComponent struct {
type Entity_StatsHolderComponent struct { type Entity_StatsHolderComponent struct {
BaseStats map[Stat]int BaseStats map[Stat]int
// StatModifiers []StatModifier
} }
type Entity_SpeedComponent struct { type Entity_BehaviorComponent struct {
Speed int Speed int
Behavior func() (complete, requeue bool)
} }
type Entity_HealthComponent struct { type Entity_HealthComponent struct {
@ -83,10 +83,14 @@ type Entity_HealthComponent struct {
IsDead bool IsDead bool
} }
type Entity_DropTableComponent struct {
DropTable *LootTable
}
type Entity interface { type Entity interface {
UniqueId() uuid.UUID UniqueId() uuid.UUID
Speed() *Entity_SpeedComponent Behavior() *Entity_BehaviorComponent
Named() *Entity_NamedComponent Named() *Entity_NamedComponent
Described() *Entity_DescribedComponent Described() *Entity_DescribedComponent
Presentable() *Entity_PresentableComponent Presentable() *Entity_PresentableComponent
@ -94,12 +98,13 @@ type Entity interface {
Equipped() *Entity_EquippedComponent Equipped() *Entity_EquippedComponent
Stats() *Entity_StatsHolderComponent Stats() *Entity_StatsHolderComponent
HealthData() *Entity_HealthComponent HealthData() *Entity_HealthComponent
DropTable() *Entity_DropTableComponent
} }
type BaseEntity struct { type BaseEntity struct {
id uuid.UUID id uuid.UUID
speed *Entity_SpeedComponent behavior *Entity_BehaviorComponent
named *Entity_NamedComponent named *Entity_NamedComponent
described *Entity_DescribedComponent described *Entity_DescribedComponent
presentable *Entity_PresentableComponent presentable *Entity_PresentableComponent
@ -107,6 +112,7 @@ type BaseEntity struct {
equipped *Entity_EquippedComponent equipped *Entity_EquippedComponent
stats *Entity_StatsHolderComponent stats *Entity_StatsHolderComponent
damageable *Entity_HealthComponent damageable *Entity_HealthComponent
dropTable *Entity_DropTableComponent
} }
func (be *BaseEntity) UniqueId() uuid.UUID { func (be *BaseEntity) UniqueId() uuid.UUID {
@ -141,8 +147,12 @@ func (be *BaseEntity) HealthData() *Entity_HealthComponent {
return be.damageable return be.damageable
} }
func (be *BaseEntity) Speed() *Entity_SpeedComponent { func (be *BaseEntity) Behavior() *Entity_BehaviorComponent {
return be.speed return be.behavior
}
func (be *BaseEntity) DropTable() *Entity_DropTableComponent {
return be.dropTable
} }
func CreateEntity(components ...func(*BaseEntity)) *BaseEntity { func CreateEntity(components ...func(*BaseEntity)) *BaseEntity {
@ -202,7 +212,6 @@ func WithStats(baseStats map[Stat]int, statModifiers ...StatModifier) func(e *Ba
return func(e *BaseEntity) { return func(e *BaseEntity) {
e.stats = &Entity_StatsHolderComponent{ e.stats = &Entity_StatsHolderComponent{
BaseStats: baseStats, BaseStats: baseStats,
// StatModifiers: statModifiers,
} }
} }
} }
@ -217,10 +226,27 @@ func WithHealthData(health, maxHealth int, isDead bool) func(e *BaseEntity) {
} }
} }
func WithSpeed(speed int) func(e *BaseEntity) { func WithBehavior(speed int, behavior func(npc Entity) (complete, requeue bool)) func(e *BaseEntity) {
return func(e *BaseEntity) { return func(e *BaseEntity) {
e.speed = &Entity_SpeedComponent{ e.behavior = &Entity_BehaviorComponent{
Speed: speed, Speed: speed,
Behavior: func() (complete bool, requeue bool) {
return behavior(e)
},
}
}
}
func WithDropTable(table map[int]ItemSupplier) func(e *BaseEntity) {
dropTable := CreateLootTable()
for k, v := range table {
dropTable.Add(k, v)
}
return func(e *BaseEntity) {
e.dropTable = &Entity_DropTableComponent{
DropTable: dropTable,
} }
} }
} }

View file

@ -0,0 +1,160 @@
package model
import (
"fmt"
"mvvasilev/last_light/engine"
)
// func ProjectileBehavior() func(npc Entity) (complete bool, requeue bool) {
// return func(npc Entity) (complete bool, requeue bool) {
// }
// }
func HostileNPCBehavior(eventLog *engine.GameEventLog, dungeon *Dungeon, player *Player) func(npc Entity) (complete bool, requeue bool) {
return func(npc Entity) (complete bool, requeue bool) {
CalcPathToPlayerAndMove(25, eventLog, dungeon, npc, player)
return true, true
}
}
func CalcPathToPlayerAndMove(simulationDistance int, eventLog *engine.GameEventLog, dungeon *Dungeon, npc Entity, player *Player) {
if npc.Positioned().Position.DistanceSquared(player.Position()) > simulationDistance*simulationDistance {
return
}
if npc.HealthData().IsDead {
dungeon.CurrentLevel().DropEntity(npc.UniqueId())
return
}
playerVisibleAndInRange := false
hasLos, _ := HasLineOfSight(dungeon, npc.Positioned().Position, player.Position())
if npc.Positioned().Position.DistanceSquared(player.Position()) < 144 && hasLos {
playerVisibleAndInRange = true
}
if !playerVisibleAndInRange {
randomMove := Direction(engine.RandInt(int(DirectionNone), int(East)))
nextPos := npc.Positioned().Position
switch randomMove {
case North:
nextPos = nextPos.WithOffset(0, -1)
case South:
nextPos = nextPos.WithOffset(0, +1)
case West:
nextPos = nextPos.WithOffset(-1, 0)
case East:
nextPos = nextPos.WithOffset(+1, 0)
default:
return
}
if dungeon.CurrentLevel().IsTilePassable(nextPos.XY()) {
dungeon.CurrentLevel().MoveEntityTo(
npc.UniqueId(),
nextPos.X(),
nextPos.Y(),
)
}
return
}
if WithinHitRange(npc.Positioned().Position, player.Position()) {
ExecuteAttack(eventLog, npc, player)
}
pathToPlayer := engine.FindPath(
npc.Positioned().Position,
player.Position(),
12,
func(x, y int) bool {
if x == player.Position().X() && y == player.Position().Y() {
return true
}
return dungeon.CurrentLevel().IsTilePassable(x, y)
},
)
if pathToPlayer == nil {
return
}
nextPos, hasNext := pathToPlayer.Next()
if !hasNext {
return
}
if nextPos.Equals(player.Position()) {
return
}
dungeon.CurrentLevel().MoveEntityTo(npc.UniqueId(), nextPos.X(), nextPos.Y())
}
func HasLineOfSight(dungeon *Dungeon, start, end engine.Position) (hasLos bool, lastTile Tile) {
positions := engine.CastRay(start, end)
tile := dungeon.CurrentLevel().TileAt(positions[0].XY())
for _, p := range positions {
tile = dungeon.CurrentLevel().TileAt(p.XY())
if tile.Opaque() {
return false, tile
}
}
return true, tile
}
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 ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim Entity) {
hit, precision, evasion, dmg, dmgType := CalculateAttack(attacker, victim)
attackerName := "Unknown"
if attacker.Named() != nil {
attackerName = attacker.Named().Name
}
victimName := "Unknown"
if victim.Named() != nil {
victimName = victim.Named().Name
}
if !hit {
eventLog.Log(fmt.Sprintf("%s attacked %s, but missed ( %v Evasion vs %v Precision)", attackerName, victimName, evasion, precision))
return
}
victim.HealthData().Health -= dmg
if victim.HealthData().Health <= 0 {
victim.HealthData().IsDead = true
eventLog.Log(fmt.Sprintf("%s attacked %s, and was victorious ( %v Evasion vs %v Precision)", attackerName, victimName, evasion, precision))
return
}
eventLog.Log(fmt.Sprintf("%s attacked %s, and hit for %v %v damage", attackerName, victimName, dmg, DamageTypeName(dmgType)))
}
func CalculateAttack(attacker, victim Entity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) {
if attacker.Equipped() != nil && attacker.Equipped().Inventory.AtSlot(EquippedSlotDominantHand) != nil {
weapon := attacker.Equipped().Inventory.AtSlot(EquippedSlotDominantHand)
return PhysicalWeaponAttack(attacker, weapon, victim)
} else {
return UnarmedAttack(attacker, victim)
}
}

View file

@ -12,19 +12,28 @@ const (
ImpClaws specialItemType = 100_000 + iota ImpClaws specialItemType = 100_000 + iota
) )
func Entity_Imp(x, y int) Entity { func Entity_ArrowProjectile(startX, startY int, targetX, targetY int) Entity {
return CreateEntity(
WithName("Arrow"),
WithPosition(engine.PositionAt(startX, startY)),
)
}
func Entity_Imp(x, y int, behavior func(npc Entity) (complete, requeue bool)) Entity {
return CreateEntity( return CreateEntity(
WithName("Imp"), WithName("Imp"),
WithDescription("A fiery little creature"), WithDescription("A fiery little creature"),
WithHealthData(15, 15, false), WithHealthData(15, 15, false),
WithPosition(engine.PositionAt(x, y)), WithPosition(engine.PositionAt(x, y)),
WithPresentation('i', tcell.StyleDefault.Foreground(tcell.ColorDarkRed)), WithPresentation('i', tcell.StyleDefault.Foreground(tcell.ColorDarkRed)),
WithSpeed(11), WithBehavior(110, behavior),
WithStats(map[Stat]int{ WithStats(map[Stat]int{
Stat_Attributes_Constitution: 5, Stat_Attributes_Constitution: 5,
Stat_Attributes_Dexterity: 10, Stat_Attributes_Dexterity: 10,
Stat_Attributes_Strength: 5, Stat_Attributes_Strength: 5,
Stat_Attributes_Intelligence: 7, Stat_Attributes_Intelligence: 7,
Stat_ResistanceBonus_Magic_Fire: 5,
}), }),
WithInventory(BuildInventory( WithInventory(BuildInventory(
Inv_WithDominantHand(createBaseItem( Inv_WithDominantHand(createBaseItem(
@ -32,10 +41,62 @@ func Entity_Imp(x, y int) Entity {
'v', "|||", 'v', "|||",
tcell.StyleDefault, tcell.StyleDefault,
item_WithName("Claws", tcell.StyleDefault), item_WithName("Claws", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD4(1), DamageType_Physical_Slashing return RollD4(1), DamageType_Physical_Slashing
}), }),
)), )),
)), )),
) )
} }
func Entity_SkeletalKnight(x, y int, behavior func(npc Entity) (complete, requeue bool)) Entity {
return CreateEntity(
WithName("Skeletal Knight"),
WithDescription("Rattling in the dark..."),
WithHealthData(25, 25, false),
WithPosition(engine.PositionAt(x, y)),
WithPresentation('S', tcell.StyleDefault.Foreground(tcell.ColorAntiqueWhite)),
WithBehavior(150, behavior),
WithStats(map[Stat]int{
Stat_Attributes_Constitution: 10,
Stat_Attributes_Dexterity: 6,
Stat_Attributes_Strength: 12,
Stat_Attributes_Intelligence: 5,
Stat_ResistanceBonus_Physical_Bludgeoning: -2,
}),
WithInventory(BuildInventory(
Inv_WithDominantHand(Item_Longsword()),
)),
WithDropTable(map[int]ItemSupplier{
9: ItemSupplierOf(Item_Longsword()),
1: ItemSupplierOfGeneratedPrototype(Item_Longsword(), map[int]ItemRarity{1: ItemRarity_Legendary}),
}),
)
}
func Entity_SkeletalWarrior(x, y int, behavior func(npc Entity) (complete, requeue bool)) Entity {
return CreateEntity(
WithName("Skeletal Warrior"),
WithDescription("Rattling in the dark..."),
WithHealthData(25, 25, false),
WithPosition(engine.PositionAt(x, y)),
WithPresentation('S', tcell.StyleDefault.Foreground(tcell.ColorAntiqueWhite)),
WithBehavior(150, behavior),
WithStats(map[Stat]int{
Stat_Attributes_Constitution: 10,
Stat_Attributes_Dexterity: 6,
Stat_Attributes_Strength: 12,
Stat_Attributes_Intelligence: 5,
Stat_ResistanceBonus_Physical_Bludgeoning: -2,
}),
WithInventory(BuildInventory(
Inv_WithDominantHand(Item_Mace()),
)),
WithDropTable(map[int]ItemSupplier{
9: ItemSupplierOf(Item_Mace()),
1: ItemSupplierOfGeneratedPrototype(Item_Mace(), map[int]ItemRarity{1: ItemRarity_Legendary}),
}),
)
}

View file

@ -8,6 +8,8 @@ import (
type Player struct { type Player struct {
Entity Entity
skipNextTurn bool
} }
func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player { func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player {
@ -19,10 +21,12 @@ 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), WithBehavior(100, nil),
), ),
} }
p.Inventory().Push(Item_Bow())
p.HealthData().MaxHealth = BaseMaxHealth(p) p.HealthData().MaxHealth = BaseMaxHealth(p)
p.HealthData().Health = p.HealthData().MaxHealth p.HealthData().Health = p.HealthData().MaxHealth
@ -48,3 +52,15 @@ func (p *Player) Stats() *Entity_StatsHolderComponent {
func (p *Player) HealthData() *Entity_HealthComponent { func (p *Player) HealthData() *Entity_HealthComponent {
return p.Entity.HealthData() return p.Entity.HealthData()
} }
func (p *Player) DefaultSpeed() *Entity_BehaviorComponent {
return p.Entity.Behavior()
}
func (p *Player) SkipNextTurn(skip bool) {
p.skipNextTurn = skip
}
func (p *Player) IsNextTurnSkipped() bool {
return p.skipNextTurn
}

View file

@ -60,6 +60,7 @@ type Item_DescribedComponent struct {
} }
type Item_DamagingComponent struct { type Item_DamagingComponent struct {
IsRanged bool
DamageRoll func() (damage int, dmgType DamageType) DamageRoll func() (damage int, dmgType DamageType)
} }
@ -176,9 +177,10 @@ func item_WithEquippable(slot EquippedSlot) func(*BaseItem) {
} }
} }
func item_WithDamaging(damageFunc func() (damage int, dmgType DamageType)) func(*BaseItem) { func item_WithDamaging(isRanged bool, damageFunc func() (damage int, dmgType DamageType)) func(*BaseItem) {
return func(bi *BaseItem) { return func(bi *BaseItem) {
bi.damaging = &Item_DamagingComponent{ bi.damaging = &Item_DamagingComponent{
IsRanged: isRanged,
DamageRoll: damageFunc, DamageRoll: damageFunc,
} }
} }

View file

@ -175,7 +175,7 @@ func Item_Bow() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Bow", tcell.StyleDefault), item_WithName("Bow", tcell.StyleDefault),
item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault), item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(true, func() (damage int, dmgType DamageType) {
return RollD8(1), DamageType_Physical_Piercing return RollD8(1), DamageType_Physical_Piercing
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -192,7 +192,7 @@ func Item_Longsword() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Longsword", tcell.StyleDefault), item_WithName("Longsword", tcell.StyleDefault),
item_WithDescription("Deals 1d8 Slashing damage", tcell.StyleDefault), item_WithDescription("Deals 1d8 Slashing damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD8(1), DamageType_Physical_Slashing return RollD8(1), DamageType_Physical_Slashing
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -209,7 +209,7 @@ func Item_Club() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Club", tcell.StyleDefault), item_WithName("Club", tcell.StyleDefault),
item_WithDescription("Deals 1d8 Bludgeoning damage", tcell.StyleDefault), item_WithDescription("Deals 1d8 Bludgeoning damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD8(1), DamageType_Physical_Bludgeoning return RollD8(1), DamageType_Physical_Bludgeoning
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -226,7 +226,7 @@ func Item_Dagger() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Dagger", tcell.StyleDefault), item_WithName("Dagger", tcell.StyleDefault),
item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault), item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Piercing return RollD6(1), DamageType_Physical_Piercing
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -241,9 +241,9 @@ func Item_Handaxe() Item {
" ─╗", " ─╗",
tcell.StyleDefault.Foreground(tcell.ColorSilver), tcell.StyleDefault.Foreground(tcell.ColorSilver),
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Dagger", tcell.StyleDefault), item_WithName("Handaxe", tcell.StyleDefault),
item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault), item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Piercing return RollD6(1), DamageType_Physical_Piercing
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -260,7 +260,7 @@ func Item_Javelin() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Javelin", tcell.StyleDefault), item_WithName("Javelin", tcell.StyleDefault),
item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault), item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Piercing return RollD6(1), DamageType_Physical_Piercing
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -277,7 +277,7 @@ func Item_LightHammer() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Light Hammer", tcell.StyleDefault), item_WithName("Light Hammer", tcell.StyleDefault),
item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault), item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Bludgeoning return RollD6(1), DamageType_Physical_Bludgeoning
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -294,7 +294,7 @@ func Item_Mace() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Mace", tcell.StyleDefault), item_WithName("Mace", tcell.StyleDefault),
item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault), item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Bludgeoning return RollD6(1), DamageType_Physical_Bludgeoning
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -311,7 +311,7 @@ func Item_Quarterstaff() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Quarterstaff", tcell.StyleDefault), item_WithName("Quarterstaff", tcell.StyleDefault),
item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault), item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Bludgeoning return RollD6(1), DamageType_Physical_Bludgeoning
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -328,7 +328,7 @@ func Item_Sickle() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Sickle", tcell.StyleDefault), item_WithName("Sickle", tcell.StyleDefault),
item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault), item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Slashing return RollD6(1), DamageType_Physical_Slashing
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
@ -345,7 +345,7 @@ func Item_Spear() Item {
item_WithQuantity(1, 1), item_WithQuantity(1, 1),
item_WithName("Spear", tcell.StyleDefault), item_WithName("Spear", tcell.StyleDefault),
item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault), item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault),
item_WithDamaging(func() (damage int, dmgType DamageType) { item_WithDamaging(false, func() (damage int, dmgType DamageType) {
return RollD8(1), DamageType_Physical_Piercing return RollD8(1), DamageType_Physical_Piercing
}), }),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),

View file

@ -13,8 +13,52 @@ import (
const MaxNumberOfModifiers = 6 const MaxNumberOfModifiers = 6
type RarityTable struct {
table []ItemRarity
}
func CreateRarityTable() *RarityTable {
return &RarityTable{
table: make([]ItemRarity, 0),
}
}
func (igt *RarityTable) Add(weight int, rarity ItemRarity) {
for range weight {
igt.table = append(igt.table, rarity)
}
}
func (igt *RarityTable) Generate() ItemRarity {
return igt.table[rand.Intn(len(igt.table))]
}
type ItemSupplier func() Item type ItemSupplier func() Item
func EmptyItemSupplier() ItemSupplier {
return func() Item {
return nil
}
}
func ItemSupplierOf(item Item) ItemSupplier {
return func() Item {
return item
}
}
func ItemSupplierOfGeneratedPrototype(prototype Item, rarities map[int]ItemRarity) ItemSupplier {
rarityTable := CreateRarityTable()
for k, v := range rarities {
rarityTable.Add(k, v)
}
return func() Item {
return GenerateItemOfTypeAndRarity(prototype, rarityTable.Generate())
}
}
type LootTable struct { type LootTable struct {
table []ItemSupplier table []ItemSupplier
} }
@ -285,7 +329,7 @@ func GenerateItemOfTypeAndRarity(prototype Item, rarity ItemRarity) Item {
prototype.Style(), prototype.Style(),
item_WithName(name, style), item_WithName(name, style),
item_WithDescription(prototype.Described().Description, prototype.Described().Style), item_WithDescription(prototype.Described().Description, prototype.Described().Style),
item_WithDamaging(prototype.Damaging().DamageRoll), item_WithDamaging(prototype.Damaging().IsRanged, prototype.Damaging().DamageRoll),
item_WithEquippable(prototype.Equippable().Slot), item_WithEquippable(prototype.Equippable().Slot),
item_WithStatModifiers(statModifiers), item_WithStatModifiers(statModifiers),
item_WithMetaTypes(metaTypes), item_WithMetaTypes(metaTypes),

View file

@ -46,7 +46,8 @@ const (
Stat_ResistanceBonus_Magic_Acid Stat = 280 Stat_ResistanceBonus_Magic_Acid Stat = 280
Stat_ResistanceBonus_Magic_Poison Stat = 290 Stat_ResistanceBonus_Magic_Poison Stat = 290
Stat_MaxHealthBonus Stat = 140 Stat_MaxHealthBonus Stat = 1000
Stat_SpeedBonus Stat = 1010
) )
func StatLongName(stat Stat) string { func StatLongName(stat Stat) string {
@ -273,6 +274,16 @@ func statValue(stats *Entity_StatsHolderComponent, stat Stat) int {
return stats.BaseStats[stat] return stats.BaseStats[stat]
} }
func statModifierValue(statModifiers []StatModifier, stat Stat) int {
for _, sm := range statModifiers {
if sm.Stat == stat {
return sm.Bonus
}
}
return 0
}
// Base Max Health is determined from constitution: // Base Max Health is determined from constitution:
// 5*Constitution + Max Health Bonus // 5*Constitution + Max Health Bonus
func BaseMaxHealth(entity Entity) int { func BaseMaxHealth(entity Entity) int {
@ -340,7 +351,7 @@ func UnarmedDamage(attacker Entity) int {
func PhysicalWeaponDamage(attacker Entity, weapon Item, victim Entity) (totalDamage int, dmgType DamageType) { func PhysicalWeaponDamage(attacker Entity, weapon Item, victim Entity) (totalDamage int, dmgType DamageType) {
if attacker.Stats() == nil || weapon.Damaging() == nil || victim.Stats() == nil { if attacker.Stats() == nil || weapon.Damaging() == nil || victim.Stats() == nil {
return 0, DamageType_Physical_Unarmed return UnarmedDamage(attacker), DamageType_Physical_Unarmed
} }
totalDamage, dmgType = weapon.Damaging().DamageRoll() totalDamage, dmgType = weapon.Damaging().DamageRoll()
@ -350,6 +361,14 @@ func PhysicalWeaponDamage(attacker Entity, weapon Item, victim Entity) (totalDam
totalDamage = totalDamage + statValue(attacker.Stats(), bonusDmgStat) - statValue(victim.Stats(), dmgResistStat) totalDamage = totalDamage + statValue(attacker.Stats(), bonusDmgStat) - statValue(victim.Stats(), dmgResistStat)
if weapon.StatModifier() != nil {
totalDamage += statModifierValue(weapon.StatModifier().StatModifiers, bonusDmgStat)
}
if totalDamage <= 0 {
return 0, dmgType
}
return return
} }

View file

@ -90,7 +90,7 @@ func (d *Dungeon) HasNextLevel() bool {
type DungeonLevel struct { type DungeonLevel struct {
ground Map ground Map
entitiesByPosition map[engine.Position]Entity entitiesByPosition map[engine.Position][]Entity
entities map[uuid.UUID]Entity entities map[uuid.UUID]Entity
} }
@ -142,7 +142,7 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) (dLevel *Dun
dLevel = &DungeonLevel{ dLevel = &DungeonLevel{
ground: groundLevel, ground: groundLevel,
entities: map[uuid.UUID]Entity{}, entities: map[uuid.UUID]Entity{},
entitiesByPosition: map[engine.Position]Entity{}, entitiesByPosition: map[engine.Position][]Entity{},
} }
if groundLevel.Rooms() == nil { if groundLevel.Rooms() == nil {
@ -238,7 +238,11 @@ func (d *DungeonLevel) AddEntity(entity Entity) {
d.entities[entity.UniqueId()] = entity d.entities[entity.UniqueId()] = entity
if entity.Positioned() != nil { if entity.Positioned() != nil {
d.entitiesByPosition[entity.Positioned().Position] = entity if d.entitiesByPosition[entity.Positioned().Position] == nil {
d.entitiesByPosition[entity.Positioned().Position] = []Entity{entity}
} else {
d.entitiesByPosition[entity.Positioned().Position] = append(d.entitiesByPosition[entity.Positioned().Position], entity)
}
} }
} }
@ -253,7 +257,11 @@ func (d *DungeonLevel) MoveEntityTo(uuid uuid.UUID, x, y int) {
ent.Positioned().Position = engine.PositionAt(x, y) ent.Positioned().Position = engine.PositionAt(x, y)
d.entitiesByPosition[ent.Positioned().Position] = ent if d.entitiesByPosition[ent.Positioned().Position] == nil {
d.entitiesByPosition[ent.Positioned().Position] = []Entity{ent}
} else {
d.entitiesByPosition[ent.Positioned().Position] = append(d.entitiesByPosition[ent.Positioned().Position], ent)
}
} }
func (d *DungeonLevel) RemoveEntityAt(x, y int) { func (d *DungeonLevel) RemoveEntityAt(x, y int) {
@ -295,7 +303,7 @@ func (d *DungeonLevel) TileAt(x, y int) Tile {
tile := Map_TileAt(d.ground, x, y) tile := Map_TileAt(d.ground, x, y)
if entity != nil { if entity != nil {
return CreateTileFromPrototype(tile, Tile_WithEntity(entity)) return CreateTileFromPrototype(tile, Tile_WithEntities(entity))
} }
return tile return tile
@ -308,14 +316,14 @@ func (d *DungeonLevel) IsTilePassable(x, y int) bool {
tile := d.TileAt(x, y) tile := d.TileAt(x, y)
if tile.Entity() != nil { if tile.Entities() != nil {
return false return false
} }
return tile.Passable() return tile.Passable()
} }
func (d *DungeonLevel) EntityAt(x, y int) (e Entity) { func (d *DungeonLevel) EntitiesAt(x, y int) (e []Entity) {
return d.entitiesByPosition[engine.PositionAt(x, y)] return d.entitiesByPosition[engine.PositionAt(x, y)]
} }

View file

@ -1,7 +1,10 @@
package model package model
import ( import (
"slices"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/google/uuid"
) )
type Material uint type Material uint
@ -23,7 +26,7 @@ type Tile_ItemComponent struct {
} }
type Tile_EntityComponent struct { type Tile_EntityComponent struct {
Entity Entity Entities []Entity
} }
type Tile interface { type Tile interface {
@ -37,9 +40,9 @@ type Tile interface {
RemoveItem() RemoveItem()
WithItem(item Item) WithItem(item Item)
Entity() *Tile_EntityComponent Entities() *Tile_EntityComponent
RemoveEntity() RemoveEntity(uuid uuid.UUID)
WithEntity(entity Entity) AddEntity(entity Entity)
} }
type BaseTile struct { type BaseTile struct {
@ -49,8 +52,8 @@ type BaseTile struct {
material Material material Material
passable, opaque, transparent bool passable, opaque, transparent bool
item *Tile_ItemComponent item *Tile_ItemComponent
entity *Tile_EntityComponent entities *Tile_EntityComponent
} }
func CreateTileFromPrototype(prototype Tile, components ...func(*BaseTile)) Tile { func CreateTileFromPrototype(prototype Tile, components ...func(*BaseTile)) Tile {
@ -118,24 +121,46 @@ func (t *BaseTile) WithItem(item Item) {
} }
} }
func (t *BaseTile) Entity() *Tile_EntityComponent { func (t *BaseTile) Entities() *Tile_EntityComponent {
return t.entity return t.entities
} }
func (t *BaseTile) RemoveEntity() { func (t *BaseTile) RemoveEntity(uuid uuid.UUID) {
t.entity = nil if t.entities == nil {
} return
func (t *BaseTile) WithEntity(entity Entity) {
t.entity = &Tile_EntityComponent{
Entity: entity,
} }
t.entities.Entities = slices.DeleteFunc(t.entities.Entities, func(e Entity) bool { return e.UniqueId() == uuid })
}
func (t *BaseTile) AddEntity(entity Entity) {
if t.entities == nil {
t.entities = &Tile_EntityComponent{
Entities: []Entity{
entity,
},
}
return
}
t.entities.Entities = append(t.entities.Entities, entity)
} }
func Tile_WithEntity(entity Entity) func(*BaseTile) { func Tile_WithEntity(entity Entity) func(*BaseTile) {
return func(bt *BaseTile) { return func(bt *BaseTile) {
bt.entity = &Tile_EntityComponent{ bt.entities = &Tile_EntityComponent{
Entity: entity, Entities: []Entity{
entity,
},
}
}
}
func Tile_WithEntities(entities []Entity) func(*BaseTile) {
return func(bt *BaseTile) {
bt.entities = &Tile_EntityComponent{
Entities: entities,
} }
} }
} }

View file

@ -1,22 +1,213 @@
package state package state
import ( import (
"fmt"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/systems"
"time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
) )
const CursorRune = '+'
const CursorBlinkTime = 200 // Blink cursor every 200ms, showing what's under it
type LookState struct { type LookState struct {
prevState GameState
inputSystem *systems.InputSystem
turnSystem *systems.TurnSystem
eventLog *engine.GameEventLog
player *model.Player
dungeon *model.Dungeon
showCursor bool
cursorPos engine.Position
lastCursorBlinkTime time.Time
} }
func (ls *LookState) OnInput(e *tcell.EventKey) { func CreateLookState(prevState GameState, eventLog *engine.GameEventLog, dungeon *model.Dungeon, inputSystem *systems.InputSystem, turnSystem *systems.TurnSystem, player *model.Player) *LookState {
panic("not implemented") // TODO: Implement return &LookState{
prevState: prevState,
inputSystem: inputSystem,
turnSystem: turnSystem,
dungeon: dungeon,
player: player,
eventLog: eventLog,
cursorPos: engine.PositionAt(0, 0),
lastCursorBlinkTime: time.Now(),
}
}
func (ls *LookState) InputContext() systems.InputContext {
return systems.InputContext_Look
} }
func (ls *LookState) OnTick(dt int64) GameState { func (ls *LookState) OnTick(dt int64) GameState {
panic("not implemented") // TODO: Implement switch ls.inputSystem.NextAction() {
case systems.InputAction_Move_North:
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.North))
case systems.InputAction_Move_South:
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.South))
case systems.InputAction_Move_East:
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.East))
case systems.InputAction_Move_West:
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.West))
case systems.InputAction_Describe:
ls.Describe()
case systems.InputAction_Shoot:
ls.ShootEquippedWeapon()
case systems.InputAction_Menu_Exit:
return ls.prevState
}
return ls
}
func (ls *LookState) ShootEquippedWeapon() {
weapon := ls.player.Inventory().AtSlot(model.EquippedSlotDominantHand)
if weapon == nil {
ls.eventLog.Log("You don't have anything equipped!")
return
}
if weapon.Damaging() == nil {
ls.eventLog.Log("Item unusable")
return
}
damaging := weapon.Damaging()
if !damaging.IsRanged {
ls.eventLog.Log("Equipped weapon is not ranged!")
return
}
// TODO: Projectiles
ls.player.SkipNextTurn(true)
ls.turnSystem.NextTurn()
}
func (ls *LookState) Describe() {
dX, dY := ls.lookCursorCoordsToDungeonCoords()
isVisibleFromPlayer, lastTile := model.HasLineOfSight(ls.dungeon, ls.player.Position(), engine.PositionAt(dX, dY))
if !isVisibleFromPlayer {
materialName, _ := materialToDescription(lastTile.Material())
ls.eventLog.Log(fmt.Sprintf("%s obscures your view", materialName))
return
}
tile := ls.dungeon.CurrentLevel().TileAt(dX, dY)
entities := tile.Entities()
if entities != nil {
ls.DescribeEntities(entities.Entities)
return
}
item := tile.Item()
if item != nil {
ls.DescribeItem(item.Item)
return
}
materialName, materialDesc := materialToDescription(tile.Material())
ls.eventLog.Log(fmt.Sprintf("%s: %s", materialName, materialDesc))
}
func (ls *LookState) DescribeEntities(entities []model.Entity) {
if entities == nil {
return
}
for _, entity := range entities {
if entity == ls.player {
ls.eventLog.Log("You")
continue
}
if entity.Named() == nil {
continue
}
if entity.Described() != nil {
ls.eventLog.Log(fmt.Sprintf("%s: %s", entity.Named().Name, entity.Described().Description))
} else {
ls.eventLog.Log(entity.Named().Name)
}
}
}
func (ls *LookState) DescribeItem(item model.Item) {
if item == nil {
return
}
if item.Named() == nil {
return
}
if item.Described() != nil {
ls.eventLog.Log(fmt.Sprintf("%s: %s", item.Named().Name, item.Described().Description))
} else {
ls.eventLog.Log(item.Named().Name)
}
}
func materialToDescription(material model.Material) (name, description string) {
switch material {
case model.MaterialVoid:
return "Void", "Who knows what lurks here..."
case model.MaterialWall:
return "Wall", "Mediocre masonry"
case model.MaterialGround:
return "Ground", "Try not to trip"
}
return "Void", "Who knows what lurks here..."
}
func (ls *LookState) lookCursorCoordsToScreenCoords() (sX, xY int) {
x, y := ls.cursorPos.XY()
middleOfScreenX, middleOfScreenY := engine.TERMINAL_SIZE_WIDTH/2, engine.TERMINAL_SIZE_HEIGHT/2
return middleOfScreenX + x, middleOfScreenY + y
}
func (ls *LookState) lookCursorCoordsToDungeonCoords() (sX, xY int) {
x, y := ls.cursorPos.XY()
playerX, playerY := ls.player.Position().XY()
return playerX + x, playerY + y
} }
func (ls *LookState) CollectDrawables() []engine.Drawable { func (ls *LookState) CollectDrawables() []engine.Drawable {
panic("not implemented") // TODO: Implement drawables := append(ls.prevState.CollectDrawables(), engine.CreateDrawingInstructions(func(v views.View) {
if time.Since(ls.lastCursorBlinkTime).Milliseconds() >= CursorBlinkTime {
ls.showCursor = !ls.showCursor
ls.lastCursorBlinkTime = time.Now()
}
if ls.showCursor {
x, y := ls.lookCursorCoordsToScreenCoords()
v.SetContent(x, y, CursorRune, nil, tcell.StyleDefault)
}
}))
return drawables
} }

View file

@ -1,7 +1,6 @@
package state package state
import ( import (
"fmt"
"math/rand" "math/rand"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model" "mvvasilev/last_light/game/model"
@ -51,11 +50,11 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
playerStats, playerStats,
) )
s.turnSystem.Schedule(10, func() (complete bool, requeue bool) { s.turnSystem.Schedule(s.player.DefaultSpeed().Speed, func() (complete bool, requeue bool) {
requeue = true requeue = true
complete = false complete = false
if s.player.HealthData().IsDead { if s.player.HealthData().Health <= 0 || s.player.HealthData().IsDead {
s.nextGameState = CreateGameOverState(inputSystem) s.nextGameState = CreateGameOverState(inputSystem)
} }
@ -64,40 +63,34 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem) s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem)
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_EnterLookMode:
s.viewShortLogs = !s.viewShortLogs
s.nextGameState = CreateLookState(s, s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player)
case systems.InputAction_PickUpItem: case systems.InputAction_PickUpItem:
complete = PickUpItemUnderPlayer(s.eventLog, s.dungeon, s.player) complete = PickUpItemUnderPlayer(s.eventLog, s.dungeon, s.player)
case systems.InputAction_Interact: case systems.InputAction_Interact:
complete = s.InteractBelowPlayer() complete = s.InteractBelowPlayer()
case systems.InputAction_OpenLogs: case systems.InputAction_OpenLogs:
s.viewShortLogs = !s.viewShortLogs s.viewShortLogs = !s.viewShortLogs
case systems.InputAction_MovePlayer_East: case systems.InputAction_Move_East:
complete = s.MovePlayer(model.East) complete = s.MovePlayer(model.East)
case systems.InputAction_MovePlayer_West: case systems.InputAction_Move_West:
complete = s.MovePlayer(model.West) complete = s.MovePlayer(model.West)
case systems.InputAction_MovePlayer_North: case systems.InputAction_Move_North:
complete = s.MovePlayer(model.North) complete = s.MovePlayer(model.North)
case systems.InputAction_MovePlayer_South: case systems.InputAction_Move_South:
complete = s.MovePlayer(model.South) complete = s.MovePlayer(model.South)
default: default:
} }
if s.player.IsNextTurnSkipped() {
s.player.SkipNextTurn(false)
complete = true
}
return 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.turnSystem.Schedule(20, func() (complete bool, requeue bool) {
// s.CalcPathToPlayerAndMove()
// return true, true
// })
s.eventLog = engine.CreateGameEventLog(100) s.eventLog = engine.CreateGameEventLog(100)
s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault) s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault)
@ -107,26 +100,25 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
entityTable := model.CreateEntityTable() entityTable := model.CreateEntityTable()
entityTable.Add(1, func(x, y int) model.Entity { return model.Entity_Imp(x, y) }) entityTable.Add(1, func(x, y int) model.Entity {
return model.Entity_Imp(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player))
})
entityTable.Add(1, func(x, y int) model.Entity {
return model.Entity_SkeletalKnight(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player))
})
entityTable.Add(1, func(x, y int) model.Entity {
return model.Entity_SkeletalWarrior(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player))
})
s.npcs = SpawnNPCs(s.dungeon, 7, entityTable) s.npcs = SpawnNPCs(s.dungeon, 7, entityTable)
for _, npc := range s.npcs { for _, npc := range s.npcs {
speed := 10 if npc.Behavior() != nil {
speed := npc.Behavior().Speed
if npc.Speed() != nil { s.turnSystem.Schedule(speed, npc.Behavior().Behavior)
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),
s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position, s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position,
@ -170,7 +162,7 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction)) newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction))
ent := ps.dungeon.CurrentLevel().EntityAt(newPlayerPos.XY()) ent := ps.dungeon.CurrentLevel().EntitiesAt(newPlayerPos.XY())[0]
// We are moving into an entity with health data. Attack it. // We are moving into an entity with health data. Attack it.
if ent != nil && ent.HealthData() != nil { if ent != nil && ent.HealthData() != nil {
@ -179,7 +171,7 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
return false return false
} }
ExecuteAttack(ps.eventLog, ps.player, ent) model.ExecuteAttack(ps.eventLog, ps.player, ent)
return true return true
} }
@ -192,53 +184,12 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
return true return true
} else { } else {
ps.eventLog.Log("You bump into an impassable object" + model.DirectionName(direction)) ps.eventLog.Log("You bump into an impassable object")
return false return false
} }
} }
func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim model.Entity) {
hit, precision, evasion, dmg, dmgType := CalculateAttack(attacker, victim)
attackerName := "Unknown"
if attacker.Named() != nil {
attackerName = attacker.Named().Name
}
victimName := "Unknown"
if victim.Named() != nil {
victimName = victim.Named().Name
}
if !hit {
eventLog.Log(fmt.Sprintf("%s attacked %s, but missed ( %v Evasion vs %v Precision)", attackerName, victimName, evasion, precision))
return
}
victim.HealthData().Health -= dmg
if victim.HealthData().Health <= 0 {
victim.HealthData().IsDead = true
eventLog.Log(fmt.Sprintf("%s attacked %s, and was victorious ( %v Evasion vs %v Precision)", attackerName, victimName, evasion, precision))
return
}
eventLog.Log(fmt.Sprintf("%s attacked %s, and hit for %v %v damage", attackerName, victimName, dmg, model.DamageTypeName(dmgType)))
}
func CalculateAttack(attacker, victim model.Entity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType model.DamageType) {
if attacker.Equipped() != nil && attacker.Equipped().Inventory.AtSlot(model.EquippedSlotDominantHand) != nil {
weapon := attacker.Equipped().Inventory.AtSlot(model.EquippedSlotDominantHand)
return model.PhysicalWeaponAttack(attacker, weapon, victim)
} else {
return model.UnarmedAttack(attacker, victim)
}
}
func (ps *PlayingState) InteractBelowPlayer() (success bool) { func (ps *PlayingState) InteractBelowPlayer() (success bool) {
playerPos := ps.player.Position() playerPos := ps.player.Position()
@ -356,101 +307,6 @@ func PickUpItemUnderPlayer(eventLog *engine.GameEventLog, dungeon *model.Dungeon
return true return true
} }
func HasLineOfSight(dungeon *model.Dungeon, start, end engine.Position) bool {
positions := engine.CastRay(start, end)
for _, p := range positions {
if dungeon.CurrentLevel().IsGroundTileOpaque(p.XY()) {
return false
}
}
return true
}
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 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 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 := npc.Positioned().Position
switch randomMove {
case model.North:
nextPos = nextPos.WithOffset(0, -1)
case model.South:
nextPos = nextPos.WithOffset(0, +1)
case model.West:
nextPos = nextPos.WithOffset(-1, 0)
case model.East:
nextPos = nextPos.WithOffset(+1, 0)
default:
return
}
if dungeon.CurrentLevel().IsTilePassable(nextPos.XY()) {
dungeon.CurrentLevel().MoveEntityTo(
npc.UniqueId(),
nextPos.X(),
nextPos.Y(),
)
}
return
}
if WithinHitRange(npc.Positioned().Position, player.Position()) {
ExecuteAttack(eventLog, npc, player)
}
pathToPlayer := engine.FindPath(
npc.Positioned().Position,
player.Position(),
12,
func(x, y int) bool {
if x == player.Position().X() && y == player.Position().Y() {
return true
}
return dungeon.CurrentLevel().IsTilePassable(x, y)
},
)
if pathToPlayer == nil {
return
}
nextPos, hasNext := pathToPlayer.Next()
if !hasNext {
return
}
if nextPos.Equals(player.Position()) {
return
}
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) {
ps.nextGameState = ps ps.nextGameState = ps
@ -477,8 +333,9 @@ func (ps *PlayingState) CollectDrawables() []engine.Drawable {
tile := visibilityMap[engine.PositionAt(x, y)] tile := visibilityMap[engine.PositionAt(x, y)]
if tile != nil { if tile != nil {
if tile.Entity() != nil {
return tile.Entity().Entity.Presentable().Rune, tile.Entity().Entity.Presentable().Style if tile.Entities() != nil {
return tile.Entities().Entities[0].Presentable().Rune, tile.Entities().Entities[0].Presentable().Style
} }
if tile.Item() != nil { if tile.Item() != nil {

View file

@ -12,6 +12,7 @@ const (
InputContext_Play = "play" InputContext_Play = "play"
InputContext_Menu = "menu" InputContext_Menu = "menu"
InputContext_Inventory = "inventory" InputContext_Inventory = "inventory"
InputContext_Look = "look"
) )
type InputKey string type InputKey string
@ -25,10 +26,10 @@ type InputAction int
const ( const (
InputAction_None InputAction = iota InputAction_None InputAction = iota
InputAction_MovePlayer_North InputAction_Move_North
InputAction_MovePlayer_South InputAction_Move_South
InputAction_MovePlayer_East InputAction_Move_East
InputAction_MovePlayer_West InputAction_Move_West
InputAction_Interact InputAction_Interact
InputAction_OpenInventory InputAction_OpenInventory
@ -36,6 +37,10 @@ const (
InputAction_OpenLogs InputAction_OpenLogs
InputAction_DropItem InputAction_DropItem
InputAction_InteractItem InputAction_InteractItem
InputAction_UseOn
InputAction_Describe
InputAction_EnterLookMode
InputAction_Shoot
InputAction_PauseGame InputAction_PauseGame
@ -58,15 +63,16 @@ type InputSystem struct {
func CreateInputSystemWithDefaultBindings() *InputSystem { func CreateInputSystemWithDefaultBindings() *InputSystem {
return &InputSystem{ return &InputSystem{
keyBindings: map[InputKey]InputAction{ keyBindings: map[InputKey]InputAction{
InputKeyOf(InputContext_Play, 0, tcell.KeyUp, 0): InputAction_MovePlayer_North, InputKeyOf(InputContext_Play, 0, tcell.KeyUp, 0): InputAction_Move_North,
InputKeyOf(InputContext_Play, 0, tcell.KeyDown, 0): InputAction_MovePlayer_South, InputKeyOf(InputContext_Play, 0, tcell.KeyDown, 0): InputAction_Move_South,
InputKeyOf(InputContext_Play, 0, tcell.KeyLeft, 0): InputAction_MovePlayer_West, InputKeyOf(InputContext_Play, 0, tcell.KeyLeft, 0): InputAction_Move_West,
InputKeyOf(InputContext_Play, 0, tcell.KeyRight, 0): InputAction_MovePlayer_East, InputKeyOf(InputContext_Play, 0, tcell.KeyRight, 0): InputAction_Move_East,
InputKeyOf(InputContext_Play, 0, tcell.KeyEsc, 0): InputAction_PauseGame, InputKeyOf(InputContext_Play, 0, tcell.KeyEsc, 0): InputAction_PauseGame,
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'i'): InputAction_OpenInventory, InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'i'): InputAction_OpenInventory,
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'l'): InputAction_OpenLogs, InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'l'): InputAction_OpenLogs,
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'e'): InputAction_Interact, InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'e'): InputAction_Interact,
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'p'): InputAction_PickUpItem, InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'p'): InputAction_PickUpItem,
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'k'): InputAction_EnterLookMode,
InputKeyOf(InputContext_Menu, 0, tcell.KeyESC, 0): InputAction_Menu_Exit, InputKeyOf(InputContext_Menu, 0, tcell.KeyESC, 0): InputAction_Menu_Exit,
InputKeyOf(InputContext_Menu, 0, tcell.KeyLeft, 0): InputAction_Menu_HighlightLeft, InputKeyOf(InputContext_Menu, 0, tcell.KeyLeft, 0): InputAction_Menu_HighlightLeft,
InputKeyOf(InputContext_Menu, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight, InputKeyOf(InputContext_Menu, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight,
@ -81,6 +87,13 @@ func CreateInputSystemWithDefaultBindings() *InputSystem {
InputKeyOf(InputContext_Inventory, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight, InputKeyOf(InputContext_Inventory, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyUp, 0): InputAction_Menu_HighlightUp, InputKeyOf(InputContext_Inventory, 0, tcell.KeyUp, 0): InputAction_Menu_HighlightUp,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyDown, 0): InputAction_Menu_HighlightDown, InputKeyOf(InputContext_Inventory, 0, tcell.KeyDown, 0): InputAction_Menu_HighlightDown,
InputKeyOf(InputContext_Look, 0, tcell.KeyUp, 0): InputAction_Move_North,
InputKeyOf(InputContext_Look, 0, tcell.KeyDown, 0): InputAction_Move_South,
InputKeyOf(InputContext_Look, 0, tcell.KeyLeft, 0): InputAction_Move_West,
InputKeyOf(InputContext_Look, 0, tcell.KeyRight, 0): InputAction_Move_East,
InputKeyOf(InputContext_Look, 0, tcell.KeyRune, 'd'): InputAction_Describe,
InputKeyOf(InputContext_Look, 0, tcell.KeyRune, 'a'): InputAction_Shoot,
InputKeyOf(InputContext_Look, 0, tcell.KeyESC, 0): InputAction_Menu_Exit,
}, },
} }
} }

View file

@ -53,33 +53,48 @@ func (uihp *UIHealthBar) Draw(v views.View) {
uihp.window.Draw(v) uihp.window.Draw(v)
stages := []rune{'█', '▓', '▒', '░'} // 0 = 1.0, 1 = 0.75, 2 = 0.5, 3 = 0.25 stages := []string{"█", "▓", "▒", "░"} // 0 = 1.0, 1 = 0.75, 2 = 0.5, 3 = 0.25
percentage := (float64(w) - 2.0) * (float64(uihp.player.HealthData().Health) / float64(uihp.player.HealthData().MaxHealth)) percentage := (float64(w) - 2.0) * (float64(uihp.player.HealthData().Health) / float64(uihp.player.HealthData().MaxHealth))
whole := math.Trunc(percentage) whole := math.Trunc(percentage)
last := percentage - whole last := percentage - whole
hpBar := ""
hpStyle := tcell.StyleDefault.Foreground(tcell.ColorIndianRed) hpStyle := tcell.StyleDefault.Foreground(tcell.ColorIndianRed)
for i := range int(whole) { for range int(whole) {
v.SetContent(x+1+i, y+1, stages[0], nil, hpStyle) hpBar += stages[0]
} }
if last > 0.0 { lastRune := func() string {
if last <= 0.0 {
return ""
}
if last <= 0.25 { if last <= 0.25 {
v.SetContent(x+1+int(whole), y+1, stages[3], nil, hpStyle) return stages[3]
} }
if last <= 0.50 { if last <= 0.50 {
v.SetContent(x+1+int(whole), y+1, stages[2], nil, hpStyle) return stages[2]
} }
if last <= 0.75 { if last <= 0.75 {
v.SetContent(x+1+int(whole), y+1, stages[1], nil, hpStyle) return stages[1]
} }
if last <= 1.00 {
return stages[0]
}
return ""
} }
hpBar += lastRune()
engine.DrawText(x+1, y+1, hpBar, hpStyle, v)
hpText := fmt.Sprintf("%v/%v", uihp.player.HealthData().Health, uihp.player.HealthData().MaxHealth) hpText := fmt.Sprintf("%v/%v", uihp.player.HealthData().Health, uihp.player.HealthData().MaxHealth)
engine.DrawText( engine.DrawText(