diff --git a/game/model/entity.go b/game/model/entity.go index 3f79cf9..2ebda75 100644 --- a/game/model/entity.go +++ b/game/model/entity.go @@ -70,11 +70,11 @@ type Entity_EquippedComponent struct { type Entity_StatsHolderComponent struct { BaseStats map[Stat]int - // StatModifiers []StatModifier } -type Entity_SpeedComponent struct { - Speed int +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, } } } diff --git a/game/model/entity_behavior.go b/game/model/entity_behavior.go new file mode 100644 index 0000000..ea14b91 --- /dev/null +++ b/game/model/entity_behavior.go @@ -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) + } +} diff --git a/game/model/entity_npcs.go b/game/model/entity_npcs.go index 719def0..7590b13 100644 --- a/game/model/entity_npcs.go +++ b/game/model/entity_npcs.go @@ -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}), + }), + ) +} diff --git a/game/model/entity_player.go b/game/model/entity_player.go index 40875e4..609a134 100644 --- a/game/model/entity_player.go +++ b/game/model/entity_player.go @@ -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 +} diff --git a/game/model/item.go b/game/model/item.go index 201f979..329b6bc 100644 --- a/game/model/item.go +++ b/game/model/item.go @@ -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, } } diff --git a/game/model/items.go b/game/model/items.go index b274dd2..ed713bc 100644 --- a/game/model/items.go +++ b/game/model/items.go @@ -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}), diff --git a/game/model/rpg_generate_items.go b/game/model/rpg_generate_items.go index fdddba4..4822147 100644 --- a/game/model/rpg_generate_items.go +++ b/game/model/rpg_generate_items.go @@ -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), diff --git a/game/model/rpg_system.go b/game/model/rpg_system.go index cb89461..922b6d0 100644 --- a/game/model/rpg_system.go +++ b/game/model/rpg_system.go @@ -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 } diff --git a/game/model/world_dungeon.go b/game/model/world_dungeon.go index 058ec04..76aa9d7 100644 --- a/game/model/world_dungeon.go +++ b/game/model/world_dungeon.go @@ -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)] } diff --git a/game/model/world_tile.go b/game/model/world_tile.go index 9150bba..ed45716 100644 --- a/game/model/world_tile.go +++ b/game/model/world_tile.go @@ -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 { @@ -49,8 +52,8 @@ type BaseTile struct { material Material passable, opaque, transparent bool - item *Tile_ItemComponent - entity *Tile_EntityComponent + item *Tile_ItemComponent + 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, } } } diff --git a/game/state/look_state.go b/game/state/look_state.go index f16a0ec..7dbe9cf 100644 --- a/game/state/look_state.go +++ b/game/state/look_state.go @@ -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 } diff --git a/game/state/playing_state.go b/game/state/playing_state.go index 289071e..2b501c5 100644 --- a/game/state/playing_state.go +++ b/game/state/playing_state.go @@ -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 { diff --git a/game/systems/input_system.go b/game/systems/input_system.go index 5347695..255bf5f 100644 --- a/game/systems/input_system.go +++ b/game/systems/input_system.go @@ -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, }, } } diff --git a/game/ui/ui_health_bar.go b/game/ui/ui_health_bar.go index 3d7910a..51938f7 100644 --- a/game/ui/ui_health_bar.go +++ b/game/ui/ui_health_bar.go @@ -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] } - if last > 0.0 { + lastRune := func() string { + if last <= 0.0 { + return "" + } + 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(