From 8e6309c824837436065af8af0168f62191679bd7 Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Sat, 15 Jun 2024 17:46:09 +0300 Subject: [PATCH] Character stats menu start, key bindings menu start, add tophat item --- game/model/entity_behavior.go | 168 ++++++++++++++++++--- game/model/entity_player.go | 1 + game/model/inventory.go | 10 ++ game/model/item.go | 46 +++++- game/model/items.go | 37 +++++ game/model/world_dungeon.go | 4 + game/state/character_stats_state.go | 26 ++++ game/state/inventory_screen_state.go | 5 +- game/state/look_state.go | 65 +------- game/state/playing_state.go | 8 +- game/ui/menu/menu_character_stats.go | 1 + game/ui/menu/menu_key_bindings.go | 1 + game/ui/menu/menu_player_inventory_menu.go | 9 ++ 13 files changed, 289 insertions(+), 92 deletions(-) create mode 100644 game/state/character_stats_state.go create mode 100644 game/ui/menu/menu_character_stats.go create mode 100644 game/ui/menu/menu_key_bindings.go diff --git a/game/model/entity_behavior.go b/game/model/entity_behavior.go index 69bb85b..4b42ede 100644 --- a/game/model/entity_behavior.go +++ b/game/model/entity_behavior.go @@ -3,22 +3,9 @@ package model import ( "fmt" "mvvasilev/last_light/engine" -) + "mvvasilev/last_light/game/systems" -type ArrowSprite rune - -// -// \ | / -// -// ─ + ─ -// -// / | \ - -const ( - ProjectileSprite_NorthSouth ArrowSprite = '|' - ProjectileSprite_EastWest ArrowSprite = '─' - ProjectileSprite_NorthEastSouthWest ArrowSprite = '/' - ProjectileSprite_NorthWestSouthEast ArrowSprite = '\\' + "github.com/gdamore/tcell/v2" ) 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 } - ExecuteAttack(eventLog, projectileData.Source, nextTileEntityData.Entity) + // TODO: calculate additional projectile damage + ExecuteAttack(eventLog, projectileData.Source, nextTileEntityData.Entity, true) return false } @@ -86,7 +74,7 @@ func ProjectileFollowPathNext(npc Entity, eventLog *engine.GameEventLog, dungeon 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) { 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) { if npc.Positioned().Position.DistanceSquared(player.Position()) > simulationDistance*simulationDistance { return @@ -141,7 +139,7 @@ func CalcPathToPlayerAndMove(simulationDistance int, eventLog *engine.GameEventL } if WithinHitRange(npc.Positioned().Position, player.Position()) { - ExecuteAttack(eventLog, npc, player) + ExecuteAttack(eventLog, npc, player, false) } 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 } -func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim Entity) { - hit, precision, evasion, dmg, dmgType := CalculateAttack(attacker, victim) +func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim Entity, isRanged bool) { + hit, precision, evasion, dmg, dmgType := CalculateAttack(attacker, victim, isRanged) if attacker.Projectile() != nil { 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))) } -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 { 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) } else { 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 +} diff --git a/game/model/entity_player.go b/game/model/entity_player.go index d1976e6..d9b1d61 100644 --- a/game/model/entity_player.go +++ b/game/model/entity_player.go @@ -27,6 +27,7 @@ func CreatePlayer(x, y int, playerBaseStats map[Stat]int) *Player { } p.Inventory().Push(Item_Bow()) + p.Inventory().Push(Item_Arrow(8)) p.HealthData().MaxHealth = BaseMaxHealth(p) p.HealthData().Health = p.HealthData().MaxHealth diff --git a/game/model/inventory.go b/game/model/inventory.go index eafda82..f37c206 100644 --- a/game/model/inventory.go +++ b/game/model/inventory.go @@ -144,3 +144,13 @@ func (i *BasicInventory) ItemAt(x, y int) (item Item) { 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 +} diff --git a/game/model/item.go b/game/model/item.go index 329b6bc..9904835 100644 --- a/game/model/item.go +++ b/game/model/item.go @@ -16,9 +16,19 @@ const ( MetaItemType_Magic_Armour MetaItemType_Armour MetaItemType_Consumable + MetaItemType_Projectile MetaItemType_Potion ) +type ProjectileDirection int + +const ( + ProjectileDirection_NorthSouth ProjectileDirection = iota + ProjectileDirection_EastWest + ProjectileDirection_NorthEastSouthWest + ProjectileDirection_NorthWestSouthEast +) + type Item interface { TileIcon() rune Icon() string @@ -33,6 +43,12 @@ type Item interface { Damaging() *Item_DamagingComponent StatModifier() *Item_StatModifierComponent MetaTypes() *Item_MetaTypesComponent + ProjectileData() *Item_ProjectileComponent +} + +type Item_ProjectileComponent struct { + Sprites map[ProjectileDirection]rune + AdditionalDamageRoll func() (dmg int, dmgType DamageType) } type Item_QuantifiableComponent struct { @@ -78,14 +94,15 @@ type BaseItem struct { style tcell.Style itemType ItemType - quantifiable *Item_QuantifiableComponent - usable *Item_UsableComponent - equippable *Item_EquippableComponent - named *Item_NamedComponent - described *Item_DescribedComponent - damaging *Item_DamagingComponent - statModifier *Item_StatModifierComponent - metaTypes *Item_MetaTypesComponent + quantifiable *Item_QuantifiableComponent + usable *Item_UsableComponent + equippable *Item_EquippableComponent + named *Item_NamedComponent + described *Item_DescribedComponent + damaging *Item_DamagingComponent + statModifier *Item_StatModifierComponent + metaTypes *Item_MetaTypesComponent + projectileData *Item_ProjectileComponent } func (i *BaseItem) TileIcon() rune { @@ -136,6 +153,10 @@ func (i *BaseItem) MetaTypes() *Item_MetaTypesComponent { 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 { i := &BaseItem{ 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, + } + } +} diff --git a/game/model/items.go b/game/model/items.go index ed713bc..148ae73 100644 --- a/game/model/items.go +++ b/game/model/items.go @@ -15,6 +15,7 @@ const ( ItemType_SmallHealthPotion ItemType_HealthPotion ItemType_LargeHealthPotion + ItemType_Arrow // Weapons ItemType_Bow @@ -30,6 +31,7 @@ const ( ItemType_Quarterstaff // Armour + ItemType_TopHat // 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 { // return &BasicItemType{ // id: 1, diff --git a/game/model/world_dungeon.go b/game/model/world_dungeon.go index 37d9611..cf324a8 100644 --- a/game/model/world_dungeon.go +++ b/game/model/world_dungeon.go @@ -102,6 +102,10 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) (dLevel *Dun return Item_HealthPotion() }) + genTable.Add(3, func() Item { + return Item_Arrow(engine.RandInt(1, 5)) + }) + itemPool := []Item{ Item_Bow(), Item_Longsword(), diff --git a/game/state/character_stats_state.go b/game/state/character_stats_state.go new file mode 100644 index 0000000..aa986a5 --- /dev/null +++ b/game/state/character_stats_state.go @@ -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{} +} diff --git a/game/state/inventory_screen_state.go b/game/state/inventory_screen_state.go index 7ba1fc2..6548221 100644 --- a/game/state/inventory_screen_state.go +++ b/game/state/inventory_screen_state.go @@ -60,6 +60,9 @@ func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) { if item.Usable() != nil { 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 { @@ -70,7 +73,7 @@ func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) { 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: iss.player.Inventory().Drop(iss.selectedInventorySlot.XY()) diff --git a/game/state/look_state.go b/game/state/look_state.go index 49ca998..c08bd21 100644 --- a/game/state/look_state.go +++ b/game/state/look_state.go @@ -79,72 +79,21 @@ func (ls *LookState) OnTick(dt int64) GameState { } 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() cursorPos := engine.PositionAt(dX, dY) - distance := cursorPos.Distance(ls.player.Position()) - - if distance > 12 { - ls.eventLog.Log("Can't see in the dark that far") - - return - } - - path := engine.LinePath( - ls.player.Position(), - engine.PositionAt(dX, dY), + success := model.ShootProjectile( + ls.player, + cursorPos, + ls.eventLog, + ls.dungeon, + ls.turnSystem, ) - if path == nil { - ls.eventLog.Log("Can't shoot there, something is in the way") + if !success { 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.nextGameState = ls.prevState diff --git a/game/state/playing_state.go b/game/state/playing_state.go index e2534b7..7050ac4 100644 --- a/game/state/playing_state.go +++ b/game/state/playing_state.go @@ -101,13 +101,13 @@ 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, 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 { - 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 { - 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) @@ -171,7 +171,7 @@ func (ps *PlayingState) MovePlayer(direction model.Direction) (success bool) { return false } - model.ExecuteAttack(ps.eventLog, ps.player, ent) + model.ExecuteAttack(ps.eventLog, ps.player, ent, false) return true } diff --git a/game/ui/menu/menu_character_stats.go b/game/ui/menu/menu_character_stats.go new file mode 100644 index 0000000..001361e --- /dev/null +++ b/game/ui/menu/menu_character_stats.go @@ -0,0 +1 @@ +package menu diff --git a/game/ui/menu/menu_key_bindings.go b/game/ui/menu/menu_key_bindings.go new file mode 100644 index 0000000..001361e --- /dev/null +++ b/game/ui/menu/menu_key_bindings.go @@ -0,0 +1 @@ +package menu diff --git a/game/ui/menu/menu_player_inventory_menu.go b/game/ui/menu/menu_player_inventory_menu.go index 0e4a285..7e23fe6 100644 --- a/game/ui/menu/menu_player_inventory_menu.go +++ b/game/ui/menu/menu_player_inventory_menu.go @@ -148,6 +148,15 @@ func drawEquipmentSlot(screenX, screenY int, item model.Item, highlighted bool, style = highlightStyle } + if item.Quantifiable() != nil { + ui.CreateSingleLineUILabel( + screenX, + screenY, + fmt.Sprintf("%03d", item.Quantifiable().CurrentQuantity), + style, + ).Draw(v) + } + ui.CreateSingleLineUILabel( screenX, screenY+1,