mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-19 12:49:52 +03:00
Multiple entities per tile, look state, entity behavior moved
This commit is contained in:
parent
fb50992ce4
commit
e8f3c6ca9e
14 changed files with 685 additions and 248 deletions
|
@ -70,11 +70,11 @@ type Entity_EquippedComponent struct {
|
|||
|
||||
type Entity_StatsHolderComponent struct {
|
||||
BaseStats map[Stat]int
|
||||
// StatModifiers []StatModifier
|
||||
}
|
||||
|
||||
type Entity_SpeedComponent struct {
|
||||
type Entity_BehaviorComponent struct {
|
||||
Speed int
|
||||
Behavior func() (complete, requeue bool)
|
||||
}
|
||||
|
||||
type Entity_HealthComponent struct {
|
||||
|
@ -83,10 +83,14 @@ type Entity_HealthComponent struct {
|
|||
IsDead bool
|
||||
}
|
||||
|
||||
type Entity_DropTableComponent struct {
|
||||
DropTable *LootTable
|
||||
}
|
||||
|
||||
type Entity interface {
|
||||
UniqueId() uuid.UUID
|
||||
|
||||
Speed() *Entity_SpeedComponent
|
||||
Behavior() *Entity_BehaviorComponent
|
||||
Named() *Entity_NamedComponent
|
||||
Described() *Entity_DescribedComponent
|
||||
Presentable() *Entity_PresentableComponent
|
||||
|
@ -94,12 +98,13 @@ type Entity interface {
|
|||
Equipped() *Entity_EquippedComponent
|
||||
Stats() *Entity_StatsHolderComponent
|
||||
HealthData() *Entity_HealthComponent
|
||||
DropTable() *Entity_DropTableComponent
|
||||
}
|
||||
|
||||
type BaseEntity struct {
|
||||
id uuid.UUID
|
||||
|
||||
speed *Entity_SpeedComponent
|
||||
behavior *Entity_BehaviorComponent
|
||||
named *Entity_NamedComponent
|
||||
described *Entity_DescribedComponent
|
||||
presentable *Entity_PresentableComponent
|
||||
|
@ -107,6 +112,7 @@ type BaseEntity struct {
|
|||
equipped *Entity_EquippedComponent
|
||||
stats *Entity_StatsHolderComponent
|
||||
damageable *Entity_HealthComponent
|
||||
dropTable *Entity_DropTableComponent
|
||||
}
|
||||
|
||||
func (be *BaseEntity) UniqueId() uuid.UUID {
|
||||
|
@ -141,8 +147,12 @@ func (be *BaseEntity) HealthData() *Entity_HealthComponent {
|
|||
return be.damageable
|
||||
}
|
||||
|
||||
func (be *BaseEntity) Speed() *Entity_SpeedComponent {
|
||||
return be.speed
|
||||
func (be *BaseEntity) Behavior() *Entity_BehaviorComponent {
|
||||
return be.behavior
|
||||
}
|
||||
|
||||
func (be *BaseEntity) DropTable() *Entity_DropTableComponent {
|
||||
return be.dropTable
|
||||
}
|
||||
|
||||
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) {
|
||||
e.stats = &Entity_StatsHolderComponent{
|
||||
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) {
|
||||
e.speed = &Entity_SpeedComponent{
|
||||
e.behavior = &Entity_BehaviorComponent{
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
160
game/model/entity_behavior.go
Normal file
160
game/model/entity_behavior.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -12,19 +12,28 @@ const (
|
|||
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(
|
||||
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),
|
||||
WithBehavior(110, behavior),
|
||||
WithStats(map[Stat]int{
|
||||
Stat_Attributes_Constitution: 5,
|
||||
Stat_Attributes_Dexterity: 10,
|
||||
Stat_Attributes_Strength: 5,
|
||||
Stat_Attributes_Intelligence: 7,
|
||||
|
||||
Stat_ResistanceBonus_Magic_Fire: 5,
|
||||
}),
|
||||
WithInventory(BuildInventory(
|
||||
Inv_WithDominantHand(createBaseItem(
|
||||
|
@ -32,10 +41,62 @@ func Entity_Imp(x, y int) Entity {
|
|||
'v', "|||",
|
||||
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
|
||||
}),
|
||||
)),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
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}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
|
||||
type Player struct {
|
||||
Entity
|
||||
|
||||
skipNextTurn bool
|
||||
}
|
||||
|
||||
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()),
|
||||
WithStats(playerBaseStats),
|
||||
WithHealthData(0, 0, false),
|
||||
WithSpeed(10),
|
||||
WithBehavior(100, nil),
|
||||
),
|
||||
}
|
||||
|
||||
p.Inventory().Push(Item_Bow())
|
||||
|
||||
p.HealthData().MaxHealth = BaseMaxHealth(p)
|
||||
p.HealthData().Health = p.HealthData().MaxHealth
|
||||
|
||||
|
@ -48,3 +52,15 @@ func (p *Player) Stats() *Entity_StatsHolderComponent {
|
|||
func (p *Player) HealthData() *Entity_HealthComponent {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ type Item_DescribedComponent struct {
|
|||
}
|
||||
|
||||
type Item_DamagingComponent struct {
|
||||
IsRanged bool
|
||||
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) {
|
||||
bi.damaging = &Item_DamagingComponent{
|
||||
IsRanged: isRanged,
|
||||
DamageRoll: damageFunc,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,7 +175,7 @@ func Item_Bow() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Bow", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -192,7 +192,7 @@ func Item_Longsword() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Longsword", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -209,7 +209,7 @@ func Item_Club() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Club", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -226,7 +226,7 @@ func Item_Dagger() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Dagger", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -241,9 +241,9 @@ func Item_Handaxe() Item {
|
|||
" ─╗",
|
||||
tcell.StyleDefault.Foreground(tcell.ColorSilver),
|
||||
item_WithQuantity(1, 1),
|
||||
item_WithName("Dagger", tcell.StyleDefault),
|
||||
item_WithName("Handaxe", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -260,7 +260,7 @@ func Item_Javelin() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Javelin", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -277,7 +277,7 @@ func Item_LightHammer() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Light Hammer", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -294,7 +294,7 @@ func Item_Mace() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Mace", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -311,7 +311,7 @@ func Item_Quarterstaff() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Quarterstaff", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -328,7 +328,7 @@ func Item_Sickle() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Sickle", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
@ -345,7 +345,7 @@ func Item_Spear() Item {
|
|||
item_WithQuantity(1, 1),
|
||||
item_WithName("Spear", 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
|
||||
}),
|
||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||
|
|
|
@ -13,8 +13,52 @@ import (
|
|||
|
||||
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
|
||||
|
||||
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 {
|
||||
table []ItemSupplier
|
||||
}
|
||||
|
@ -285,7 +329,7 @@ func GenerateItemOfTypeAndRarity(prototype Item, rarity ItemRarity) Item {
|
|||
prototype.Style(),
|
||||
item_WithName(name, 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_WithStatModifiers(statModifiers),
|
||||
item_WithMetaTypes(metaTypes),
|
||||
|
|
|
@ -46,7 +46,8 @@ const (
|
|||
Stat_ResistanceBonus_Magic_Acid Stat = 280
|
||||
Stat_ResistanceBonus_Magic_Poison Stat = 290
|
||||
|
||||
Stat_MaxHealthBonus Stat = 140
|
||||
Stat_MaxHealthBonus Stat = 1000
|
||||
Stat_SpeedBonus Stat = 1010
|
||||
)
|
||||
|
||||
func StatLongName(stat Stat) string {
|
||||
|
@ -273,6 +274,16 @@ func statValue(stats *Entity_StatsHolderComponent, stat Stat) int {
|
|||
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:
|
||||
// 5*Constitution + Max Health Bonus
|
||||
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) {
|
||||
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()
|
||||
|
@ -350,6 +361,14 @@ func PhysicalWeaponDamage(attacker Entity, weapon Item, victim Entity) (totalDam
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ func (d *Dungeon) HasNextLevel() bool {
|
|||
|
||||
type DungeonLevel struct {
|
||||
ground Map
|
||||
entitiesByPosition map[engine.Position]Entity
|
||||
entitiesByPosition map[engine.Position][]Entity
|
||||
entities map[uuid.UUID]Entity
|
||||
}
|
||||
|
||||
|
@ -142,7 +142,7 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) (dLevel *Dun
|
|||
dLevel = &DungeonLevel{
|
||||
ground: groundLevel,
|
||||
entities: map[uuid.UUID]Entity{},
|
||||
entitiesByPosition: map[engine.Position]Entity{},
|
||||
entitiesByPosition: map[engine.Position][]Entity{},
|
||||
}
|
||||
|
||||
if groundLevel.Rooms() == nil {
|
||||
|
@ -238,7 +238,11 @@ func (d *DungeonLevel) AddEntity(entity Entity) {
|
|||
d.entities[entity.UniqueId()] = entity
|
||||
|
||||
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)
|
||||
|
||||
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) {
|
||||
|
@ -295,7 +303,7 @@ func (d *DungeonLevel) TileAt(x, y int) Tile {
|
|||
tile := Map_TileAt(d.ground, x, y)
|
||||
|
||||
if entity != nil {
|
||||
return CreateTileFromPrototype(tile, Tile_WithEntity(entity))
|
||||
return CreateTileFromPrototype(tile, Tile_WithEntities(entity))
|
||||
}
|
||||
|
||||
return tile
|
||||
|
@ -308,14 +316,14 @@ func (d *DungeonLevel) IsTilePassable(x, y int) bool {
|
|||
|
||||
tile := d.TileAt(x, y)
|
||||
|
||||
if tile.Entity() != nil {
|
||||
if tile.Entities() != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
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)]
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Material uint
|
||||
|
@ -23,7 +26,7 @@ type Tile_ItemComponent struct {
|
|||
}
|
||||
|
||||
type Tile_EntityComponent struct {
|
||||
Entity Entity
|
||||
Entities []Entity
|
||||
}
|
||||
|
||||
type Tile interface {
|
||||
|
@ -37,9 +40,9 @@ type Tile interface {
|
|||
RemoveItem()
|
||||
WithItem(item Item)
|
||||
|
||||
Entity() *Tile_EntityComponent
|
||||
RemoveEntity()
|
||||
WithEntity(entity Entity)
|
||||
Entities() *Tile_EntityComponent
|
||||
RemoveEntity(uuid uuid.UUID)
|
||||
AddEntity(entity Entity)
|
||||
}
|
||||
|
||||
type BaseTile struct {
|
||||
|
@ -50,7 +53,7 @@ type BaseTile struct {
|
|||
passable, opaque, transparent bool
|
||||
|
||||
item *Tile_ItemComponent
|
||||
entity *Tile_EntityComponent
|
||||
entities *Tile_EntityComponent
|
||||
}
|
||||
|
||||
func CreateTileFromPrototype(prototype Tile, components ...func(*BaseTile)) Tile {
|
||||
|
@ -118,24 +121,46 @@ func (t *BaseTile) WithItem(item Item) {
|
|||
}
|
||||
}
|
||||
|
||||
func (t *BaseTile) Entity() *Tile_EntityComponent {
|
||||
return t.entity
|
||||
func (t *BaseTile) Entities() *Tile_EntityComponent {
|
||||
return t.entities
|
||||
}
|
||||
|
||||
func (t *BaseTile) RemoveEntity() {
|
||||
t.entity = nil
|
||||
}
|
||||
|
||||
func (t *BaseTile) WithEntity(entity Entity) {
|
||||
t.entity = &Tile_EntityComponent{
|
||||
Entity: entity,
|
||||
func (t *BaseTile) RemoveEntity(uuid uuid.UUID) {
|
||||
if t.entities == nil {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
return func(bt *BaseTile) {
|
||||
bt.entity = &Tile_EntityComponent{
|
||||
Entity: entity,
|
||||
bt.entities = &Tile_EntityComponent{
|
||||
Entities: []Entity{
|
||||
entity,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Tile_WithEntities(entities []Entity) func(*BaseTile) {
|
||||
return func(bt *BaseTile) {
|
||||
bt.entities = &Tile_EntityComponent{
|
||||
Entities: entities,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,213 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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/views"
|
||||
)
|
||||
|
||||
const CursorRune = '+'
|
||||
const CursorBlinkTime = 200 // Blink cursor every 200ms, showing what's under it
|
||||
|
||||
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) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
func CreateLookState(prevState GameState, eventLog *engine.GameEventLog, dungeon *model.Dungeon, inputSystem *systems.InputSystem, turnSystem *systems.TurnSystem, player *model.Player) *LookState {
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/model"
|
||||
|
@ -51,11 +50,11 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
|
|||
playerStats,
|
||||
)
|
||||
|
||||
s.turnSystem.Schedule(10, func() (complete bool, requeue bool) {
|
||||
s.turnSystem.Schedule(s.player.DefaultSpeed().Speed, func() (complete bool, requeue bool) {
|
||||
requeue = true
|
||||
complete = false
|
||||
|
||||
if s.player.HealthData().IsDead {
|
||||
if s.player.HealthData().Health <= 0 || s.player.HealthData().IsDead {
|
||||
s.nextGameState = CreateGameOverState(inputSystem)
|
||||
}
|
||||
|
||||
|
@ -64,40 +63,34 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
|
|||
s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem)
|
||||
case systems.InputAction_OpenInventory:
|
||||
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:
|
||||
complete = PickUpItemUnderPlayer(s.eventLog, s.dungeon, s.player)
|
||||
case systems.InputAction_Interact:
|
||||
complete = s.InteractBelowPlayer()
|
||||
case systems.InputAction_OpenLogs:
|
||||
s.viewShortLogs = !s.viewShortLogs
|
||||
case systems.InputAction_MovePlayer_East:
|
||||
case systems.InputAction_Move_East:
|
||||
complete = s.MovePlayer(model.East)
|
||||
case systems.InputAction_MovePlayer_West:
|
||||
case systems.InputAction_Move_West:
|
||||
complete = s.MovePlayer(model.West)
|
||||
case systems.InputAction_MovePlayer_North:
|
||||
case systems.InputAction_Move_North:
|
||||
complete = s.MovePlayer(model.North)
|
||||
case systems.InputAction_MovePlayer_South:
|
||||
case systems.InputAction_Move_South:
|
||||
complete = s.MovePlayer(model.South)
|
||||
default:
|
||||
}
|
||||
|
||||
if s.player.IsNextTurnSkipped() {
|
||||
s.player.SkipNextTurn(false)
|
||||
complete = true
|
||||
}
|
||||
|
||||
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.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.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)
|
||||
|
||||
for _, npc := range s.npcs {
|
||||
speed := 10
|
||||
|
||||
if npc.Speed() != nil {
|
||||
speed = npc.Speed().Speed
|
||||
if npc.Behavior() != nil {
|
||||
speed := npc.Behavior().Speed
|
||||
s.turnSystem.Schedule(speed, npc.Behavior().Behavior)
|
||||
}
|
||||
|
||||
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),
|
||||
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))
|
||||
|
||||
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.
|
||||
if ent != nil && ent.HealthData() != nil {
|
||||
|
@ -179,7 +171,7 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
|
|||
return false
|
||||
}
|
||||
|
||||
ExecuteAttack(ps.eventLog, ps.player, ent)
|
||||
model.ExecuteAttack(ps.eventLog, ps.player, ent)
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -192,53 +184,12 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
|
|||
|
||||
return true
|
||||
} else {
|
||||
ps.eventLog.Log("You bump into an impassable object" + model.DirectionName(direction))
|
||||
ps.eventLog.Log("You bump into an impassable object")
|
||||
|
||||
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) {
|
||||
playerPos := ps.player.Position()
|
||||
|
||||
|
@ -356,101 +307,6 @@ func PickUpItemUnderPlayer(eventLog *engine.GameEventLog, dungeon *model.Dungeon
|
|||
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) {
|
||||
ps.nextGameState = ps
|
||||
|
||||
|
@ -477,8 +333,9 @@ func (ps *PlayingState) CollectDrawables() []engine.Drawable {
|
|||
tile := visibilityMap[engine.PositionAt(x, y)]
|
||||
|
||||
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 {
|
||||
|
|
|
@ -12,6 +12,7 @@ const (
|
|||
InputContext_Play = "play"
|
||||
InputContext_Menu = "menu"
|
||||
InputContext_Inventory = "inventory"
|
||||
InputContext_Look = "look"
|
||||
)
|
||||
|
||||
type InputKey string
|
||||
|
@ -25,10 +26,10 @@ type InputAction int
|
|||
const (
|
||||
InputAction_None InputAction = iota
|
||||
|
||||
InputAction_MovePlayer_North
|
||||
InputAction_MovePlayer_South
|
||||
InputAction_MovePlayer_East
|
||||
InputAction_MovePlayer_West
|
||||
InputAction_Move_North
|
||||
InputAction_Move_South
|
||||
InputAction_Move_East
|
||||
InputAction_Move_West
|
||||
|
||||
InputAction_Interact
|
||||
InputAction_OpenInventory
|
||||
|
@ -36,6 +37,10 @@ const (
|
|||
InputAction_OpenLogs
|
||||
InputAction_DropItem
|
||||
InputAction_InteractItem
|
||||
InputAction_UseOn
|
||||
InputAction_Describe
|
||||
InputAction_EnterLookMode
|
||||
InputAction_Shoot
|
||||
|
||||
InputAction_PauseGame
|
||||
|
||||
|
@ -58,15 +63,16 @@ type InputSystem struct {
|
|||
func CreateInputSystemWithDefaultBindings() *InputSystem {
|
||||
return &InputSystem{
|
||||
keyBindings: map[InputKey]InputAction{
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyUp, 0): InputAction_MovePlayer_North,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyDown, 0): InputAction_MovePlayer_South,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyLeft, 0): InputAction_MovePlayer_West,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRight, 0): InputAction_MovePlayer_East,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyUp, 0): InputAction_Move_North,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyDown, 0): InputAction_Move_South,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyLeft, 0): InputAction_Move_West,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRight, 0): InputAction_Move_East,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyEsc, 0): InputAction_PauseGame,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'i'): InputAction_OpenInventory,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'l'): InputAction_OpenLogs,
|
||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'e'): InputAction_Interact,
|
||||
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.KeyLeft, 0): InputAction_Menu_HighlightLeft,
|
||||
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.KeyUp, 0): InputAction_Menu_HighlightUp,
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,33 +53,48 @@ func (uihp *UIHealthBar) Draw(v views.View) {
|
|||
|
||||
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))
|
||||
|
||||
whole := math.Trunc(percentage)
|
||||
last := percentage - whole
|
||||
|
||||
hpBar := ""
|
||||
hpStyle := tcell.StyleDefault.Foreground(tcell.ColorIndianRed)
|
||||
|
||||
for i := range int(whole) {
|
||||
v.SetContent(x+1+i, y+1, stages[0], nil, hpStyle)
|
||||
for range int(whole) {
|
||||
hpBar += stages[0]
|
||||
}
|
||||
|
||||
lastRune := func() string {
|
||||
if last <= 0.0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if last > 0.0 {
|
||||
if last <= 0.25 {
|
||||
v.SetContent(x+1+int(whole), y+1, stages[3], nil, hpStyle)
|
||||
return stages[3]
|
||||
}
|
||||
|
||||
if last <= 0.50 {
|
||||
v.SetContent(x+1+int(whole), y+1, stages[2], nil, hpStyle)
|
||||
return stages[2]
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
engine.DrawText(
|
||||
|
|
Loading…
Add table
Reference in a new issue