Character stats menu start, key bindings menu start, add tophat item

This commit is contained in:
Miroslav Vasilev 2024-06-15 17:46:09 +03:00
parent 9445958338
commit 8e6309c824
13 changed files with 289 additions and 92 deletions

View file

@ -3,22 +3,9 @@ package model
import ( import (
"fmt" "fmt"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
) "mvvasilev/last_light/game/systems"
type ArrowSprite rune "github.com/gdamore/tcell/v2"
//
// \ | /
//
// ─ + ─
//
// / | \
const (
ProjectileSprite_NorthSouth ArrowSprite = '|'
ProjectileSprite_EastWest ArrowSprite = '─'
ProjectileSprite_NorthEastSouthWest ArrowSprite = '/'
ProjectileSprite_NorthWestSouthEast ArrowSprite = '\\'
) )
func ProjectileBehavior(eventLog *engine.GameEventLog, dungeon *Dungeon) func(npc Entity) (complete bool, requeue bool) { func ProjectileBehavior(eventLog *engine.GameEventLog, dungeon *Dungeon) func(npc Entity) (complete bool, requeue bool) {
@ -76,7 +63,8 @@ func ProjectileFollowPathNext(npc Entity, eventLog *engine.GameEventLog, dungeon
return false return false
} }
ExecuteAttack(eventLog, projectileData.Source, nextTileEntityData.Entity) // TODO: calculate additional projectile damage
ExecuteAttack(eventLog, projectileData.Source, nextTileEntityData.Entity, true)
return false return false
} }
@ -86,7 +74,7 @@ func ProjectileFollowPathNext(npc Entity, eventLog *engine.GameEventLog, dungeon
return return
} }
func HostileNPCBehavior(eventLog *engine.GameEventLog, dungeon *Dungeon, player *Player) func(npc Entity) (complete bool, requeue bool) { func HostileMeleeNPCBehavior(eventLog *engine.GameEventLog, dungeon *Dungeon, player *Player) func(npc Entity) (complete bool, requeue bool) {
return func(npc Entity) (complete bool, requeue bool) { return func(npc Entity) (complete bool, requeue bool) {
CalcPathToPlayerAndMove(25, eventLog, dungeon, npc, player) CalcPathToPlayerAndMove(25, eventLog, dungeon, npc, player)
@ -94,6 +82,16 @@ func HostileNPCBehavior(eventLog *engine.GameEventLog, dungeon *Dungeon, player
} }
} }
func HostileRangedNPCBehavior(eventLog *engine.GameEventLog, dungeon *Dungeon, player *Player) func(npc Entity) (complete bool, requeue bool) {
return func(npc Entity) (complete bool, requeue bool) {
return true, true
}
}
func CalcPathToPlayerAndKeepDistance(simulationDistance int, eventLog *engine.GameEventLog, dungeon *Dungeon, npc Entity, player *Player) {
}
func CalcPathToPlayerAndMove(simulationDistance int, eventLog *engine.GameEventLog, dungeon *Dungeon, npc Entity, player *Player) { func CalcPathToPlayerAndMove(simulationDistance int, eventLog *engine.GameEventLog, dungeon *Dungeon, npc Entity, player *Player) {
if npc.Positioned().Position.DistanceSquared(player.Position()) > simulationDistance*simulationDistance { if npc.Positioned().Position.DistanceSquared(player.Position()) > simulationDistance*simulationDistance {
return return
@ -141,7 +139,7 @@ func CalcPathToPlayerAndMove(simulationDistance int, eventLog *engine.GameEventL
} }
if WithinHitRange(npc.Positioned().Position, player.Position()) { if WithinHitRange(npc.Positioned().Position, player.Position()) {
ExecuteAttack(eventLog, npc, player) ExecuteAttack(eventLog, npc, player, false)
} }
pathToPlayer := engine.FindPath( pathToPlayer := engine.FindPath(
@ -193,8 +191,8 @@ 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 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) { func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim Entity, isRanged bool) {
hit, precision, evasion, dmg, dmgType := CalculateAttack(attacker, victim) hit, precision, evasion, dmg, dmgType := CalculateAttack(attacker, victim, isRanged)
if attacker.Projectile() != nil { if attacker.Projectile() != nil {
attacker = attacker.Projectile().Source attacker = attacker.Projectile().Source
@ -232,12 +230,140 @@ func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim Entity) {
eventLog.Log(fmt.Sprintf("%s attacked %s, and hit for %v %v damage", attackerName, victimName, dmg, DamageTypeName(dmgType))) 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) { func CalculateAttack(attacker, victim Entity, isRanged bool) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) {
if attacker.Equipped() != nil && attacker.Equipped().Inventory.AtSlot(EquippedSlotDominantHand) != nil { if attacker.Equipped() != nil && attacker.Equipped().Inventory.AtSlot(EquippedSlotDominantHand) != nil {
weapon := attacker.Equipped().Inventory.AtSlot(EquippedSlotDominantHand) weapon := attacker.Equipped().Inventory.AtSlot(EquippedSlotDominantHand)
if weapon.Damaging() != nil {
// If the weapon is ranged, but the combat isn't, do unarmed
if isRanged && !weapon.Damaging().IsRanged {
return UnarmedAttack(attacker, victim)
}
// Doing melee damage from ranged? Don't think so.
if !isRanged && weapon.Damaging().IsRanged {
return false, 0, 0, 0, DamageType_Physical_Unarmed
}
}
return PhysicalWeaponAttack(attacker, weapon, victim) return PhysicalWeaponAttack(attacker, weapon, victim)
} else { } else {
return UnarmedAttack(attacker, victim) return UnarmedAttack(attacker, victim)
} }
} }
func ShootProjectile(shooter Entity, target engine.Position, eventLog *engine.GameEventLog, dungeon *Dungeon, turnSystem *systems.TurnSystem) (success bool) {
success = false
logMessage := func(msg string) {
if eventLog != nil {
eventLog.Log(msg)
}
}
if shooter.Equipped() == nil || shooter.Positioned() == nil {
return
}
shooterName := "Unknown"
if shooter.Named() != nil {
shooterName = shooter.Named().Name
}
weapon := shooter.Equipped().Inventory.AtSlot(EquippedSlotDominantHand)
if weapon == nil {
logMessage(fmt.Sprintf("%s wants to shoot, but doesn't have anything equipped!", shooterName))
return
}
if weapon.Damaging() == nil || !weapon.Damaging().IsRanged {
itemName := "dominant hand"
if weapon.Named() != nil {
itemName = weapon.Named().Name
}
logMessage(fmt.Sprintf("%s wants to use %s for this, but can't!", shooterName, itemName))
return
}
projectileItem := shooter.Equipped().Inventory.AtSlot(EquippedSlotOffhand)
if projectileItem == nil {
logMessage(fmt.Sprintf("%s doesn't have any projectiles equipped!", shooterName))
return
}
if projectileItem.ProjectileData() == nil {
projectileItemName := "off hand"
if projectileItem.Named() != nil {
projectileItemName = projectileItem.Named().Name
}
logMessage(fmt.Sprintf("%s can't use %s as ammo", shooterName, projectileItemName))
return
}
distance := target.Distance(shooter.Positioned().Position)
if distance > 12 {
// logMessage("Can't see in the dark that far")
return
}
path := engine.LinePath(
shooter.Positioned().Position,
target,
)
if path == nil {
// logMessage("Can't shoot there, something is in the way")
return
}
direction := map[engine.Position]ProjectileDirection{
engine.PositionAt(-1, -1): ProjectileDirection_NorthWestSouthEast,
engine.PositionAt(+1, +1): ProjectileDirection_NorthWestSouthEast,
engine.PositionAt(-1, +1): ProjectileDirection_NorthEastSouthWest,
engine.PositionAt(+1, -1): ProjectileDirection_NorthEastSouthWest,
engine.PositionAt(0, +1): ProjectileDirection_NorthSouth,
engine.PositionAt(0, -1): ProjectileDirection_NorthSouth,
engine.PositionAt(-1, 0): ProjectileDirection_EastWest,
engine.PositionAt(+1, 0): ProjectileDirection_EastWest,
}[shooter.Positioned().Position.Diff(target).Sign()]
projectile := Entity_Projectile(
"Projectile",
projectileItem.ProjectileData().Sprites[direction],
tcell.StyleDefault,
shooter,
path,
eventLog,
dungeon,
)
turnSystem.Schedule(
projectile.Behavior().Speed,
projectile.Behavior().Behavior,
)
if projectileItem.Quantifiable() == nil {
shooter.Equipped().Inventory.Equip(nil, EquippedSlotOffhand)
} else {
projectileItem.Quantifiable().CurrentQuantity--
if projectileItem.Quantifiable().CurrentQuantity <= 0 {
shooter.Equipped().Inventory.Equip(nil, EquippedSlotOffhand)
}
}
return true
}

View file

@ -27,6 +27,7 @@ func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player {
} }
p.Inventory().Push(Item_Bow()) p.Inventory().Push(Item_Bow())
p.Inventory().Push(Item_Arrow(8))
p.HealthData().MaxHealth = BaseMaxHealth(p) p.HealthData().MaxHealth = BaseMaxHealth(p)
p.HealthData().Health = p.HealthData().MaxHealth p.HealthData().Health = p.HealthData().MaxHealth

View file

@ -144,3 +144,13 @@ func (i *BasicInventory) ItemAt(x, y int) (item Item) {
return i.contents[index] return i.contents[index]
} }
func (i *BasicInventory) Find(filter func(i Item) bool) Item {
for _, c := range i.contents {
if filter(c) {
return c
}
}
return nil
}

View file

@ -16,9 +16,19 @@ const (
MetaItemType_Magic_Armour MetaItemType_Magic_Armour
MetaItemType_Armour MetaItemType_Armour
MetaItemType_Consumable MetaItemType_Consumable
MetaItemType_Projectile
MetaItemType_Potion MetaItemType_Potion
) )
type ProjectileDirection int
const (
ProjectileDirection_NorthSouth ProjectileDirection = iota
ProjectileDirection_EastWest
ProjectileDirection_NorthEastSouthWest
ProjectileDirection_NorthWestSouthEast
)
type Item interface { type Item interface {
TileIcon() rune TileIcon() rune
Icon() string Icon() string
@ -33,6 +43,12 @@ type Item interface {
Damaging() *Item_DamagingComponent Damaging() *Item_DamagingComponent
StatModifier() *Item_StatModifierComponent StatModifier() *Item_StatModifierComponent
MetaTypes() *Item_MetaTypesComponent MetaTypes() *Item_MetaTypesComponent
ProjectileData() *Item_ProjectileComponent
}
type Item_ProjectileComponent struct {
Sprites map[ProjectileDirection]rune
AdditionalDamageRoll func() (dmg int, dmgType DamageType)
} }
type Item_QuantifiableComponent struct { type Item_QuantifiableComponent struct {
@ -78,14 +94,15 @@ type BaseItem struct {
style tcell.Style style tcell.Style
itemType ItemType itemType ItemType
quantifiable *Item_QuantifiableComponent quantifiable *Item_QuantifiableComponent
usable *Item_UsableComponent usable *Item_UsableComponent
equippable *Item_EquippableComponent equippable *Item_EquippableComponent
named *Item_NamedComponent named *Item_NamedComponent
described *Item_DescribedComponent described *Item_DescribedComponent
damaging *Item_DamagingComponent damaging *Item_DamagingComponent
statModifier *Item_StatModifierComponent statModifier *Item_StatModifierComponent
metaTypes *Item_MetaTypesComponent metaTypes *Item_MetaTypesComponent
projectileData *Item_ProjectileComponent
} }
func (i *BaseItem) TileIcon() rune { func (i *BaseItem) TileIcon() rune {
@ -136,6 +153,10 @@ func (i *BaseItem) MetaTypes() *Item_MetaTypesComponent {
return i.metaTypes return i.metaTypes
} }
func (i *BaseItem) ProjectileData() *Item_ProjectileComponent {
return i.projectileData
}
func createBaseItem(itemType ItemType, tileIcon rune, icon string, style tcell.Style, components ...func(*BaseItem)) *BaseItem { func createBaseItem(itemType ItemType, tileIcon rune, icon string, style tcell.Style, components ...func(*BaseItem)) *BaseItem {
i := &BaseItem{ i := &BaseItem{
itemType: itemType, itemType: itemType,
@ -219,3 +240,12 @@ func item_WithMetaTypes(metaTypes []ItemMetaType) func(*BaseItem) {
} }
} }
} }
func item_WithProjectileData(sprites map[ProjectileDirection]rune, additionalDamageRoll func() (dmg int, dmgType DamageType)) func(*BaseItem) {
return func(bi *BaseItem) {
bi.projectileData = &Item_ProjectileComponent{
Sprites: sprites,
AdditionalDamageRoll: additionalDamageRoll,
}
}
}

View file

@ -15,6 +15,7 @@ const (
ItemType_SmallHealthPotion ItemType_SmallHealthPotion
ItemType_HealthPotion ItemType_HealthPotion
ItemType_LargeHealthPotion ItemType_LargeHealthPotion
ItemType_Arrow
// Weapons // Weapons
ItemType_Bow ItemType_Bow
@ -30,6 +31,7 @@ const (
ItemType_Quarterstaff ItemType_Quarterstaff
// Armour // Armour
ItemType_TopHat
// Special // Special
) )
@ -353,6 +355,41 @@ func Item_Spear() Item {
) )
} }
func Item_Arrow(amount int) Item {
return createBaseItem(
ItemType_Arrow,
'-',
"»->",
tcell.StyleDefault.Foreground(tcell.ColorBurlyWood),
item_WithQuantity(amount, 32),
item_WithName("Arrow", tcell.StyleDefault),
item_WithProjectileData(
map[ProjectileDirection]rune{
ProjectileDirection_NorthSouth: '|',
ProjectileDirection_EastWest: '─',
ProjectileDirection_NorthEastSouthWest: '/',
ProjectileDirection_NorthWestSouthEast: '\\',
},
nil,
),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Consumable, MetaItemType_Projectile}),
item_WithEquippable(EquippedSlotOffhand),
)
}
func Item_TopHat() Item {
return createBaseItem(
ItemType_TopHat,
'■',
"_█_",
tcell.StyleDefault.Foreground(tcell.ColorLightYellow),
item_WithName("Top Hat", tcell.StyleDefault),
item_WithDescription("Smells fishy...", tcell.StyleDefault),
item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Armour, MetaItemType_Armour}),
item_WithEquippable(EquippedSlotHead),
)
}
// func ItemTypeGold() ItemType { // func ItemTypeGold() ItemType {
// return &BasicItemType{ // return &BasicItemType{
// id: 1, // id: 1,

View file

@ -102,6 +102,10 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) (dLevel *Dun
return Item_HealthPotion() return Item_HealthPotion()
}) })
genTable.Add(3, func() Item {
return Item_Arrow(engine.RandInt(1, 5))
})
itemPool := []Item{ itemPool := []Item{
Item_Bow(), Item_Bow(),
Item_Longsword(), Item_Longsword(),

View file

@ -0,0 +1,26 @@
package state
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/ui"
)
type CharacterStatsState struct {
window *ui.UIWindow
player *model.Player
}
func (css *CharacterStatsState) InputContext() systems.InputContext {
return systems.InputContext_Menu
}
func (css *CharacterStatsState) OnTick(dt int64) GameState {
return css
}
func (css *CharacterStatsState) CollectDrawables() []engine.Drawable {
return []engine.Drawable{}
}

View file

@ -60,6 +60,9 @@ func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) {
if item.Usable() != nil { if item.Usable() != nil {
item.Usable().Use(iss.eventLog, iss.dungeon, iss.player) item.Usable().Use(iss.eventLog, iss.dungeon, iss.player)
iss.player.Inventory().ReduceQuantityAt(iss.selectedInventorySlot.X(), iss.selectedInventorySlot.Y(), 1)
return
} }
if item.Equippable() != nil { if item.Equippable() != nil {
@ -70,7 +73,7 @@ func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) {
iss.player.Inventory().Equip(item, item.Equippable().Slot) iss.player.Inventory().Equip(item, item.Equippable().Slot)
} }
iss.player.Inventory().ReduceQuantityAt(iss.selectedInventorySlot.X(), iss.selectedInventorySlot.Y(), 1) iss.player.Inventory().Drop(iss.selectedInventorySlot.X(), iss.selectedInventorySlot.Y())
case systems.InputAction_DropItem: case systems.InputAction_DropItem:
iss.player.Inventory().Drop(iss.selectedInventorySlot.XY()) iss.player.Inventory().Drop(iss.selectedInventorySlot.XY())

View file

@ -79,72 +79,21 @@ func (ls *LookState) OnTick(dt int64) GameState {
} }
func (ls *LookState) ShootEquippedWeapon() { 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
dX, dY := ls.lookCursorCoordsToDungeonCoords() dX, dY := ls.lookCursorCoordsToDungeonCoords()
cursorPos := engine.PositionAt(dX, dY) cursorPos := engine.PositionAt(dX, dY)
distance := cursorPos.Distance(ls.player.Position()) success := model.ShootProjectile(
ls.player,
if distance > 12 { cursorPos,
ls.eventLog.Log("Can't see in the dark that far") ls.eventLog,
ls.dungeon,
return ls.turnSystem,
}
path := engine.LinePath(
ls.player.Position(),
engine.PositionAt(dX, dY),
) )
if path == nil { if !success {
ls.eventLog.Log("Can't shoot there, something is in the way")
return return
} }
direction := ls.player.Position().Diff(cursorPos).Sign()
sprites := map[engine.Position]model.ArrowSprite{
engine.PositionAt(-1, -1): model.ProjectileSprite_NorthWestSouthEast,
engine.PositionAt(+1, -1): model.ProjectileSprite_NorthWestSouthEast,
engine.PositionAt(-1, +1): model.ProjectileSprite_NorthEastSouthWest,
engine.PositionAt(+1, +1): model.ProjectileSprite_NorthEastSouthWest,
engine.PositionAt(0, +1): model.ProjectileSprite_NorthSouth,
engine.PositionAt(0, -1): model.ProjectileSprite_NorthSouth,
engine.PositionAt(-1, 0): model.ProjectileSprite_EastWest,
engine.PositionAt(+1, 0): model.ProjectileSprite_EastWest,
}
sprite := sprites[direction]
projectile := model.Entity_Projectile("Arrow", rune(sprite), tcell.StyleDefault, ls.player, path, ls.eventLog, ls.dungeon)
ls.turnSystem.Schedule(
projectile.Behavior().Speed,
projectile.Behavior().Behavior,
)
ls.player.SkipNextTurn(true) ls.player.SkipNextTurn(true)
ls.nextGameState = ls.prevState ls.nextGameState = ls.prevState

View file

@ -101,13 +101,13 @@ func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.Inp
entityTable := model.CreateEntityTable() entityTable := model.CreateEntityTable()
entityTable.Add(1, func(x, y int) model.Entity { entityTable.Add(1, func(x, y int) model.Entity {
return model.Entity_Imp(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player)) return model.Entity_Imp(x, y, model.HostileMeleeNPCBehavior(s.eventLog, s.dungeon, s.player))
}) })
entityTable.Add(1, func(x, y int) model.Entity { entityTable.Add(1, func(x, y int) model.Entity {
return model.Entity_SkeletalKnight(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player)) return model.Entity_SkeletalKnight(x, y, model.HostileMeleeNPCBehavior(s.eventLog, s.dungeon, s.player))
}) })
entityTable.Add(1, func(x, y int) model.Entity { entityTable.Add(1, func(x, y int) model.Entity {
return model.Entity_SkeletalWarrior(x, y, model.HostileNPCBehavior(s.eventLog, s.dungeon, s.player)) return model.Entity_SkeletalWarrior(x, y, model.HostileMeleeNPCBehavior(s.eventLog, s.dungeon, s.player))
}) })
s.npcs = SpawnNPCs(s.dungeon, 7, entityTable) s.npcs = SpawnNPCs(s.dungeon, 7, entityTable)
@ -171,7 +171,7 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) {
return false return false
} }
model.ExecuteAttack(ps.eventLog, ps.player, ent) model.ExecuteAttack(ps.eventLog, ps.player, ent, false)
return true return true
} }

View file

@ -0,0 +1 @@
package menu

View file

@ -0,0 +1 @@
package menu

View file

@ -148,6 +148,15 @@ func drawEquipmentSlot(screenX, screenY int, item model.Item, highlighted bool,
style = highlightStyle style = highlightStyle
} }
if item.Quantifiable() != nil {
ui.CreateSingleLineUILabel(
screenX,
screenY,
fmt.Sprintf("%03d", item.Quantifiable().CurrentQuantity),
style,
).Draw(v)
}
ui.CreateSingleLineUILabel( ui.CreateSingleLineUILabel(
screenX, screenX,
screenY+1, screenY+1,