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 {
|
type Entity_StatsHolderComponent struct {
|
||||||
BaseStats map[Stat]int
|
BaseStats map[Stat]int
|
||||||
// StatModifiers []StatModifier
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Entity_SpeedComponent struct {
|
type Entity_BehaviorComponent struct {
|
||||||
Speed int
|
Speed int
|
||||||
|
Behavior func() (complete, requeue bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Entity_HealthComponent struct {
|
type Entity_HealthComponent struct {
|
||||||
|
@ -83,10 +83,14 @@ type Entity_HealthComponent struct {
|
||||||
IsDead bool
|
IsDead bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Entity_DropTableComponent struct {
|
||||||
|
DropTable *LootTable
|
||||||
|
}
|
||||||
|
|
||||||
type Entity interface {
|
type Entity interface {
|
||||||
UniqueId() uuid.UUID
|
UniqueId() uuid.UUID
|
||||||
|
|
||||||
Speed() *Entity_SpeedComponent
|
Behavior() *Entity_BehaviorComponent
|
||||||
Named() *Entity_NamedComponent
|
Named() *Entity_NamedComponent
|
||||||
Described() *Entity_DescribedComponent
|
Described() *Entity_DescribedComponent
|
||||||
Presentable() *Entity_PresentableComponent
|
Presentable() *Entity_PresentableComponent
|
||||||
|
@ -94,12 +98,13 @@ type Entity interface {
|
||||||
Equipped() *Entity_EquippedComponent
|
Equipped() *Entity_EquippedComponent
|
||||||
Stats() *Entity_StatsHolderComponent
|
Stats() *Entity_StatsHolderComponent
|
||||||
HealthData() *Entity_HealthComponent
|
HealthData() *Entity_HealthComponent
|
||||||
|
DropTable() *Entity_DropTableComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseEntity struct {
|
type BaseEntity struct {
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
|
|
||||||
speed *Entity_SpeedComponent
|
behavior *Entity_BehaviorComponent
|
||||||
named *Entity_NamedComponent
|
named *Entity_NamedComponent
|
||||||
described *Entity_DescribedComponent
|
described *Entity_DescribedComponent
|
||||||
presentable *Entity_PresentableComponent
|
presentable *Entity_PresentableComponent
|
||||||
|
@ -107,6 +112,7 @@ type BaseEntity struct {
|
||||||
equipped *Entity_EquippedComponent
|
equipped *Entity_EquippedComponent
|
||||||
stats *Entity_StatsHolderComponent
|
stats *Entity_StatsHolderComponent
|
||||||
damageable *Entity_HealthComponent
|
damageable *Entity_HealthComponent
|
||||||
|
dropTable *Entity_DropTableComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func (be *BaseEntity) UniqueId() uuid.UUID {
|
func (be *BaseEntity) UniqueId() uuid.UUID {
|
||||||
|
@ -141,8 +147,12 @@ func (be *BaseEntity) HealthData() *Entity_HealthComponent {
|
||||||
return be.damageable
|
return be.damageable
|
||||||
}
|
}
|
||||||
|
|
||||||
func (be *BaseEntity) Speed() *Entity_SpeedComponent {
|
func (be *BaseEntity) Behavior() *Entity_BehaviorComponent {
|
||||||
return be.speed
|
return be.behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
func (be *BaseEntity) DropTable() *Entity_DropTableComponent {
|
||||||
|
return be.dropTable
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateEntity(components ...func(*BaseEntity)) *BaseEntity {
|
func CreateEntity(components ...func(*BaseEntity)) *BaseEntity {
|
||||||
|
@ -202,7 +212,6 @@ func WithStats(baseStats map[Stat]int, statModifiers ...StatModifier) func(e *Ba
|
||||||
return func(e *BaseEntity) {
|
return func(e *BaseEntity) {
|
||||||
e.stats = &Entity_StatsHolderComponent{
|
e.stats = &Entity_StatsHolderComponent{
|
||||||
BaseStats: baseStats,
|
BaseStats: baseStats,
|
||||||
// StatModifiers: statModifiers,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,10 +226,27 @@ func WithHealthData(health, maxHealth int, isDead bool) func(e *BaseEntity) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithSpeed(speed int) func(e *BaseEntity) {
|
func WithBehavior(speed int, behavior func(npc Entity) (complete, requeue bool)) func(e *BaseEntity) {
|
||||||
return func(e *BaseEntity) {
|
return func(e *BaseEntity) {
|
||||||
e.speed = &Entity_SpeedComponent{
|
e.behavior = &Entity_BehaviorComponent{
|
||||||
Speed: speed,
|
Speed: speed,
|
||||||
|
Behavior: func() (complete bool, requeue bool) {
|
||||||
|
return behavior(e)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDropTable(table map[int]ItemSupplier) func(e *BaseEntity) {
|
||||||
|
dropTable := CreateLootTable()
|
||||||
|
|
||||||
|
for k, v := range table {
|
||||||
|
dropTable.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(e *BaseEntity) {
|
||||||
|
e.dropTable = &Entity_DropTableComponent{
|
||||||
|
DropTable: dropTable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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
|
ImpClaws specialItemType = 100_000 + iota
|
||||||
)
|
)
|
||||||
|
|
||||||
func Entity_Imp(x, y int) Entity {
|
func Entity_ArrowProjectile(startX, startY int, targetX, targetY int) Entity {
|
||||||
|
return CreateEntity(
|
||||||
|
WithName("Arrow"),
|
||||||
|
WithPosition(engine.PositionAt(startX, startY)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Entity_Imp(x, y int, behavior func(npc Entity) (complete, requeue bool)) Entity {
|
||||||
return CreateEntity(
|
return CreateEntity(
|
||||||
WithName("Imp"),
|
WithName("Imp"),
|
||||||
WithDescription("A fiery little creature"),
|
WithDescription("A fiery little creature"),
|
||||||
WithHealthData(15, 15, false),
|
WithHealthData(15, 15, false),
|
||||||
WithPosition(engine.PositionAt(x, y)),
|
WithPosition(engine.PositionAt(x, y)),
|
||||||
WithPresentation('i', tcell.StyleDefault.Foreground(tcell.ColorDarkRed)),
|
WithPresentation('i', tcell.StyleDefault.Foreground(tcell.ColorDarkRed)),
|
||||||
WithSpeed(11),
|
WithBehavior(110, behavior),
|
||||||
WithStats(map[Stat]int{
|
WithStats(map[Stat]int{
|
||||||
Stat_Attributes_Constitution: 5,
|
Stat_Attributes_Constitution: 5,
|
||||||
Stat_Attributes_Dexterity: 10,
|
Stat_Attributes_Dexterity: 10,
|
||||||
Stat_Attributes_Strength: 5,
|
Stat_Attributes_Strength: 5,
|
||||||
Stat_Attributes_Intelligence: 7,
|
Stat_Attributes_Intelligence: 7,
|
||||||
|
|
||||||
|
Stat_ResistanceBonus_Magic_Fire: 5,
|
||||||
}),
|
}),
|
||||||
WithInventory(BuildInventory(
|
WithInventory(BuildInventory(
|
||||||
Inv_WithDominantHand(createBaseItem(
|
Inv_WithDominantHand(createBaseItem(
|
||||||
|
@ -32,10 +41,62 @@ func Entity_Imp(x, y int) Entity {
|
||||||
'v', "|||",
|
'v', "|||",
|
||||||
tcell.StyleDefault,
|
tcell.StyleDefault,
|
||||||
item_WithName("Claws", tcell.StyleDefault),
|
item_WithName("Claws", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD4(1), DamageType_Physical_Slashing
|
return RollD4(1), DamageType_Physical_Slashing
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Entity_SkeletalKnight(x, y int, behavior func(npc Entity) (complete, requeue bool)) Entity {
|
||||||
|
return CreateEntity(
|
||||||
|
WithName("Skeletal Knight"),
|
||||||
|
WithDescription("Rattling in the dark..."),
|
||||||
|
WithHealthData(25, 25, false),
|
||||||
|
WithPosition(engine.PositionAt(x, y)),
|
||||||
|
WithPresentation('S', tcell.StyleDefault.Foreground(tcell.ColorAntiqueWhite)),
|
||||||
|
WithBehavior(150, behavior),
|
||||||
|
WithStats(map[Stat]int{
|
||||||
|
Stat_Attributes_Constitution: 10,
|
||||||
|
Stat_Attributes_Dexterity: 6,
|
||||||
|
Stat_Attributes_Strength: 12,
|
||||||
|
Stat_Attributes_Intelligence: 5,
|
||||||
|
|
||||||
|
Stat_ResistanceBonus_Physical_Bludgeoning: -2,
|
||||||
|
}),
|
||||||
|
WithInventory(BuildInventory(
|
||||||
|
Inv_WithDominantHand(Item_Longsword()),
|
||||||
|
)),
|
||||||
|
WithDropTable(map[int]ItemSupplier{
|
||||||
|
9: ItemSupplierOf(Item_Longsword()),
|
||||||
|
1: ItemSupplierOfGeneratedPrototype(Item_Longsword(), map[int]ItemRarity{1: ItemRarity_Legendary}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Entity_SkeletalWarrior(x, y int, behavior func(npc Entity) (complete, requeue bool)) Entity {
|
||||||
|
return CreateEntity(
|
||||||
|
WithName("Skeletal Warrior"),
|
||||||
|
WithDescription("Rattling in the dark..."),
|
||||||
|
WithHealthData(25, 25, false),
|
||||||
|
WithPosition(engine.PositionAt(x, y)),
|
||||||
|
WithPresentation('S', tcell.StyleDefault.Foreground(tcell.ColorAntiqueWhite)),
|
||||||
|
WithBehavior(150, behavior),
|
||||||
|
WithStats(map[Stat]int{
|
||||||
|
Stat_Attributes_Constitution: 10,
|
||||||
|
Stat_Attributes_Dexterity: 6,
|
||||||
|
Stat_Attributes_Strength: 12,
|
||||||
|
Stat_Attributes_Intelligence: 5,
|
||||||
|
|
||||||
|
Stat_ResistanceBonus_Physical_Bludgeoning: -2,
|
||||||
|
}),
|
||||||
|
WithInventory(BuildInventory(
|
||||||
|
Inv_WithDominantHand(Item_Mace()),
|
||||||
|
)),
|
||||||
|
WithDropTable(map[int]ItemSupplier{
|
||||||
|
9: ItemSupplierOf(Item_Mace()),
|
||||||
|
1: ItemSupplierOfGeneratedPrototype(Item_Mace(), map[int]ItemRarity{1: ItemRarity_Legendary}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
|
|
||||||
type Player struct {
|
type Player struct {
|
||||||
Entity
|
Entity
|
||||||
|
|
||||||
|
skipNextTurn bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player {
|
func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player {
|
||||||
|
@ -19,10 +21,12 @@ func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player {
|
||||||
WithInventory(CreateEquippedInventory()),
|
WithInventory(CreateEquippedInventory()),
|
||||||
WithStats(playerBaseStats),
|
WithStats(playerBaseStats),
|
||||||
WithHealthData(0, 0, false),
|
WithHealthData(0, 0, false),
|
||||||
WithSpeed(10),
|
WithBehavior(100, nil),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.Inventory().Push(Item_Bow())
|
||||||
|
|
||||||
p.HealthData().MaxHealth = BaseMaxHealth(p)
|
p.HealthData().MaxHealth = BaseMaxHealth(p)
|
||||||
p.HealthData().Health = p.HealthData().MaxHealth
|
p.HealthData().Health = p.HealthData().MaxHealth
|
||||||
|
|
||||||
|
@ -48,3 +52,15 @@ func (p *Player) Stats() *Entity_StatsHolderComponent {
|
||||||
func (p *Player) HealthData() *Entity_HealthComponent {
|
func (p *Player) HealthData() *Entity_HealthComponent {
|
||||||
return p.Entity.HealthData()
|
return p.Entity.HealthData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Player) DefaultSpeed() *Entity_BehaviorComponent {
|
||||||
|
return p.Entity.Behavior()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) SkipNextTurn(skip bool) {
|
||||||
|
p.skipNextTurn = skip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) IsNextTurnSkipped() bool {
|
||||||
|
return p.skipNextTurn
|
||||||
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ type Item_DescribedComponent struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Item_DamagingComponent struct {
|
type Item_DamagingComponent struct {
|
||||||
|
IsRanged bool
|
||||||
DamageRoll func() (damage int, dmgType DamageType)
|
DamageRoll func() (damage int, dmgType DamageType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,9 +177,10 @@ func item_WithEquippable(slot EquippedSlot) func(*BaseItem) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func item_WithDamaging(damageFunc func() (damage int, dmgType DamageType)) func(*BaseItem) {
|
func item_WithDamaging(isRanged bool, damageFunc func() (damage int, dmgType DamageType)) func(*BaseItem) {
|
||||||
return func(bi *BaseItem) {
|
return func(bi *BaseItem) {
|
||||||
bi.damaging = &Item_DamagingComponent{
|
bi.damaging = &Item_DamagingComponent{
|
||||||
|
IsRanged: isRanged,
|
||||||
DamageRoll: damageFunc,
|
DamageRoll: damageFunc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,7 +175,7 @@ func Item_Bow() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Bow", tcell.StyleDefault),
|
item_WithName("Bow", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(true, func() (damage int, dmgType DamageType) {
|
||||||
return RollD8(1), DamageType_Physical_Piercing
|
return RollD8(1), DamageType_Physical_Piercing
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -192,7 +192,7 @@ func Item_Longsword() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Longsword", tcell.StyleDefault),
|
item_WithName("Longsword", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d8 Slashing damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d8 Slashing damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD8(1), DamageType_Physical_Slashing
|
return RollD8(1), DamageType_Physical_Slashing
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -209,7 +209,7 @@ func Item_Club() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Club", tcell.StyleDefault),
|
item_WithName("Club", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d8 Bludgeoning damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d8 Bludgeoning damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD8(1), DamageType_Physical_Bludgeoning
|
return RollD8(1), DamageType_Physical_Bludgeoning
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -226,7 +226,7 @@ func Item_Dagger() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Dagger", tcell.StyleDefault),
|
item_WithName("Dagger", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD6(1), DamageType_Physical_Piercing
|
return RollD6(1), DamageType_Physical_Piercing
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -241,9 +241,9 @@ func Item_Handaxe() Item {
|
||||||
" ─╗",
|
" ─╗",
|
||||||
tcell.StyleDefault.Foreground(tcell.ColorSilver),
|
tcell.StyleDefault.Foreground(tcell.ColorSilver),
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Dagger", tcell.StyleDefault),
|
item_WithName("Handaxe", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD6(1), DamageType_Physical_Piercing
|
return RollD6(1), DamageType_Physical_Piercing
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -260,7 +260,7 @@ func Item_Javelin() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Javelin", tcell.StyleDefault),
|
item_WithName("Javelin", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD6(1), DamageType_Physical_Piercing
|
return RollD6(1), DamageType_Physical_Piercing
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -277,7 +277,7 @@ func Item_LightHammer() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Light Hammer", tcell.StyleDefault),
|
item_WithName("Light Hammer", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD6(1), DamageType_Physical_Bludgeoning
|
return RollD6(1), DamageType_Physical_Bludgeoning
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -294,7 +294,7 @@ func Item_Mace() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Mace", tcell.StyleDefault),
|
item_WithName("Mace", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD6(1), DamageType_Physical_Bludgeoning
|
return RollD6(1), DamageType_Physical_Bludgeoning
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -311,7 +311,7 @@ func Item_Quarterstaff() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Quarterstaff", tcell.StyleDefault),
|
item_WithName("Quarterstaff", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD6(1), DamageType_Physical_Bludgeoning
|
return RollD6(1), DamageType_Physical_Bludgeoning
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -328,7 +328,7 @@ func Item_Sickle() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Sickle", tcell.StyleDefault),
|
item_WithName("Sickle", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD6(1), DamageType_Physical_Slashing
|
return RollD6(1), DamageType_Physical_Slashing
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
@ -345,7 +345,7 @@ func Item_Spear() Item {
|
||||||
item_WithQuantity(1, 1),
|
item_WithQuantity(1, 1),
|
||||||
item_WithName("Spear", tcell.StyleDefault),
|
item_WithName("Spear", tcell.StyleDefault),
|
||||||
item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault),
|
item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault),
|
||||||
item_WithDamaging(func() (damage int, dmgType DamageType) {
|
item_WithDamaging(false, func() (damage int, dmgType DamageType) {
|
||||||
return RollD8(1), DamageType_Physical_Piercing
|
return RollD8(1), DamageType_Physical_Piercing
|
||||||
}),
|
}),
|
||||||
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}),
|
||||||
|
|
|
@ -13,8 +13,52 @@ import (
|
||||||
|
|
||||||
const MaxNumberOfModifiers = 6
|
const MaxNumberOfModifiers = 6
|
||||||
|
|
||||||
|
type RarityTable struct {
|
||||||
|
table []ItemRarity
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRarityTable() *RarityTable {
|
||||||
|
return &RarityTable{
|
||||||
|
table: make([]ItemRarity, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (igt *RarityTable) Add(weight int, rarity ItemRarity) {
|
||||||
|
for range weight {
|
||||||
|
igt.table = append(igt.table, rarity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (igt *RarityTable) Generate() ItemRarity {
|
||||||
|
return igt.table[rand.Intn(len(igt.table))]
|
||||||
|
}
|
||||||
|
|
||||||
type ItemSupplier func() Item
|
type ItemSupplier func() Item
|
||||||
|
|
||||||
|
func EmptyItemSupplier() ItemSupplier {
|
||||||
|
return func() Item {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ItemSupplierOf(item Item) ItemSupplier {
|
||||||
|
return func() Item {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ItemSupplierOfGeneratedPrototype(prototype Item, rarities map[int]ItemRarity) ItemSupplier {
|
||||||
|
rarityTable := CreateRarityTable()
|
||||||
|
|
||||||
|
for k, v := range rarities {
|
||||||
|
rarityTable.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() Item {
|
||||||
|
return GenerateItemOfTypeAndRarity(prototype, rarityTable.Generate())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type LootTable struct {
|
type LootTable struct {
|
||||||
table []ItemSupplier
|
table []ItemSupplier
|
||||||
}
|
}
|
||||||
|
@ -285,7 +329,7 @@ func GenerateItemOfTypeAndRarity(prototype Item, rarity ItemRarity) Item {
|
||||||
prototype.Style(),
|
prototype.Style(),
|
||||||
item_WithName(name, style),
|
item_WithName(name, style),
|
||||||
item_WithDescription(prototype.Described().Description, prototype.Described().Style),
|
item_WithDescription(prototype.Described().Description, prototype.Described().Style),
|
||||||
item_WithDamaging(prototype.Damaging().DamageRoll),
|
item_WithDamaging(prototype.Damaging().IsRanged, prototype.Damaging().DamageRoll),
|
||||||
item_WithEquippable(prototype.Equippable().Slot),
|
item_WithEquippable(prototype.Equippable().Slot),
|
||||||
item_WithStatModifiers(statModifiers),
|
item_WithStatModifiers(statModifiers),
|
||||||
item_WithMetaTypes(metaTypes),
|
item_WithMetaTypes(metaTypes),
|
||||||
|
|
|
@ -46,7 +46,8 @@ const (
|
||||||
Stat_ResistanceBonus_Magic_Acid Stat = 280
|
Stat_ResistanceBonus_Magic_Acid Stat = 280
|
||||||
Stat_ResistanceBonus_Magic_Poison Stat = 290
|
Stat_ResistanceBonus_Magic_Poison Stat = 290
|
||||||
|
|
||||||
Stat_MaxHealthBonus Stat = 140
|
Stat_MaxHealthBonus Stat = 1000
|
||||||
|
Stat_SpeedBonus Stat = 1010
|
||||||
)
|
)
|
||||||
|
|
||||||
func StatLongName(stat Stat) string {
|
func StatLongName(stat Stat) string {
|
||||||
|
@ -273,6 +274,16 @@ func statValue(stats *Entity_StatsHolderComponent, stat Stat) int {
|
||||||
return stats.BaseStats[stat]
|
return stats.BaseStats[stat]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func statModifierValue(statModifiers []StatModifier, stat Stat) int {
|
||||||
|
for _, sm := range statModifiers {
|
||||||
|
if sm.Stat == stat {
|
||||||
|
return sm.Bonus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// Base Max Health is determined from constitution:
|
// Base Max Health is determined from constitution:
|
||||||
// 5*Constitution + Max Health Bonus
|
// 5*Constitution + Max Health Bonus
|
||||||
func BaseMaxHealth(entity Entity) int {
|
func BaseMaxHealth(entity Entity) int {
|
||||||
|
@ -340,7 +351,7 @@ func UnarmedDamage(attacker Entity) int {
|
||||||
|
|
||||||
func PhysicalWeaponDamage(attacker Entity, weapon Item, victim Entity) (totalDamage int, dmgType DamageType) {
|
func PhysicalWeaponDamage(attacker Entity, weapon Item, victim Entity) (totalDamage int, dmgType DamageType) {
|
||||||
if attacker.Stats() == nil || weapon.Damaging() == nil || victim.Stats() == nil {
|
if attacker.Stats() == nil || weapon.Damaging() == nil || victim.Stats() == nil {
|
||||||
return 0, DamageType_Physical_Unarmed
|
return UnarmedDamage(attacker), DamageType_Physical_Unarmed
|
||||||
}
|
}
|
||||||
|
|
||||||
totalDamage, dmgType = weapon.Damaging().DamageRoll()
|
totalDamage, dmgType = weapon.Damaging().DamageRoll()
|
||||||
|
@ -350,6 +361,14 @@ func PhysicalWeaponDamage(attacker Entity, weapon Item, victim Entity) (totalDam
|
||||||
|
|
||||||
totalDamage = totalDamage + statValue(attacker.Stats(), bonusDmgStat) - statValue(victim.Stats(), dmgResistStat)
|
totalDamage = totalDamage + statValue(attacker.Stats(), bonusDmgStat) - statValue(victim.Stats(), dmgResistStat)
|
||||||
|
|
||||||
|
if weapon.StatModifier() != nil {
|
||||||
|
totalDamage += statModifierValue(weapon.StatModifier().StatModifiers, bonusDmgStat)
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalDamage <= 0 {
|
||||||
|
return 0, dmgType
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@ func (d *Dungeon) HasNextLevel() bool {
|
||||||
|
|
||||||
type DungeonLevel struct {
|
type DungeonLevel struct {
|
||||||
ground Map
|
ground Map
|
||||||
entitiesByPosition map[engine.Position]Entity
|
entitiesByPosition map[engine.Position][]Entity
|
||||||
entities map[uuid.UUID]Entity
|
entities map[uuid.UUID]Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) (dLevel *Dun
|
||||||
dLevel = &DungeonLevel{
|
dLevel = &DungeonLevel{
|
||||||
ground: groundLevel,
|
ground: groundLevel,
|
||||||
entities: map[uuid.UUID]Entity{},
|
entities: map[uuid.UUID]Entity{},
|
||||||
entitiesByPosition: map[engine.Position]Entity{},
|
entitiesByPosition: map[engine.Position][]Entity{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if groundLevel.Rooms() == nil {
|
if groundLevel.Rooms() == nil {
|
||||||
|
@ -238,7 +238,11 @@ func (d *DungeonLevel) AddEntity(entity Entity) {
|
||||||
d.entities[entity.UniqueId()] = entity
|
d.entities[entity.UniqueId()] = entity
|
||||||
|
|
||||||
if entity.Positioned() != nil {
|
if entity.Positioned() != nil {
|
||||||
d.entitiesByPosition[entity.Positioned().Position] = entity
|
if d.entitiesByPosition[entity.Positioned().Position] == nil {
|
||||||
|
d.entitiesByPosition[entity.Positioned().Position] = []Entity{entity}
|
||||||
|
} else {
|
||||||
|
d.entitiesByPosition[entity.Positioned().Position] = append(d.entitiesByPosition[entity.Positioned().Position], entity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +257,11 @@ func (d *DungeonLevel) MoveEntityTo(uuid uuid.UUID, x, y int) {
|
||||||
|
|
||||||
ent.Positioned().Position = engine.PositionAt(x, y)
|
ent.Positioned().Position = engine.PositionAt(x, y)
|
||||||
|
|
||||||
d.entitiesByPosition[ent.Positioned().Position] = ent
|
if d.entitiesByPosition[ent.Positioned().Position] == nil {
|
||||||
|
d.entitiesByPosition[ent.Positioned().Position] = []Entity{ent}
|
||||||
|
} else {
|
||||||
|
d.entitiesByPosition[ent.Positioned().Position] = append(d.entitiesByPosition[ent.Positioned().Position], ent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DungeonLevel) RemoveEntityAt(x, y int) {
|
func (d *DungeonLevel) RemoveEntityAt(x, y int) {
|
||||||
|
@ -295,7 +303,7 @@ func (d *DungeonLevel) TileAt(x, y int) Tile {
|
||||||
tile := Map_TileAt(d.ground, x, y)
|
tile := Map_TileAt(d.ground, x, y)
|
||||||
|
|
||||||
if entity != nil {
|
if entity != nil {
|
||||||
return CreateTileFromPrototype(tile, Tile_WithEntity(entity))
|
return CreateTileFromPrototype(tile, Tile_WithEntities(entity))
|
||||||
}
|
}
|
||||||
|
|
||||||
return tile
|
return tile
|
||||||
|
@ -308,14 +316,14 @@ func (d *DungeonLevel) IsTilePassable(x, y int) bool {
|
||||||
|
|
||||||
tile := d.TileAt(x, y)
|
tile := d.TileAt(x, y)
|
||||||
|
|
||||||
if tile.Entity() != nil {
|
if tile.Entities() != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return tile.Passable()
|
return tile.Passable()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DungeonLevel) EntityAt(x, y int) (e Entity) {
|
func (d *DungeonLevel) EntitiesAt(x, y int) (e []Entity) {
|
||||||
return d.entitiesByPosition[engine.PositionAt(x, y)]
|
return d.entitiesByPosition[engine.PositionAt(x, y)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Material uint
|
type Material uint
|
||||||
|
@ -23,7 +26,7 @@ type Tile_ItemComponent struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tile_EntityComponent struct {
|
type Tile_EntityComponent struct {
|
||||||
Entity Entity
|
Entities []Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tile interface {
|
type Tile interface {
|
||||||
|
@ -37,9 +40,9 @@ type Tile interface {
|
||||||
RemoveItem()
|
RemoveItem()
|
||||||
WithItem(item Item)
|
WithItem(item Item)
|
||||||
|
|
||||||
Entity() *Tile_EntityComponent
|
Entities() *Tile_EntityComponent
|
||||||
RemoveEntity()
|
RemoveEntity(uuid uuid.UUID)
|
||||||
WithEntity(entity Entity)
|
AddEntity(entity Entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseTile struct {
|
type BaseTile struct {
|
||||||
|
@ -49,8 +52,8 @@ type BaseTile struct {
|
||||||
material Material
|
material Material
|
||||||
passable, opaque, transparent bool
|
passable, opaque, transparent bool
|
||||||
|
|
||||||
item *Tile_ItemComponent
|
item *Tile_ItemComponent
|
||||||
entity *Tile_EntityComponent
|
entities *Tile_EntityComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateTileFromPrototype(prototype Tile, components ...func(*BaseTile)) Tile {
|
func CreateTileFromPrototype(prototype Tile, components ...func(*BaseTile)) Tile {
|
||||||
|
@ -118,24 +121,46 @@ func (t *BaseTile) WithItem(item Item) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *BaseTile) Entity() *Tile_EntityComponent {
|
func (t *BaseTile) Entities() *Tile_EntityComponent {
|
||||||
return t.entity
|
return t.entities
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *BaseTile) RemoveEntity() {
|
func (t *BaseTile) RemoveEntity(uuid uuid.UUID) {
|
||||||
t.entity = nil
|
if t.entities == nil {
|
||||||
}
|
return
|
||||||
|
|
||||||
func (t *BaseTile) WithEntity(entity Entity) {
|
|
||||||
t.entity = &Tile_EntityComponent{
|
|
||||||
Entity: entity,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.entities.Entities = slices.DeleteFunc(t.entities.Entities, func(e Entity) bool { return e.UniqueId() == uuid })
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BaseTile) AddEntity(entity Entity) {
|
||||||
|
if t.entities == nil {
|
||||||
|
t.entities = &Tile_EntityComponent{
|
||||||
|
Entities: []Entity{
|
||||||
|
entity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.entities.Entities = append(t.entities.Entities, entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Tile_WithEntity(entity Entity) func(*BaseTile) {
|
func Tile_WithEntity(entity Entity) func(*BaseTile) {
|
||||||
return func(bt *BaseTile) {
|
return func(bt *BaseTile) {
|
||||||
bt.entity = &Tile_EntityComponent{
|
bt.entities = &Tile_EntityComponent{
|
||||||
Entity: entity,
|
Entities: []Entity{
|
||||||
|
entity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Tile_WithEntities(entities []Entity) func(*BaseTile) {
|
||||||
|
return func(bt *BaseTile) {
|
||||||
|
bt.entities = &Tile_EntityComponent{
|
||||||
|
Entities: entities,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,213 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
|
"mvvasilev/last_light/game/model"
|
||||||
|
"mvvasilev/last_light/game/systems"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/gdamore/tcell/v2/views"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CursorRune = '+'
|
||||||
|
const CursorBlinkTime = 200 // Blink cursor every 200ms, showing what's under it
|
||||||
|
|
||||||
type LookState struct {
|
type LookState struct {
|
||||||
|
prevState GameState
|
||||||
|
|
||||||
|
inputSystem *systems.InputSystem
|
||||||
|
turnSystem *systems.TurnSystem
|
||||||
|
eventLog *engine.GameEventLog
|
||||||
|
player *model.Player
|
||||||
|
dungeon *model.Dungeon
|
||||||
|
|
||||||
|
showCursor bool
|
||||||
|
cursorPos engine.Position
|
||||||
|
lastCursorBlinkTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ls *LookState) OnInput(e *tcell.EventKey) {
|
func CreateLookState(prevState GameState, eventLog *engine.GameEventLog, dungeon *model.Dungeon, inputSystem *systems.InputSystem, turnSystem *systems.TurnSystem, player *model.Player) *LookState {
|
||||||
panic("not implemented") // TODO: Implement
|
return &LookState{
|
||||||
|
prevState: prevState,
|
||||||
|
inputSystem: inputSystem,
|
||||||
|
turnSystem: turnSystem,
|
||||||
|
dungeon: dungeon,
|
||||||
|
player: player,
|
||||||
|
eventLog: eventLog,
|
||||||
|
cursorPos: engine.PositionAt(0, 0),
|
||||||
|
lastCursorBlinkTime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LookState) InputContext() systems.InputContext {
|
||||||
|
return systems.InputContext_Look
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ls *LookState) OnTick(dt int64) GameState {
|
func (ls *LookState) OnTick(dt int64) GameState {
|
||||||
panic("not implemented") // TODO: Implement
|
switch ls.inputSystem.NextAction() {
|
||||||
|
case systems.InputAction_Move_North:
|
||||||
|
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.North))
|
||||||
|
case systems.InputAction_Move_South:
|
||||||
|
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.South))
|
||||||
|
case systems.InputAction_Move_East:
|
||||||
|
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.East))
|
||||||
|
case systems.InputAction_Move_West:
|
||||||
|
ls.cursorPos = ls.cursorPos.WithOffset(model.MovementDirectionOffset(model.West))
|
||||||
|
case systems.InputAction_Describe:
|
||||||
|
ls.Describe()
|
||||||
|
case systems.InputAction_Shoot:
|
||||||
|
ls.ShootEquippedWeapon()
|
||||||
|
case systems.InputAction_Menu_Exit:
|
||||||
|
return ls.prevState
|
||||||
|
}
|
||||||
|
|
||||||
|
return ls
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LookState) ShootEquippedWeapon() {
|
||||||
|
weapon := ls.player.Inventory().AtSlot(model.EquippedSlotDominantHand)
|
||||||
|
|
||||||
|
if weapon == nil {
|
||||||
|
ls.eventLog.Log("You don't have anything equipped!")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if weapon.Damaging() == nil {
|
||||||
|
ls.eventLog.Log("Item unusable")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
damaging := weapon.Damaging()
|
||||||
|
|
||||||
|
if !damaging.IsRanged {
|
||||||
|
ls.eventLog.Log("Equipped weapon is not ranged!")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Projectiles
|
||||||
|
|
||||||
|
ls.player.SkipNextTurn(true)
|
||||||
|
|
||||||
|
ls.turnSystem.NextTurn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LookState) Describe() {
|
||||||
|
dX, dY := ls.lookCursorCoordsToDungeonCoords()
|
||||||
|
|
||||||
|
isVisibleFromPlayer, lastTile := model.HasLineOfSight(ls.dungeon, ls.player.Position(), engine.PositionAt(dX, dY))
|
||||||
|
|
||||||
|
if !isVisibleFromPlayer {
|
||||||
|
materialName, _ := materialToDescription(lastTile.Material())
|
||||||
|
|
||||||
|
ls.eventLog.Log(fmt.Sprintf("%s obscures your view", materialName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tile := ls.dungeon.CurrentLevel().TileAt(dX, dY)
|
||||||
|
|
||||||
|
entities := tile.Entities()
|
||||||
|
|
||||||
|
if entities != nil {
|
||||||
|
ls.DescribeEntities(entities.Entities)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item := tile.Item()
|
||||||
|
|
||||||
|
if item != nil {
|
||||||
|
ls.DescribeItem(item.Item)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
materialName, materialDesc := materialToDescription(tile.Material())
|
||||||
|
|
||||||
|
ls.eventLog.Log(fmt.Sprintf("%s: %s", materialName, materialDesc))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LookState) DescribeEntities(entities []model.Entity) {
|
||||||
|
if entities == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entity := range entities {
|
||||||
|
if entity == ls.player {
|
||||||
|
ls.eventLog.Log("You")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity.Named() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if entity.Described() != nil {
|
||||||
|
ls.eventLog.Log(fmt.Sprintf("%s: %s", entity.Named().Name, entity.Described().Description))
|
||||||
|
} else {
|
||||||
|
ls.eventLog.Log(entity.Named().Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LookState) DescribeItem(item model.Item) {
|
||||||
|
if item == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Named() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Described() != nil {
|
||||||
|
ls.eventLog.Log(fmt.Sprintf("%s: %s", item.Named().Name, item.Described().Description))
|
||||||
|
} else {
|
||||||
|
ls.eventLog.Log(item.Named().Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func materialToDescription(material model.Material) (name, description string) {
|
||||||
|
switch material {
|
||||||
|
case model.MaterialVoid:
|
||||||
|
return "Void", "Who knows what lurks here..."
|
||||||
|
case model.MaterialWall:
|
||||||
|
return "Wall", "Mediocre masonry"
|
||||||
|
case model.MaterialGround:
|
||||||
|
return "Ground", "Try not to trip"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Void", "Who knows what lurks here..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LookState) lookCursorCoordsToScreenCoords() (sX, xY int) {
|
||||||
|
x, y := ls.cursorPos.XY()
|
||||||
|
middleOfScreenX, middleOfScreenY := engine.TERMINAL_SIZE_WIDTH/2, engine.TERMINAL_SIZE_HEIGHT/2
|
||||||
|
return middleOfScreenX + x, middleOfScreenY + y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LookState) lookCursorCoordsToDungeonCoords() (sX, xY int) {
|
||||||
|
x, y := ls.cursorPos.XY()
|
||||||
|
playerX, playerY := ls.player.Position().XY()
|
||||||
|
return playerX + x, playerY + y
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ls *LookState) CollectDrawables() []engine.Drawable {
|
func (ls *LookState) CollectDrawables() []engine.Drawable {
|
||||||
panic("not implemented") // TODO: Implement
|
drawables := append(ls.prevState.CollectDrawables(), engine.CreateDrawingInstructions(func(v views.View) {
|
||||||
|
if time.Since(ls.lastCursorBlinkTime).Milliseconds() >= CursorBlinkTime {
|
||||||
|
ls.showCursor = !ls.showCursor
|
||||||
|
ls.lastCursorBlinkTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.showCursor {
|
||||||
|
x, y := ls.lookCursorCoordsToScreenCoords()
|
||||||
|
v.SetContent(x, y, CursorRune, nil, tcell.StyleDefault)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return drawables
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
"mvvasilev/last_light/game/model"
|
"mvvasilev/last_light/game/model"
|
||||||
|
@ -51,11 +50,11 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
|
||||||
playerStats,
|
playerStats,
|
||||||
)
|
)
|
||||||
|
|
||||||
s.turnSystem.Schedule(10, func() (complete bool, requeue bool) {
|
s.turnSystem.Schedule(s.player.DefaultSpeed().Speed, func() (complete bool, requeue bool) {
|
||||||
requeue = true
|
requeue = true
|
||||||
complete = false
|
complete = false
|
||||||
|
|
||||||
if s.player.HealthData().IsDead {
|
if s.player.HealthData().Health <= 0 || s.player.HealthData().IsDead {
|
||||||
s.nextGameState = CreateGameOverState(inputSystem)
|
s.nextGameState = CreateGameOverState(inputSystem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,40 +63,34 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
|
||||||
s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem)
|
s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem)
|
||||||
case systems.InputAction_OpenInventory:
|
case systems.InputAction_OpenInventory:
|
||||||
s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s)
|
s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s)
|
||||||
|
case systems.InputAction_EnterLookMode:
|
||||||
|
s.viewShortLogs = !s.viewShortLogs
|
||||||
|
s.nextGameState = CreateLookState(s, s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player)
|
||||||
case systems.InputAction_PickUpItem:
|
case systems.InputAction_PickUpItem:
|
||||||
complete = PickUpItemUnderPlayer(s.eventLog, s.dungeon, s.player)
|
complete = PickUpItemUnderPlayer(s.eventLog, s.dungeon, s.player)
|
||||||
case systems.InputAction_Interact:
|
case systems.InputAction_Interact:
|
||||||
complete = s.InteractBelowPlayer()
|
complete = s.InteractBelowPlayer()
|
||||||
case systems.InputAction_OpenLogs:
|
case systems.InputAction_OpenLogs:
|
||||||
s.viewShortLogs = !s.viewShortLogs
|
s.viewShortLogs = !s.viewShortLogs
|
||||||
case systems.InputAction_MovePlayer_East:
|
case systems.InputAction_Move_East:
|
||||||
complete = s.MovePlayer(model.East)
|
complete = s.MovePlayer(model.East)
|
||||||
case systems.InputAction_MovePlayer_West:
|
case systems.InputAction_Move_West:
|
||||||
complete = s.MovePlayer(model.West)
|
complete = s.MovePlayer(model.West)
|
||||||
case systems.InputAction_MovePlayer_North:
|
case systems.InputAction_Move_North:
|
||||||
complete = s.MovePlayer(model.North)
|
complete = s.MovePlayer(model.North)
|
||||||
case systems.InputAction_MovePlayer_South:
|
case systems.InputAction_Move_South:
|
||||||
complete = s.MovePlayer(model.South)
|
complete = s.MovePlayer(model.South)
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.player.IsNextTurnSkipped() {
|
||||||
|
s.player.SkipNextTurn(false)
|
||||||
|
complete = true
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
// s.someNPC = model.CreateEntity(
|
|
||||||
// model.WithPosition(s.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position),
|
|
||||||
// model.WithName("NPC"),
|
|
||||||
// model.WithPresentation('n', tcell.StyleDefault),
|
|
||||||
// model.WithStats(model.RandomStats(21, 1, 20, []model.Stat{model.Stat_Attributes_Strength, model.Stat_Attributes_Constitution, model.Stat_Attributes_Intelligence, model.Stat_Attributes_Dexterity})),
|
|
||||||
// model.WithHealthData(20, 20, false),
|
|
||||||
// )
|
|
||||||
|
|
||||||
// s.turnSystem.Schedule(20, func() (complete bool, requeue bool) {
|
|
||||||
// s.CalcPathToPlayerAndMove()
|
|
||||||
|
|
||||||
// return true, true
|
|
||||||
// })
|
|
||||||
|
|
||||||
s.eventLog = engine.CreateGameEventLog(100)
|
s.eventLog = engine.CreateGameEventLog(100)
|
||||||
|
|
||||||
s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault)
|
s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault)
|
||||||
|
@ -107,26 +100,25 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
|
||||||
|
|
||||||
entityTable := model.CreateEntityTable()
|
entityTable := model.CreateEntityTable()
|
||||||
|
|
||||||
entityTable.Add(1, func(x, y int) model.Entity { return model.Entity_Imp(x, y) })
|
entityTable.Add(1, func(x, y int) model.Entity {
|
||||||
|
return model.Entity_Imp(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player))
|
||||||
|
})
|
||||||
|
entityTable.Add(1, func(x, y int) model.Entity {
|
||||||
|
return model.Entity_SkeletalKnight(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player))
|
||||||
|
})
|
||||||
|
entityTable.Add(1, func(x, y int) model.Entity {
|
||||||
|
return model.Entity_SkeletalWarrior(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player))
|
||||||
|
})
|
||||||
|
|
||||||
s.npcs = SpawnNPCs(s.dungeon, 7, entityTable)
|
s.npcs = SpawnNPCs(s.dungeon, 7, entityTable)
|
||||||
|
|
||||||
for _, npc := range s.npcs {
|
for _, npc := range s.npcs {
|
||||||
speed := 10
|
if npc.Behavior() != nil {
|
||||||
|
speed := npc.Behavior().Speed
|
||||||
if npc.Speed() != nil {
|
s.turnSystem.Schedule(speed, npc.Behavior().Behavior)
|
||||||
speed = npc.Speed().Speed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.turnSystem.Schedule(speed, func() (complete bool, requeue bool) {
|
|
||||||
CalcPathToPlayerAndMove(25, s.eventLog, s.dungeon, npc, s.player)
|
|
||||||
|
|
||||||
return true, true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// s.dungeon.CurrentLevel().AddEntity(s.someNPC)
|
|
||||||
|
|
||||||
s.viewport = engine.CreateViewport(
|
s.viewport = engine.CreateViewport(
|
||||||
engine.PositionAt(0, 0),
|
engine.PositionAt(0, 0),
|
||||||
s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position,
|
s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position,
|
||||||
|
@ -170,7 +162,7 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
|
||||||
|
|
||||||
newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction))
|
newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction))
|
||||||
|
|
||||||
ent := ps.dungeon.CurrentLevel().EntityAt(newPlayerPos.XY())
|
ent := ps.dungeon.CurrentLevel().EntitiesAt(newPlayerPos.XY())[0]
|
||||||
|
|
||||||
// We are moving into an entity with health data. Attack it.
|
// We are moving into an entity with health data. Attack it.
|
||||||
if ent != nil && ent.HealthData() != nil {
|
if ent != nil && ent.HealthData() != nil {
|
||||||
|
@ -179,7 +171,7 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
ExecuteAttack(ps.eventLog, ps.player, ent)
|
model.ExecuteAttack(ps.eventLog, ps.player, ent)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -192,53 +184,12 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
ps.eventLog.Log("You bump into an impassable object" + model.DirectionName(direction))
|
ps.eventLog.Log("You bump into an impassable object")
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim model.Entity) {
|
|
||||||
hit, precision, evasion, dmg, dmgType := CalculateAttack(attacker, victim)
|
|
||||||
|
|
||||||
attackerName := "Unknown"
|
|
||||||
|
|
||||||
if attacker.Named() != nil {
|
|
||||||
attackerName = attacker.Named().Name
|
|
||||||
}
|
|
||||||
|
|
||||||
victimName := "Unknown"
|
|
||||||
|
|
||||||
if victim.Named() != nil {
|
|
||||||
victimName = victim.Named().Name
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hit {
|
|
||||||
eventLog.Log(fmt.Sprintf("%s attacked %s, but missed ( %v Evasion vs %v Precision)", attackerName, victimName, evasion, precision))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
victim.HealthData().Health -= dmg
|
|
||||||
|
|
||||||
if victim.HealthData().Health <= 0 {
|
|
||||||
victim.HealthData().IsDead = true
|
|
||||||
eventLog.Log(fmt.Sprintf("%s attacked %s, and was victorious ( %v Evasion vs %v Precision)", attackerName, victimName, evasion, precision))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
eventLog.Log(fmt.Sprintf("%s attacked %s, and hit for %v %v damage", attackerName, victimName, dmg, model.DamageTypeName(dmgType)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func CalculateAttack(attacker, victim model.Entity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType model.DamageType) {
|
|
||||||
if attacker.Equipped() != nil && attacker.Equipped().Inventory.AtSlot(model.EquippedSlotDominantHand) != nil {
|
|
||||||
weapon := attacker.Equipped().Inventory.AtSlot(model.EquippedSlotDominantHand)
|
|
||||||
|
|
||||||
return model.PhysicalWeaponAttack(attacker, weapon, victim)
|
|
||||||
} else {
|
|
||||||
return model.UnarmedAttack(attacker, victim)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps *PlayingState) InteractBelowPlayer() (success bool) {
|
func (ps *PlayingState) InteractBelowPlayer() (success bool) {
|
||||||
playerPos := ps.player.Position()
|
playerPos := ps.player.Position()
|
||||||
|
|
||||||
|
@ -356,101 +307,6 @@ func PickUpItemUnderPlayer(eventLog *engine.GameEventLog, dungeon *model.Dungeon
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func HasLineOfSight(dungeon *model.Dungeon, start, end engine.Position) bool {
|
|
||||||
positions := engine.CastRay(start, end)
|
|
||||||
|
|
||||||
for _, p := range positions {
|
|
||||||
if dungeon.CurrentLevel().IsGroundTileOpaque(p.XY()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithinHitRange(pos engine.Position, otherPos engine.Position) bool {
|
|
||||||
return pos.WithOffset(-1, 0) == otherPos || pos.WithOffset(+1, 0) == otherPos || pos.WithOffset(0, -1) == otherPos || pos.WithOffset(0, +1) == otherPos
|
|
||||||
}
|
|
||||||
|
|
||||||
func CalcPathToPlayerAndMove(simulationDistance int, eventLog *engine.GameEventLog, dungeon *model.Dungeon, npc model.Entity, player *model.Player) {
|
|
||||||
if npc.Positioned().Position.DistanceSquared(player.Position()) > simulationDistance*simulationDistance {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if npc.HealthData().IsDead {
|
|
||||||
dungeon.CurrentLevel().DropEntity(npc.UniqueId())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playerVisibleAndInRange := false
|
|
||||||
|
|
||||||
if npc.Positioned().Position.DistanceSquared(player.Position()) < 144 && HasLineOfSight(dungeon, npc.Positioned().Position, player.Position()) {
|
|
||||||
playerVisibleAndInRange = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !playerVisibleAndInRange {
|
|
||||||
randomMove := model.Direction(engine.RandInt(int(model.DirectionNone), int(model.East)))
|
|
||||||
|
|
||||||
nextPos := npc.Positioned().Position
|
|
||||||
|
|
||||||
switch randomMove {
|
|
||||||
case model.North:
|
|
||||||
nextPos = nextPos.WithOffset(0, -1)
|
|
||||||
case model.South:
|
|
||||||
nextPos = nextPos.WithOffset(0, +1)
|
|
||||||
case model.West:
|
|
||||||
nextPos = nextPos.WithOffset(-1, 0)
|
|
||||||
case model.East:
|
|
||||||
nextPos = nextPos.WithOffset(+1, 0)
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if dungeon.CurrentLevel().IsTilePassable(nextPos.XY()) {
|
|
||||||
dungeon.CurrentLevel().MoveEntityTo(
|
|
||||||
npc.UniqueId(),
|
|
||||||
nextPos.X(),
|
|
||||||
nextPos.Y(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if WithinHitRange(npc.Positioned().Position, player.Position()) {
|
|
||||||
ExecuteAttack(eventLog, npc, player)
|
|
||||||
}
|
|
||||||
|
|
||||||
pathToPlayer := engine.FindPath(
|
|
||||||
npc.Positioned().Position,
|
|
||||||
player.Position(),
|
|
||||||
12,
|
|
||||||
func(x, y int) bool {
|
|
||||||
if x == player.Position().X() && y == player.Position().Y() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return dungeon.CurrentLevel().IsTilePassable(x, y)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if pathToPlayer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPos, hasNext := pathToPlayer.Next()
|
|
||||||
|
|
||||||
if !hasNext {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if nextPos.Equals(player.Position()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dungeon.CurrentLevel().MoveEntityTo(npc.UniqueId(), nextPos.X(), nextPos.Y())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps *PlayingState) OnTick(dt int64) (nextState GameState) {
|
func (ps *PlayingState) OnTick(dt int64) (nextState GameState) {
|
||||||
ps.nextGameState = ps
|
ps.nextGameState = ps
|
||||||
|
|
||||||
|
@ -477,8 +333,9 @@ func (ps *PlayingState) CollectDrawables() []engine.Drawable {
|
||||||
tile := visibilityMap[engine.PositionAt(x, y)]
|
tile := visibilityMap[engine.PositionAt(x, y)]
|
||||||
|
|
||||||
if tile != nil {
|
if tile != nil {
|
||||||
if tile.Entity() != nil {
|
|
||||||
return tile.Entity().Entity.Presentable().Rune, tile.Entity().Entity.Presentable().Style
|
if tile.Entities() != nil {
|
||||||
|
return tile.Entities().Entities[0].Presentable().Rune, tile.Entities().Entities[0].Presentable().Style
|
||||||
}
|
}
|
||||||
|
|
||||||
if tile.Item() != nil {
|
if tile.Item() != nil {
|
||||||
|
|
|
@ -12,6 +12,7 @@ const (
|
||||||
InputContext_Play = "play"
|
InputContext_Play = "play"
|
||||||
InputContext_Menu = "menu"
|
InputContext_Menu = "menu"
|
||||||
InputContext_Inventory = "inventory"
|
InputContext_Inventory = "inventory"
|
||||||
|
InputContext_Look = "look"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InputKey string
|
type InputKey string
|
||||||
|
@ -25,10 +26,10 @@ type InputAction int
|
||||||
const (
|
const (
|
||||||
InputAction_None InputAction = iota
|
InputAction_None InputAction = iota
|
||||||
|
|
||||||
InputAction_MovePlayer_North
|
InputAction_Move_North
|
||||||
InputAction_MovePlayer_South
|
InputAction_Move_South
|
||||||
InputAction_MovePlayer_East
|
InputAction_Move_East
|
||||||
InputAction_MovePlayer_West
|
InputAction_Move_West
|
||||||
|
|
||||||
InputAction_Interact
|
InputAction_Interact
|
||||||
InputAction_OpenInventory
|
InputAction_OpenInventory
|
||||||
|
@ -36,6 +37,10 @@ const (
|
||||||
InputAction_OpenLogs
|
InputAction_OpenLogs
|
||||||
InputAction_DropItem
|
InputAction_DropItem
|
||||||
InputAction_InteractItem
|
InputAction_InteractItem
|
||||||
|
InputAction_UseOn
|
||||||
|
InputAction_Describe
|
||||||
|
InputAction_EnterLookMode
|
||||||
|
InputAction_Shoot
|
||||||
|
|
||||||
InputAction_PauseGame
|
InputAction_PauseGame
|
||||||
|
|
||||||
|
@ -58,15 +63,16 @@ type InputSystem struct {
|
||||||
func CreateInputSystemWithDefaultBindings() *InputSystem {
|
func CreateInputSystemWithDefaultBindings() *InputSystem {
|
||||||
return &InputSystem{
|
return &InputSystem{
|
||||||
keyBindings: map[InputKey]InputAction{
|
keyBindings: map[InputKey]InputAction{
|
||||||
InputKeyOf(InputContext_Play, 0, tcell.KeyUp, 0): InputAction_MovePlayer_North,
|
InputKeyOf(InputContext_Play, 0, tcell.KeyUp, 0): InputAction_Move_North,
|
||||||
InputKeyOf(InputContext_Play, 0, tcell.KeyDown, 0): InputAction_MovePlayer_South,
|
InputKeyOf(InputContext_Play, 0, tcell.KeyDown, 0): InputAction_Move_South,
|
||||||
InputKeyOf(InputContext_Play, 0, tcell.KeyLeft, 0): InputAction_MovePlayer_West,
|
InputKeyOf(InputContext_Play, 0, tcell.KeyLeft, 0): InputAction_Move_West,
|
||||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRight, 0): InputAction_MovePlayer_East,
|
InputKeyOf(InputContext_Play, 0, tcell.KeyRight, 0): InputAction_Move_East,
|
||||||
InputKeyOf(InputContext_Play, 0, tcell.KeyEsc, 0): InputAction_PauseGame,
|
InputKeyOf(InputContext_Play, 0, tcell.KeyEsc, 0): InputAction_PauseGame,
|
||||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'i'): InputAction_OpenInventory,
|
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'i'): InputAction_OpenInventory,
|
||||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'l'): InputAction_OpenLogs,
|
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'l'): InputAction_OpenLogs,
|
||||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'e'): InputAction_Interact,
|
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'e'): InputAction_Interact,
|
||||||
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'p'): InputAction_PickUpItem,
|
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'p'): InputAction_PickUpItem,
|
||||||
|
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'k'): InputAction_EnterLookMode,
|
||||||
InputKeyOf(InputContext_Menu, 0, tcell.KeyESC, 0): InputAction_Menu_Exit,
|
InputKeyOf(InputContext_Menu, 0, tcell.KeyESC, 0): InputAction_Menu_Exit,
|
||||||
InputKeyOf(InputContext_Menu, 0, tcell.KeyLeft, 0): InputAction_Menu_HighlightLeft,
|
InputKeyOf(InputContext_Menu, 0, tcell.KeyLeft, 0): InputAction_Menu_HighlightLeft,
|
||||||
InputKeyOf(InputContext_Menu, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight,
|
InputKeyOf(InputContext_Menu, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight,
|
||||||
|
@ -81,6 +87,13 @@ func CreateInputSystemWithDefaultBindings() *InputSystem {
|
||||||
InputKeyOf(InputContext_Inventory, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight,
|
InputKeyOf(InputContext_Inventory, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight,
|
||||||
InputKeyOf(InputContext_Inventory, 0, tcell.KeyUp, 0): InputAction_Menu_HighlightUp,
|
InputKeyOf(InputContext_Inventory, 0, tcell.KeyUp, 0): InputAction_Menu_HighlightUp,
|
||||||
InputKeyOf(InputContext_Inventory, 0, tcell.KeyDown, 0): InputAction_Menu_HighlightDown,
|
InputKeyOf(InputContext_Inventory, 0, tcell.KeyDown, 0): InputAction_Menu_HighlightDown,
|
||||||
|
InputKeyOf(InputContext_Look, 0, tcell.KeyUp, 0): InputAction_Move_North,
|
||||||
|
InputKeyOf(InputContext_Look, 0, tcell.KeyDown, 0): InputAction_Move_South,
|
||||||
|
InputKeyOf(InputContext_Look, 0, tcell.KeyLeft, 0): InputAction_Move_West,
|
||||||
|
InputKeyOf(InputContext_Look, 0, tcell.KeyRight, 0): InputAction_Move_East,
|
||||||
|
InputKeyOf(InputContext_Look, 0, tcell.KeyRune, 'd'): InputAction_Describe,
|
||||||
|
InputKeyOf(InputContext_Look, 0, tcell.KeyRune, 'a'): InputAction_Shoot,
|
||||||
|
InputKeyOf(InputContext_Look, 0, tcell.KeyESC, 0): InputAction_Menu_Exit,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,33 +53,48 @@ func (uihp *UIHealthBar) Draw(v views.View) {
|
||||||
|
|
||||||
uihp.window.Draw(v)
|
uihp.window.Draw(v)
|
||||||
|
|
||||||
stages := []rune{'█', '▓', '▒', '░'} // 0 = 1.0, 1 = 0.75, 2 = 0.5, 3 = 0.25
|
stages := []string{"█", "▓", "▒", "░"} // 0 = 1.0, 1 = 0.75, 2 = 0.5, 3 = 0.25
|
||||||
|
|
||||||
percentage := (float64(w) - 2.0) * (float64(uihp.player.HealthData().Health) / float64(uihp.player.HealthData().MaxHealth))
|
percentage := (float64(w) - 2.0) * (float64(uihp.player.HealthData().Health) / float64(uihp.player.HealthData().MaxHealth))
|
||||||
|
|
||||||
whole := math.Trunc(percentage)
|
whole := math.Trunc(percentage)
|
||||||
last := percentage - whole
|
last := percentage - whole
|
||||||
|
|
||||||
|
hpBar := ""
|
||||||
hpStyle := tcell.StyleDefault.Foreground(tcell.ColorIndianRed)
|
hpStyle := tcell.StyleDefault.Foreground(tcell.ColorIndianRed)
|
||||||
|
|
||||||
for i := range int(whole) {
|
for range int(whole) {
|
||||||
v.SetContent(x+1+i, y+1, stages[0], nil, hpStyle)
|
hpBar += stages[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if last > 0.0 {
|
lastRune := func() string {
|
||||||
|
if last <= 0.0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
if last <= 0.25 {
|
if last <= 0.25 {
|
||||||
v.SetContent(x+1+int(whole), y+1, stages[3], nil, hpStyle)
|
return stages[3]
|
||||||
}
|
}
|
||||||
|
|
||||||
if last <= 0.50 {
|
if last <= 0.50 {
|
||||||
v.SetContent(x+1+int(whole), y+1, stages[2], nil, hpStyle)
|
return stages[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
if last <= 0.75 {
|
if last <= 0.75 {
|
||||||
v.SetContent(x+1+int(whole), y+1, stages[1], nil, hpStyle)
|
return stages[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if last <= 1.00 {
|
||||||
|
return stages[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hpBar += lastRune()
|
||||||
|
|
||||||
|
engine.DrawText(x+1, y+1, hpBar, hpStyle, v)
|
||||||
|
|
||||||
hpText := fmt.Sprintf("%v/%v", uihp.player.HealthData().Health, uihp.player.HealthData().MaxHealth)
|
hpText := fmt.Sprintf("%v/%v", uihp.player.HealthData().Health, uihp.player.HealthData().MaxHealth)
|
||||||
|
|
||||||
engine.DrawText(
|
engine.DrawText(
|
||||||
|
|
Loading…
Add table
Reference in a new issue