diff --git a/engine/arbitrary_drawable.go b/engine/engine_arbitrary_drawable.go similarity index 100% rename from engine/arbitrary_drawable.go rename to engine/engine_arbitrary_drawable.go diff --git a/engine/bounding_box.go b/engine/engine_bounding_box.go similarity index 100% rename from engine/bounding_box.go rename to engine/engine_bounding_box.go diff --git a/engine/event_logger.go b/engine/engine_event_logger.go similarity index 100% rename from engine/event_logger.go rename to engine/engine_event_logger.go diff --git a/engine/fov.go b/engine/engine_fov.go similarity index 100% rename from engine/fov.go rename to engine/engine_fov.go diff --git a/engine/grid.go b/engine/engine_grid.go similarity index 100% rename from engine/grid.go rename to engine/engine_grid.go diff --git a/engine/layers.go b/engine/engine_layers.go similarity index 100% rename from engine/layers.go rename to engine/engine_layers.go diff --git a/engine/path.go b/engine/engine_path.go similarity index 100% rename from engine/path.go rename to engine/engine_path.go diff --git a/engine/pathfinding.go b/engine/engine_pathfinding.go similarity index 100% rename from engine/pathfinding.go rename to engine/engine_pathfinding.go diff --git a/engine/pathfinding_test.go b/engine/engine_pathfinding_test.go similarity index 100% rename from engine/pathfinding_test.go rename to engine/engine_pathfinding_test.go diff --git a/engine/priority_queue.go b/engine/engine_priority_queue.go similarity index 100% rename from engine/priority_queue.go rename to engine/engine_priority_queue.go diff --git a/engine/raw.go b/engine/engine_raw.go similarity index 100% rename from engine/raw.go rename to engine/engine_raw.go diff --git a/engine/raycasting.go b/engine/engine_raycasting.go similarity index 100% rename from engine/raycasting.go rename to engine/engine_raycasting.go diff --git a/engine/rectangle.go b/engine/engine_rectangle.go similarity index 100% rename from engine/rectangle.go rename to engine/engine_rectangle.go diff --git a/engine/render_context.go b/engine/engine_render_context.go similarity index 100% rename from engine/render_context.go rename to engine/engine_render_context.go diff --git a/engine/text.go b/engine/engine_text.go similarity index 100% rename from engine/text.go rename to engine/engine_text.go diff --git a/engine/util.go b/engine/engine_util.go similarity index 90% rename from engine/util.go rename to engine/engine_util.go index eca0461..40f704c 100644 --- a/engine/util.go +++ b/engine/engine_util.go @@ -114,6 +114,14 @@ func (s Size) Contains(x, y int) bool { return 0 <= x && x < s.width && 0 <= y && y < s.height } +func LimitAdd(original, amount, limit int) int { + if original+amount > limit { + return limit + } + + return original + amount +} + func LimitIncrement(i int, limit int) int { if (i + 1) > limit { return i @@ -122,6 +130,14 @@ func LimitIncrement(i int, limit int) int { return i + 1 } +func LimitSubtract(original, amount, limit int) int { + if original-amount < limit { + return limit + } + + return original - amount +} + func LimitDecrement(i int, limit int) int { if (i - 1) < limit { return i diff --git a/engine/viewport.go b/engine/engine_viewport.go similarity index 100% rename from engine/viewport.go rename to engine/engine_viewport.go diff --git a/game/game.go b/game/game.go index e6cf33e..2a2bf63 100644 --- a/game/game.go +++ b/game/game.go @@ -2,16 +2,15 @@ package game import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" "mvvasilev/last_light/game/state" - "mvvasilev/last_light/game/turns" + "mvvasilev/last_light/game/systems" "github.com/gdamore/tcell/v2" ) type Game struct { - turnSystem *turns.TurnSystem - inputSystem *input.InputSystem + turnSystem *systems.TurnSystem + inputSystem *systems.InputSystem state state.GameState @@ -21,9 +20,9 @@ type Game struct { func CreateGame() *Game { game := new(Game) - game.turnSystem = turns.CreateTurnSystem() + game.turnSystem = systems.CreateTurnSystem() - game.inputSystem = input.CreateInputSystemWithDefaultBindings() + game.inputSystem = systems.CreateInputSystemWithDefaultBindings() game.state = state.CreateMainMenuState(game.turnSystem, game.inputSystem) diff --git a/game/item/inventory.go b/game/item/inventory.go deleted file mode 100644 index eb8ea54..0000000 --- a/game/item/inventory.go +++ /dev/null @@ -1,95 +0,0 @@ -package item - -import ( - "mvvasilev/last_light/engine" -) - -type Inventory interface { - Items() []Item - Shape() engine.Size - Push(item Item) bool - Drop(x, y int) Item - ItemAt(x, y int) Item -} - -type BasicInventory struct { - contents []Item - shape engine.Size -} - -func CreateInventory(shape engine.Size) *BasicInventory { - inv := new(BasicInventory) - - inv.contents = make([]Item, 0, shape.Height()*shape.Width()) - inv.shape = shape - - return inv -} - -func (i *BasicInventory) Items() (items []Item) { - return i.contents -} - -func (i *BasicInventory) Shape() engine.Size { - return i.shape -} - -func (inv *BasicInventory) Push(i Item) (success bool) { - if len(inv.contents) == inv.shape.Area() { - return false - } - - itemType := i.Type() - - // Try to first find a matching item with capacity - for index, existingItem := range inv.contents { - if existingItem != nil && existingItem.Type().Id() == itemType.Id() { - if existingItem.Quantity()+1 > existingItem.Type().MaxStack() { - continue - } - - it := CreateBasicItem(itemType, existingItem.Quantity()+1) - inv.contents[index] = &it - - return true - } - } - - // Next, try to find an intermediate empty slot to fit this item into - for index, existingItem := range inv.contents { - if existingItem == nil { - inv.contents[index] = i - - return true - } - } - - // Finally, just append the new item at the end - inv.contents = append(inv.contents, i) - - return true -} - -func (i *BasicInventory) Drop(x, y int) Item { - index := y*i.shape.Width() + x - - if index > len(i.contents)-1 { - return nil - } - - item := i.contents[index] - - i.contents[index] = nil - - return item -} - -func (i *BasicInventory) ItemAt(x, y int) (item Item) { - index := y*i.shape.Width() + x - - if index > len(i.contents)-1 { - return nil - } - - return i.contents[index] -} diff --git a/game/item/item.go b/game/item/item.go deleted file mode 100644 index 30ebf4f..0000000 --- a/game/item/item.go +++ /dev/null @@ -1,87 +0,0 @@ -package item - -import ( - "github.com/gdamore/tcell/v2" -) - -type Item interface { - Name() (string, tcell.Style) - Description() string - Type() ItemType - Quantity() int -} - -type BasicItem struct { - name string - nameStyle tcell.Style - description string - itemType ItemType - quantity int -} - -func EmptyItem() BasicItem { - return BasicItem{ - nameStyle: tcell.StyleDefault, - itemType: &BasicItemType{ - name: "", - description: "", - tileIcon: ' ', - itemIcon: " ", - style: tcell.StyleDefault, - maxStack: 0, - }, - } -} - -func CreateBasicItem(itemType ItemType, quantity int) BasicItem { - return BasicItem{ - itemType: itemType, - quantity: quantity, - } -} - -func CreateBasicItemWithName(name string, style tcell.Style, itemType ItemType, quantity int) BasicItem { - return BasicItem{ - name: name, - nameStyle: style, - itemType: itemType, - quantity: quantity, - } -} - -func (i BasicItem) WithName(name string, style tcell.Style) BasicItem { - i.name = name - i.nameStyle = style - - return i -} - -func (i BasicItem) Name() (string, tcell.Style) { - if i.name == "" { - return i.itemType.Name(), i.nameStyle - } - - return i.name, i.nameStyle -} - -func (i BasicItem) Description() string { - if i.description == "" { - return i.itemType.Description() - } - - return i.description -} - -func (i BasicItem) WithDescription(description string) BasicItem { - i.description = description - - return i -} - -func (i BasicItem) Type() ItemType { - return i.itemType -} - -func (i BasicItem) Quantity() int { - return i.quantity -} diff --git a/game/item/item_type.go b/game/item/item_type.go deleted file mode 100644 index 014c98a..0000000 --- a/game/item/item_type.go +++ /dev/null @@ -1,133 +0,0 @@ -package item - -import ( - "github.com/gdamore/tcell/v2" -) - -type ItemType interface { - Id() int - Name() string - Description() string - TileIcon() rune - Icon() string - Style() tcell.Style - MaxStack() int - EquippableSlot() EquippedSlot -} - -type BasicItemType struct { - id int - name string - description string - tileIcon rune - itemIcon string - maxStack int - equippableSlot EquippedSlot - - style tcell.Style -} - -func CreateBasicItemType( - id int, - name, description string, - tileIcon rune, - icon string, - maxStack int, - equippableSlot EquippedSlot, - style tcell.Style, -) *BasicItemType { - return &BasicItemType{ - id: id, - name: name, - description: description, - tileIcon: tileIcon, - itemIcon: icon, - style: style, - maxStack: maxStack, - equippableSlot: equippableSlot, - } -} - -func (it *BasicItemType) Id() int { - return it.id -} - -func (it *BasicItemType) Name() string { - return it.name -} - -func (it *BasicItemType) Description() string { - return it.description -} - -func (it *BasicItemType) TileIcon() rune { - return it.tileIcon -} - -func (it *BasicItemType) Icon() string { - return it.itemIcon -} - -func (it *BasicItemType) Style() tcell.Style { - return it.style -} - -func (it *BasicItemType) MaxStack() int { - return it.maxStack -} - -func (it *BasicItemType) EquippableSlot() EquippedSlot { - return it.equippableSlot -} - -func ItemTypeFish() ItemType { - return &BasicItemType{ - id: 0, - name: "Fish", - description: "What's a fish doing down here?", - tileIcon: '>', - itemIcon: "»o>", - style: tcell.StyleDefault.Foreground(tcell.ColorDarkCyan), - equippableSlot: EquippedSlotNone, - maxStack: 16, - } -} - -func ItemTypeGold() ItemType { - return &BasicItemType{ - id: 1, - name: "Gold", - description: "Not all those who wander are lost", - tileIcon: '¤', - itemIcon: " ¤ ", - equippableSlot: EquippedSlotNone, - style: tcell.StyleDefault.Foreground(tcell.ColorGoldenrod), - maxStack: 255, - } -} - -func ItemTypeArrow() ItemType { - return &BasicItemType{ - id: 2, - name: "Arrow", - description: "Ammunition for a bow", - tileIcon: '-', - itemIcon: "»->", - equippableSlot: EquippedSlotNone, - style: tcell.StyleDefault.Foreground(tcell.ColorGoldenrod), - maxStack: 32, - } -} - -func ItemTypeKey() ItemType { - return &BasicItemType{ - id: 3, - name: "Key", - description: "Indispensable for unlocking things", - tileIcon: '¬', - itemIcon: " o╖", - equippableSlot: EquippedSlotNone, - style: tcell.StyleDefault.Foreground(tcell.ColorDarkGoldenrod), - maxStack: 1, - } -} diff --git a/game/model/entity.go b/game/model/entity.go new file mode 100644 index 0000000..4575909 --- /dev/null +++ b/game/model/entity.go @@ -0,0 +1,226 @@ +package model + +import ( + "mvvasilev/last_light/engine" + + "github.com/gdamore/tcell/v2" + "github.com/google/uuid" +) + +type Direction int + +const ( + DirectionNone Direction = iota + North + South + West + East +) + +func DirectionName(dir Direction) string { + switch dir { + case North: + return "North" + case South: + return "South" + case West: + return "West" + case East: + return "East" + default: + return "Unknown" + } +} + +func MovementDirectionOffset(dir Direction) (int, int) { + switch dir { + case North: + return 0, -1 + case South: + return 0, 1 + case West: + return -1, 0 + case East: + return 1, 0 + } + + return 0, 0 +} + +// type Entity interface { +// UniqueId() uuid.UUID +// Presentation() (rune, tcell.Style) +// } + +// type MovableEntity interface { +// Position() engine.Position +// MoveTo(newPosition engine.Position) + +// Entity +// } + +// type EquippedEntity interface { +// Inventory() *EquippedInventory + +// Entity +// } + +type Entity_NamedComponent struct { + Name string +} + +type Entity_DescribedComponent struct { + Description string +} + +type Entity_PresentableComponent struct { + Rune rune + Style tcell.Style +} + +type Entity_PositionedComponent struct { + Position engine.Position +} + +type Entity_EquippedComponent struct { + Inventory *EquippedInventory +} + +type Entity_StatsHolderComponent struct { + BaseStats map[Stat]int + // StatModifiers []StatModifier +} + +type Entity_HealthComponent struct { + Health int + MaxHealth int + IsDead bool +} + +type Entity_V2 interface { + UniqueId() uuid.UUID + + Named() *Entity_NamedComponent + Described() *Entity_DescribedComponent + Presentable() *Entity_PresentableComponent + Positioned() *Entity_PositionedComponent + Equipped() *Entity_EquippedComponent + Stats() *Entity_StatsHolderComponent + HealthData() *Entity_HealthComponent +} + +type BaseEntity_V2 struct { + id uuid.UUID + + named *Entity_NamedComponent + described *Entity_DescribedComponent + presentable *Entity_PresentableComponent + positioned *Entity_PositionedComponent + equipped *Entity_EquippedComponent + stats *Entity_StatsHolderComponent + damageable *Entity_HealthComponent +} + +func (be *BaseEntity_V2) UniqueId() uuid.UUID { + return be.id +} + +func (be *BaseEntity_V2) Named() *Entity_NamedComponent { + return be.named +} + +func (be *BaseEntity_V2) Described() *Entity_DescribedComponent { + return be.described +} + +func (be *BaseEntity_V2) Presentable() *Entity_PresentableComponent { + return be.presentable +} + +func (be *BaseEntity_V2) Positioned() *Entity_PositionedComponent { + return be.positioned +} + +func (be *BaseEntity_V2) Equipped() *Entity_EquippedComponent { + return be.equipped +} + +func (be *BaseEntity_V2) Stats() *Entity_StatsHolderComponent { + return be.stats +} + +func (be *BaseEntity_V2) HealthData() *Entity_HealthComponent { + return be.damageable +} + +func CreateEntity(components ...func(*BaseEntity_V2)) *BaseEntity_V2 { + e := &BaseEntity_V2{ + id: uuid.New(), + } + + for _, comp := range components { + comp(e) + } + + return e +} + +func WithName(name string) func(*BaseEntity_V2) { + return func(e *BaseEntity_V2) { + e.named = &Entity_NamedComponent{ + Name: name, + } + } +} + +func WithDescription(description string) func(e *BaseEntity_V2) { + return func(e *BaseEntity_V2) { + e.described = &Entity_DescribedComponent{ + Description: description, + } + } +} + +func WithPresentation(symbol rune, style tcell.Style) func(e *BaseEntity_V2) { + return func(e *BaseEntity_V2) { + e.presentable = &Entity_PresentableComponent{ + Rune: symbol, + Style: style, + } + } +} + +func WithPosition(pos engine.Position) func(e *BaseEntity_V2) { + return func(e *BaseEntity_V2) { + e.positioned = &Entity_PositionedComponent{ + Position: pos, + } + } +} + +func WithInventory(inv *EquippedInventory) func(e *BaseEntity_V2) { + return func(e *BaseEntity_V2) { + e.equipped = &Entity_EquippedComponent{ + Inventory: inv, + } + } +} + +func WithStats(baseStats map[Stat]int, statModifiers ...StatModifier) func(e *BaseEntity_V2) { + return func(e *BaseEntity_V2) { + e.stats = &Entity_StatsHolderComponent{ + BaseStats: baseStats, + // StatModifiers: statModifiers, + } + } +} + +func WithHealthData(health, maxHealth int, isDead bool) func(e *BaseEntity_V2) { + return func(e *BaseEntity_V2) { + e.damageable = &Entity_HealthComponent{ + Health: health, + MaxHealth: maxHealth, + IsDead: isDead, + } + } +} diff --git a/game/model/entity_npc.go b/game/model/entity_npc.go new file mode 100644 index 0000000..8b53790 --- /dev/null +++ b/game/model/entity_npc.go @@ -0,0 +1 @@ +package model diff --git a/game/model/entity_player.go b/game/model/entity_player.go new file mode 100644 index 0000000..b3eb6d9 --- /dev/null +++ b/game/model/entity_player.go @@ -0,0 +1,49 @@ +package model + +import ( + "mvvasilev/last_light/engine" + + "github.com/gdamore/tcell/v2" +) + +type Player_V2 struct { + Entity_V2 +} + +func CreatePlayer_V2(x, y int, playerBaseStats map[Stat]int) *Player_V2 { + p := &Player_V2{ + Entity_V2: CreateEntity( + WithName("Player"), + WithPosition(engine.PositionAt(x, y)), + WithPresentation('@', tcell.StyleDefault), + WithInventory(CreateEquippedInventory()), + WithStats(playerBaseStats), + WithHealthData(0, 0, false), + ), + } + + p.HealthData().MaxHealth = BaseMaxHealth(p) + p.HealthData().Health = p.HealthData().MaxHealth + + return p +} + +func (p *Player_V2) Inventory() *EquippedInventory { + return p.Entity_V2.Equipped().Inventory +} + +func (p *Player_V2) Position() engine.Position { + return p.Entity_V2.Positioned().Position +} + +func (p *Player_V2) Presentation() (rune, tcell.Style) { + return p.Presentable().Rune, p.Presentable().Style +} + +func (p *Player_V2) Stats() *Entity_StatsHolderComponent { + return p.Entity_V2.Stats() +} + +func (p *Player_V2) HealthData() *Entity_HealthComponent { + return p.Entity_V2.HealthData() +} diff --git a/game/model/entity_rpg_npc.go b/game/model/entity_rpg_npc.go new file mode 100644 index 0000000..38d72a8 --- /dev/null +++ b/game/model/entity_rpg_npc.go @@ -0,0 +1,57 @@ +package model + +// import ( +// "mvvasilev/last_light/engine" +// "mvvasilev/last_light/game/item" +// "mvvasilev/last_light/game/rpg" + +// "github.com/gdamore/tcell/v2" +// ) + +// type RPGNPC interface { +// NPC +// rpg.RPGEntity +// EquippedEntity +// } + +// type BasicRPGNPC struct { +// inventory *item.EquippedInventory + +// *BasicNPC +// *rpg.BasicRPGEntity +// } + +// func CreateRPGNPC(x, y int, name string, representation rune, style tcell.Style, stats map[rpg.Stat]int) *BasicRPGNPC { +// rpgnpc := &BasicRPGNPC{ +// inventory: item.CreateEquippedInventory(), +// BasicNPC: CreateNPC( +// engine.PositionAt(x, y), +// name, +// representation, +// style, +// ), +// BasicRPGEntity: rpg.CreateBasicRPGEntity( +// stats, +// map[rpg.Stat][]rpg.StatModifier{}, +// ), +// } + +// rpgnpc.Heal(rpg.BaseMaxHealth(rpgnpc)) + +// return rpgnpc +// } + +// func (rnpc *BasicRPGNPC) Inventory() *item.EquippedInventory { +// return rnpc.inventory +// } + +// func (p *BasicRPGNPC) CalculateAttack(other rpg.RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType rpg.DamageType) { +// mainHand := p.inventory.AtSlot(item.EquippedSlotDominantHand) + +// switch mh := mainHand.(type) { +// case rpg.RPGItem: +// return rpg.PhysicalWeaponAttack(p, mh, other) +// default: +// return rpg.UnarmedAttack(p, other) +// } +// } diff --git a/game/model/inventory.go b/game/model/inventory.go new file mode 100644 index 0000000..1b73184 --- /dev/null +++ b/game/model/inventory.go @@ -0,0 +1,136 @@ +package model + +import ( + "mvvasilev/last_light/engine" +) + +type Inventory interface { + Items() []Item_V2 + Shape() engine.Size + Push(item Item_V2) bool + Drop(x, y int) Item_V2 + ItemAt(x, y int) Item_V2 +} + +type BasicInventory struct { + contents []Item_V2 + shape engine.Size +} + +func CreateInventory(shape engine.Size) *BasicInventory { + inv := new(BasicInventory) + + inv.contents = make([]Item_V2, 0, shape.Height()*shape.Width()) + inv.shape = shape + + return inv +} + +func (i *BasicInventory) Items() (items []Item_V2) { + return i.contents +} + +func (i *BasicInventory) Shape() engine.Size { + return i.shape +} + +func (inv *BasicInventory) Push(i Item_V2) (success bool) { + if len(inv.contents) == inv.shape.Area() { + return false + } + + itemType := i.Type() + + // Try to first find a matching item with capacity + for index, existingItem := range inv.contents { + if existingItem != nil && existingItem.Type() == itemType && existingItem.Quantifiable() != nil && i.Quantifiable() != nil { + // Cannot add even a single more item to this stack, skip it + if existingItem.Quantifiable().CurrentQuantity+1 > existingItem.Quantifiable().MaxQuantity { + continue + } + + // Item has capacity, but is less than total new item stack. Split between existing, and a new stack. + if existingItem.Quantifiable().CurrentQuantity+i.Quantifiable().CurrentQuantity > existingItem.Quantifiable().MaxQuantity { + // get difference in quantities + diff := existingItem.Quantifiable().MaxQuantity - existingItem.Quantifiable().CurrentQuantity + + // set existing item quantity to max + existingItem.Quantifiable().CurrentQuantity = existingItem.Quantifiable().MaxQuantity + + // set new item quantity to its current - diff + i.Quantifiable().CurrentQuantity -= i.Quantifiable().CurrentQuantity - diff + + // Cannot pick up item, doing so would overflow the inventory + if index+1 >= inv.shape.Area() { + return false + } + + inv.contents[index+1] = i + + return true + } + + inv.contents[index] = i + + return true + } + } + + // Next, try to find an intermediate empty slot to fit this item into + for index, existingItem := range inv.contents { + if existingItem == nil { + inv.contents[index] = i + + return true + } + } + + // Finally, just append the new item at the end + inv.contents = append(inv.contents, i) + + return true +} + +func (i *BasicInventory) Drop(x, y int) Item_V2 { + index := y*i.shape.Width() + x + + if index > len(i.contents)-1 { + return nil + } + + item := i.contents[index] + + i.contents[index] = nil + + return item +} + +func (i *BasicInventory) ReduceQuantityAt(x, y int, amount int) { + it := i.ItemAt(x, y) + + if it == nil { + return + } + + quantityData := it.Quantifiable() + + if quantityData != nil { + if quantityData.CurrentQuantity-amount <= 0 { + i.Drop(x, y) + } else { + quantityData.CurrentQuantity = quantityData.CurrentQuantity - amount + } + } else { + i.Drop(x, y) + } +} + +func (i *BasicInventory) ItemAt(x, y int) (item Item_V2) { + index := y*i.shape.Width() + x + + if index > len(i.contents)-1 { + return nil + } + + return i.contents[index] +} diff --git a/game/item/equipped_inventory.go b/game/model/inventory_equipped.go similarity index 81% rename from game/item/equipped_inventory.go rename to game/model/inventory_equipped.go index 673707e..1fce769 100644 --- a/game/item/equipped_inventory.go +++ b/game/model/inventory_equipped.go @@ -1,4 +1,4 @@ -package item +package model import ( "mvvasilev/last_light/engine" @@ -18,13 +18,13 @@ const ( ) type EquippedInventory struct { - offHand Item - dominantHand Item + offHand Item_V2 + dominantHand Item_V2 - head Item - chestplate Item - leggings Item - shoes Item + head Item_V2 + chestplate Item_V2 + leggings Item_V2 + shoes Item_V2 *BasicInventory } @@ -35,7 +35,7 @@ func CreateEquippedInventory() *EquippedInventory { } } -func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item { +func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item_V2 { switch slot { case EquippedSlotOffhand: return ei.offHand @@ -54,7 +54,7 @@ func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item { } } -func (ei *EquippedInventory) Equip(item Item, slot EquippedSlot) Item { +func (ei *EquippedInventory) Equip(item Item_V2, slot EquippedSlot) Item_V2 { switch slot { case EquippedSlotOffhand: ei.offHand = item diff --git a/game/model/item.go b/game/model/item.go new file mode 100644 index 0000000..2f77b3b --- /dev/null +++ b/game/model/item.go @@ -0,0 +1,308 @@ +package model + +import ( + "mvvasilev/last_light/engine" + + "github.com/gdamore/tcell/v2" +) + +type ItemMetaType int + +const ( + MetaItemType_Physical_Weapon ItemMetaType = iota + MetaItemType_Magic_Weapon + MetaItemType_Weapon + MetaItemType_Physical_Armour + MetaItemType_Magic_Armour + MetaItemType_Armour + MetaItemType_Consumable + MetaItemType_Potion +) + +type Item_V2 interface { + TileIcon() rune + Icon() string + Style() tcell.Style + Type() 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 +} + +type Item_QuantifiableComponent struct { + MaxQuantity int + CurrentQuantity int +} + +type Item_UsableComponent struct { + IsUsableBy func(Entity_V2) bool + Use func(*engine.GameEventLog, *Dungeon, Entity_V2) +} + +type Item_EquippableComponent struct { + Slot EquippedSlot +} + +type Item_NamedComponent struct { + Name string + Style tcell.Style +} + +type Item_DescribedComponent struct { + Description string + Style tcell.Style +} + +type Item_DamagingComponent struct { + DamageRoll func() (damage int, dmgType DamageType) +} + +type Item_StatModifierComponent struct { + StatModifiers []StatModifier +} + +type Item_MetaTypesComponent struct { + Types []ItemMetaType +} + +type BaseItem_V2 struct { + tileIcon rune + icon string + 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 +} + +func (i *BaseItem_V2) TileIcon() rune { + return i.tileIcon +} + +func (i *BaseItem_V2) Icon() string { + return i.icon +} + +func (i *BaseItem_V2) Style() tcell.Style { + return i.style +} + +func (i *BaseItem_V2) Type() ItemType { + return i.itemType +} + +func (i *BaseItem_V2) Quantifiable() *Item_QuantifiableComponent { + return i.quantifiable +} + +func (i *BaseItem_V2) Usable() *Item_UsableComponent { + return i.usable +} + +func (i *BaseItem_V2) Equippable() *Item_EquippableComponent { + return i.equippable +} + +func (i *BaseItem_V2) Named() *Item_NamedComponent { + return i.named +} + +func (i *BaseItem_V2) Described() *Item_DescribedComponent { + return i.described +} + +func (i *BaseItem_V2) Damaging() *Item_DamagingComponent { + return i.damaging +} + +func (i *BaseItem_V2) StatModifier() *Item_StatModifierComponent { + return i.statModifier +} + +func (i *BaseItem_V2) MetaTypes() *Item_MetaTypesComponent { + return i.metaTypes +} + +func createBaseItem(itemType ItemType, tileIcon rune, icon string, style tcell.Style, components ...func(*BaseItem_V2)) *BaseItem_V2 { + i := &BaseItem_V2{ + itemType: itemType, + tileIcon: tileIcon, + icon: icon, + style: style, + } + + for _, comp := range components { + comp(i) + } + + return i +} + +func item_WithQuantity(quantity, maxQuantity int) func(*BaseItem_V2) { + return func(bi *BaseItem_V2) { + bi.quantifiable = &Item_QuantifiableComponent{ + CurrentQuantity: quantity, + MaxQuantity: maxQuantity, + } + } +} + +func item_WithUsable(usabilityCheck func(Entity_V2) bool, useFunc func(*engine.GameEventLog, *Dungeon, Entity_V2)) func(*BaseItem_V2) { + return func(bi *BaseItem_V2) { + bi.usable = &Item_UsableComponent{ + IsUsableBy: usabilityCheck, + Use: useFunc, + } + } +} + +func item_WithEquippable(slot EquippedSlot) func(*BaseItem_V2) { + return func(bi *BaseItem_V2) { + bi.equippable = &Item_EquippableComponent{ + Slot: slot, + } + } +} + +func item_WithDamaging(damageFunc func() (damage int, dmgType DamageType)) func(*BaseItem_V2) { + return func(bi *BaseItem_V2) { + bi.damaging = &Item_DamagingComponent{ + DamageRoll: damageFunc, + } + } +} + +func item_WithName(name string, style tcell.Style) func(*BaseItem_V2) { + return func(bi *BaseItem_V2) { + bi.named = &Item_NamedComponent{ + Name: name, + Style: style, + } + } +} + +func item_WithDescription(description string, style tcell.Style) func(*BaseItem_V2) { + return func(bi *BaseItem_V2) { + bi.described = &Item_DescribedComponent{ + Description: description, + Style: style, + } + } +} + +func item_WithStatModifiers(statModifiers []StatModifier) func(*BaseItem_V2) { + return func(bi *BaseItem_V2) { + bi.statModifier = &Item_StatModifierComponent{ + StatModifiers: statModifiers, + } + } +} + +func item_WithMetaTypes(metaTypes []ItemMetaType) func(*BaseItem_V2) { + return func(bi *BaseItem_V2) { + bi.metaTypes = &Item_MetaTypesComponent{ + Types: metaTypes, + } + } +} + +// type Item interface { +// Name() (string, tcell.Style) +// Description() string +// Type() ItemType +// Quantity() int +// SetQuantity(quant int) Item +// } + +// type BasicItem struct { +// name string +// nameStyle tcell.Style +// description string +// itemType ItemType +// quantity int +// } + +// func EmptyItem() BasicItem { +// return BasicItem{ +// nameStyle: tcell.StyleDefault, +// itemType: &BasicItemType{ +// name: "", +// description: "", +// tileIcon: ' ', +// itemIcon: " ", +// style: tcell.StyleDefault, +// maxStack: 0, +// }, +// } +// } + +// func CreateBasicItem(itemType ItemType, quantity int) BasicItem { +// return BasicItem{ +// itemType: itemType, +// quantity: quantity, +// } +// } + +// func CreateBasicItemWithName(name string, style tcell.Style, itemType ItemType, quantity int) BasicItem { +// return BasicItem{ +// name: name, +// nameStyle: style, +// itemType: itemType, +// quantity: quantity, +// } +// } + +// func (i BasicItem) WithName(name string, style tcell.Style) BasicItem { +// i.name = name +// i.nameStyle = style + +// return i +// } + +// func (i BasicItem) Name() (string, tcell.Style) { +// if i.name == "" { +// return i.itemType.Name(), i.nameStyle +// } + +// return i.name, i.nameStyle +// } + +// func (i BasicItem) Description() string { +// if i.description == "" { +// return i.itemType.Description() +// } + +// return i.description +// } + +// func (i BasicItem) WithDescription(description string) BasicItem { +// i.description = description + +// return i +// } + +// func (i BasicItem) Type() ItemType { +// return i.itemType +// } + +// func (i BasicItem) Quantity() int { +// return i.quantity +// } + +// func (i BasicItem) SetQuantity(amount int) Item { +// i.quantity = i.quantity - amount + +// return i +// } diff --git a/game/model/items.go b/game/model/items.go new file mode 100644 index 0000000..c2001e4 --- /dev/null +++ b/game/model/items.go @@ -0,0 +1,486 @@ +package model + +import ( + "fmt" + "mvvasilev/last_light/engine" + + "github.com/gdamore/tcell/v2" +) + +type ItemType int + +const ( + // Consumables + ItemType_Fish ItemType = iota + ItemType_SmallHealthPotion + ItemType_HealthPotion + ItemType_LargeHealthPotion + + // Weapons + ItemType_Bow + ItemType_Longsword + ItemType_Club + ItemType_Dagger + ItemType_Handaxe + ItemType_Javelin + ItemType_LightHammer + ItemType_Mace + ItemType_Sickle + ItemType_Spear + ItemType_Quarterstaff + + // Armour + + // Special +) + +func Item_Fish() Item_V2 { + return createBaseItem( + ItemType_Fish, + '>', + "»o>", + tcell.StyleDefault, + item_WithQuantity(1, 32), + item_WithName("Fish", tcell.StyleDefault), + item_WithDescription("On use heals for 1d4", tcell.StyleDefault), + item_WithUsable( + func(e Entity_V2) bool { + return e.HealthData() != nil + }, + func(log *engine.GameEventLog, d *Dungeon, e Entity_V2) { + damageable := e.HealthData() + + if damageable != nil { + healAmt := RollD4(1) + damageable.Health = engine.LimitAdd(damageable.Health, healAmt, damageable.MaxHealth) + + name := "Entity" + + if e.Named() != nil { + name = e.Named().Name + } + + log.Log(fmt.Sprintf("%s heals for %d HP", name, healAmt)) + } + }, + ), + ) +} + +func Item_SmallHealthPotion() Item_V2 { + return createBaseItem( + ItemType_SmallHealthPotion, + 'ó', + " Ô ", + tcell.StyleDefault.Foreground(tcell.ColorRed), + item_WithQuantity(1, 3), + item_WithName("Small Health Potion", tcell.StyleDefault), + item_WithDescription("On use heals for 2d6", tcell.StyleDefault), + item_WithUsable( + func(e Entity_V2) bool { + return e.HealthData() != nil + }, + func(log *engine.GameEventLog, d *Dungeon, e Entity_V2) { + damageable := e.HealthData() + + if damageable != nil { + healAmt := RollD6(2) + damageable.Health = engine.LimitAdd(damageable.Health, healAmt, damageable.MaxHealth) + + name := "Entity" + + if e.Named() != nil { + name = e.Named().Name + } + + log.Log(fmt.Sprintf("%s heals for %d HP", name, healAmt)) + } + }, + ), + ) +} + +func Item_HealthPotion() Item_V2 { + return createBaseItem( + ItemType_HealthPotion, + 'ó', + " Ô ", + tcell.StyleDefault.Foreground(tcell.ColorRed), + item_WithQuantity(1, 2), + item_WithName("Health Potion", tcell.StyleDefault), + item_WithDescription("On use heals for 3d6", tcell.StyleDefault), + item_WithUsable( + func(e Entity_V2) bool { + return e.HealthData() != nil + }, + func(log *engine.GameEventLog, d *Dungeon, e Entity_V2) { + damageable := e.HealthData() + + if damageable != nil { + healAmt := RollD6(3) + damageable.Health = engine.LimitAdd(damageable.Health, healAmt, damageable.MaxHealth) + + name := "Entity" + + if e.Named() != nil { + name = e.Named().Name + } + + log.Log(fmt.Sprintf("%s heals for %d HP", name, healAmt)) + } + }, + ), + ) +} + +func Item_LargeHealthPotion() Item_V2 { + return createBaseItem( + ItemType_LargeHealthPotion, + 'ó', + " Ô ", + tcell.StyleDefault.Foreground(tcell.ColorRed), + item_WithQuantity(1, 1), + item_WithName("Large Health Potion", tcell.StyleDefault), + item_WithDescription("On use heals for 4d6", tcell.StyleDefault), + item_WithUsable( + func(e Entity_V2) bool { + return e.HealthData() != nil + }, + func(log *engine.GameEventLog, d *Dungeon, e Entity_V2) { + damageable := e.HealthData() + + if damageable != nil { + healAmt := RollD6(4) + damageable.Health = engine.LimitAdd(damageable.Health, healAmt, damageable.MaxHealth) + + name := "Entity" + + if e.Named() != nil { + name = e.Named().Name + } + + log.Log(fmt.Sprintf("%s heals for %d HP", name, healAmt)) + } + }, + ), + ) +} + +func Item_Bow() Item_V2 { + return createBaseItem( + ItemType_Bow, + ')', + " |)", + tcell.StyleDefault.Foreground(tcell.ColorBrown), + item_WithQuantity(1, 1), + item_WithName("Bow", tcell.StyleDefault), + item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD8(1), DamageType_Physical_Piercing + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_Longsword() Item_V2 { + return createBaseItem( + ItemType_Longsword, + '/', + "╪══", + tcell.StyleDefault.Foreground(tcell.ColorSilver), + item_WithQuantity(1, 1), + item_WithName("Longsword", tcell.StyleDefault), + item_WithDescription("Deals 1d8 Slashing damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD8(1), DamageType_Physical_Slashing + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_Club() Item_V2 { + return createBaseItem( + ItemType_Club, + '!', + "-══", + tcell.StyleDefault.Foreground(tcell.ColorSaddleBrown), + item_WithQuantity(1, 1), + item_WithName("Club", tcell.StyleDefault), + item_WithDescription("Deals 1d8 Bludgeoning damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD8(1), DamageType_Physical_Bludgeoning + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_Dagger() Item_V2 { + return createBaseItem( + ItemType_Dagger, + '-', + " +─", + tcell.StyleDefault.Foreground(tcell.ColorSilver), + item_WithQuantity(1, 1), + item_WithName("Dagger", tcell.StyleDefault), + item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD6(1), DamageType_Physical_Piercing + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_Handaxe() Item_V2 { + return createBaseItem( + ItemType_Handaxe, + '¶', + " ─╗", + tcell.StyleDefault.Foreground(tcell.ColorSilver), + item_WithQuantity(1, 1), + item_WithName("Dagger", tcell.StyleDefault), + item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD6(1), DamageType_Physical_Piercing + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_Javelin() Item_V2 { + return createBaseItem( + ItemType_Javelin, + 'Î', + " ─>", + tcell.StyleDefault.Foreground(tcell.ColorSilver), + item_WithQuantity(1, 1), + item_WithName("Javelin", tcell.StyleDefault), + item_WithDescription("Deals 1d6 Piercing damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD6(1), DamageType_Physical_Piercing + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_LightHammer() Item_V2 { + return createBaseItem( + ItemType_LightHammer, + 'i', + " ─0", + tcell.StyleDefault.Foreground(tcell.ColorSilver), + item_WithQuantity(1, 1), + item_WithName("Light Hammer", tcell.StyleDefault), + item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD6(1), DamageType_Physical_Bludgeoning + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_Mace() Item_V2 { + return createBaseItem( + ItemType_Mace, + 'i', + " ─¤", + tcell.StyleDefault.Foreground(tcell.ColorSilver), + item_WithQuantity(1, 1), + item_WithName("Mace", tcell.StyleDefault), + item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD6(1), DamageType_Physical_Bludgeoning + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_Quarterstaff() Item_V2 { + return createBaseItem( + ItemType_Quarterstaff, + '|', + "───", + tcell.StyleDefault.Foreground(tcell.ColorSilver), + item_WithQuantity(1, 1), + item_WithName("Quarterstaff", tcell.StyleDefault), + item_WithDescription("Deals 1d6 Bludgeoning damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD6(1), DamageType_Physical_Bludgeoning + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_Sickle() Item_V2 { + return createBaseItem( + ItemType_Sickle, + '?', + " ─U", + tcell.StyleDefault.Foreground(tcell.ColorSilver), + item_WithQuantity(1, 1), + item_WithName("Sickle", tcell.StyleDefault), + item_WithDescription("Deals 1d6 Slashing damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD6(1), DamageType_Physical_Slashing + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +func Item_Spear() Item_V2 { + return createBaseItem( + ItemType_Spear, + 'Î', + "──>", + tcell.StyleDefault.Foreground(tcell.ColorSilver), + item_WithQuantity(1, 1), + item_WithName("Spear", tcell.StyleDefault), + item_WithDescription("Deals 1d8 Piercing damage", tcell.StyleDefault), + item_WithDamaging(func() (damage int, dmgType DamageType) { + return RollD8(1), DamageType_Physical_Piercing + }), + item_WithMetaTypes([]ItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}), + item_WithEquippable(EquippedSlotDominantHand), + ) +} + +// import ( +// "github.com/gdamore/tcell/v2" +// ) + +// type ItemType interface { +// Id() int +// Name() string +// Description() string +// TileIcon() rune +// Icon() string +// Style() tcell.Style +// MaxStack() int +// EquippableSlot() EquippedSlot +// } + +// type BasicItemType struct { +// id int +// name string +// description string +// tileIcon rune +// itemIcon string +// maxStack int +// equippableSlot EquippedSlot + +// style tcell.Style +// } + +// func CreateBasicItemType( +// id int, +// name, description string, +// tileIcon rune, +// icon string, +// maxStack int, +// equippableSlot EquippedSlot, +// style tcell.Style, +// ) *BasicItemType { +// return &BasicItemType{ +// id: id, +// name: name, +// description: description, +// tileIcon: tileIcon, +// itemIcon: icon, +// style: style, +// maxStack: maxStack, +// equippableSlot: equippableSlot, +// } +// } + +// func (it *BasicItemType) Id() int { +// return it.id +// } + +// func (it *BasicItemType) Name() string { +// return it.name +// } + +// func (it *BasicItemType) Description() string { +// return it.description +// } + +// func (it *BasicItemType) TileIcon() rune { +// return it.tileIcon +// } + +// func (it *BasicItemType) Icon() string { +// return it.itemIcon +// } + +// func (it *BasicItemType) Style() tcell.Style { +// return it.style +// } + +// func (it *BasicItemType) MaxStack() int { +// return it.maxStack +// } + +// func (it *BasicItemType) EquippableSlot() EquippedSlot { +// return it.equippableSlot +// } + +// func ItemTypeFish() ItemType { +// return &BasicItemType{ +// id: 0, +// name: "Fish", +// description: "What's a fish doing down here?", +// tileIcon: '>', +// itemIcon: "»o>", +// style: tcell.StyleDefault.Foreground(tcell.ColorDarkCyan), +// equippableSlot: EquippedSlotNone, +// maxStack: 16, +// } +// } + +// func ItemTypeGold() ItemType { +// return &BasicItemType{ +// id: 1, +// name: "Gold", +// description: "Not all those who wander are lost", +// tileIcon: '¤', +// itemIcon: " ¤ ", +// equippableSlot: EquippedSlotNone, +// style: tcell.StyleDefault.Foreground(tcell.ColorGoldenrod), +// maxStack: 255, +// } +// } + +// func ItemTypeArrow() ItemType { +// return &BasicItemType{ +// id: 2, +// name: "Arrow", +// description: "Ammunition for a bow", +// tileIcon: '-', +// itemIcon: "»->", +// equippableSlot: EquippedSlotNone, +// style: tcell.StyleDefault.Foreground(tcell.ColorGoldenrod), +// maxStack: 32, +// } +// } + +// func ItemTypeKey() ItemType { +// return &BasicItemType{ +// id: 3, +// name: "Key", +// description: "Indispensable for unlocking things", +// tileIcon: '¬', +// itemIcon: " o╖", +// equippableSlot: EquippedSlotNone, +// style: tcell.StyleDefault.Foreground(tcell.ColorDarkGoldenrod), +// maxStack: 1, +// } +// } diff --git a/game/model/rpg_entity.go b/game/model/rpg_entity.go new file mode 100644 index 0000000..e0c2973 --- /dev/null +++ b/game/model/rpg_entity.go @@ -0,0 +1,116 @@ +package model + +// import "slices" + +// type RPGEntity interface { +// BaseStat(stat Stat) int +// SetBaseStat(stat Stat, value int) + +// CollectModifiersForStat(stat Stat) []StatModifier +// AddStatModifier(modifier StatModifier) +// RemoveStatModifier(id StatModifierId) + +// IsDead() bool +// CurrentHealth() int +// Heal(health int) +// Damage(damage int) + +// CalculateAttack(other RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) +// } + +// type BasicRPGEntity struct { +// stats map[Stat]int + +// statModifiers map[Stat][]StatModifier + +// currentHealth int +// } + +// func CreateBasicRPGEntity(baseStats map[Stat]int, statModifiers map[Stat][]StatModifier) *BasicRPGEntity { +// ent := &BasicRPGEntity{ +// stats: baseStats, +// statModifiers: statModifiers, +// } + +// ent.currentHealth = BaseMaxHealth(ent) + +// return ent +// } + +// func (brpg *BasicRPGEntity) BaseStat(stat Stat) int { +// return brpg.stats[stat] +// } + +// func (brpg *BasicRPGEntity) SetBaseStat(stat Stat, value int) { +// brpg.stats[stat] = value +// } + +// func (brpg *BasicRPGEntity) CollectModifiersForStat(stat Stat) []StatModifier { +// modifiers := brpg.statModifiers[stat] + +// if modifiers == nil { +// return []StatModifier{} +// } + +// return modifiers +// } + +// func (brpg *BasicRPGEntity) AddStatModifier(modifier StatModifier) { +// existing := brpg.statModifiers[modifier.Stat] + +// if existing == nil { +// existing = make([]StatModifier, 0) +// } + +// existing = append(existing, modifier) + +// brpg.statModifiers[modifier.Stat] = existing +// } + +// func (brpg *BasicRPGEntity) RemoveStatModifier(id StatModifierId) { +// for k, v := range brpg.statModifiers { +// for i, sm := range v { +// if sm.Id == id { +// brpg.statModifiers[k] = slices.Delete(v, i, i+1) +// } +// } +// } +// } + +// func (brpg *BasicRPGEntity) CurrentHealth() int { +// return brpg.currentHealth +// } + +// func (brpg *BasicRPGEntity) IsDead() bool { +// return brpg.CurrentHealth() <= 0 +// } + +// func (brpg *BasicRPGEntity) Heal(health int) { +// if brpg.IsDead() { +// return +// } + +// maxHealth := BaseMaxHealth(brpg) + +// if brpg.currentHealth+health > maxHealth { +// brpg.currentHealth = maxHealth + +// return +// } + +// brpg.currentHealth += health +// } + +// func (brpg *BasicRPGEntity) Damage(damage int) { +// if brpg.currentHealth-damage < 0 { +// brpg.currentHealth = 0 + +// return +// } + +// brpg.currentHealth -= damage +// } + +// func (brpg *BasicRPGEntity) CalculateAttack(other RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) { +// return UnarmedAttack(brpg, other) +// } diff --git a/game/rpg/generate_items.go b/game/model/rpg_generate_items.go similarity index 71% rename from game/rpg/generate_items.go rename to game/model/rpg_generate_items.go index 5c876d8..43c6249 100644 --- a/game/rpg/generate_items.go +++ b/game/model/rpg_generate_items.go @@ -1,9 +1,8 @@ -package rpg +package model import ( "math/rand" "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/item" "slices" "unicode" "unicode/utf8" @@ -12,11 +11,9 @@ import ( "github.com/google/uuid" ) -// TODO: figure out event logging. Need to inform the player of the things that are happening... - const MaxNumberOfModifiers = 6 -type ItemSupplier func() item.Item +type ItemSupplier func() Item_V2 type LootTable struct { table []ItemSupplier @@ -34,7 +31,7 @@ func (igt *LootTable) Add(weight int, createItemFunction ItemSupplier) { } } -func (igt *LootTable) Generate() item.Item { +func (igt *LootTable) Generate() Item_V2 { return igt.table[rand.Intn(len(igt.table))]() } @@ -118,25 +115,25 @@ func randomSuffix() string { return suffixes[rand.Intn(len(suffixes))] } -func generateItemName(itemType RPGItemType, rarity ItemRarity) (string, tcell.Style) { +func generateItemName(existingItemName string, rarity ItemRarity) (string, tcell.Style) { switch rarity { case ItemRarity_Common: - return itemType.Name(), tcell.StyleDefault + return existingItemName, tcell.StyleDefault case ItemRarity_Uncommon: - return randomAdjective() + " " + itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorLime) + return randomAdjective() + " " + existingItemName, tcell.StyleDefault.Foreground(tcell.ColorLime) case ItemRarity_Rare: - return itemType.Name() + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorBlue) + return existingItemName + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorBlue) case ItemRarity_Epic: - return randomAdjective() + " " + itemType.Name() + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorPurple) + return randomAdjective() + " " + existingItemName + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorPurple) case ItemRarity_Legendary: - return generateUniqueItemName() + ", Legendary " + itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorOrange).Attributes(tcell.AttrBold) + return generateUniqueItemName() + ", Legendary " + existingItemName, tcell.StyleDefault.Foreground(tcell.ColorOrange).Attributes(tcell.AttrBold) default: - return itemType.Name(), tcell.StyleDefault + return existingItemName, tcell.StyleDefault } } -func randomStat(metaItemTypes []RPGItemMetaType) Stat { - stats := make(map[RPGItemMetaType][]Stat, 0) +func randomStat(metaItemTypes []ItemMetaType) Stat { + stats := make(map[ItemMetaType][]Stat, 0) stats[MetaItemType_Weapon] = []Stat{ Stat_Attributes_Strength, @@ -183,6 +180,12 @@ func randomStat(metaItemTypes []RPGItemMetaType) Stat { Stat_DamageBonus_Magic_Thunder, Stat_DamageBonus_Magic_Acid, Stat_DamageBonus_Magic_Poison, + Stat_ResistanceBonus_Magic_Acid, + Stat_ResistanceBonus_Magic_Cold, + Stat_ResistanceBonus_Magic_Fire, + Stat_ResistanceBonus_Magic_Necrotic, + Stat_ResistanceBonus_Magic_Poison, + Stat_ResistanceBonus_Magic_Thunder, } stats[MetaItemType_Physical_Armour] = []Stat{ @@ -190,6 +193,10 @@ func randomStat(metaItemTypes []RPGItemMetaType) Stat { Stat_DamageBonus_Physical_Slashing, Stat_DamageBonus_Physical_Piercing, Stat_DamageBonus_Physical_Bludgeoning, + Stat_ResistanceBonus_Physical_Bludgeoning, + Stat_ResistanceBonus_Physical_Piercing, + Stat_ResistanceBonus_Physical_Slashing, + Stat_ResistanceBonus_Physical_Unarmed, } possibleStats := make([]Stat, 0, 10) @@ -201,7 +208,7 @@ func randomStat(metaItemTypes []RPGItemMetaType) Stat { return slices.Compact(possibleStats)[rand.Intn(len(stats))] } -func generateItemStatModifiers(itemType RPGItemType, rarity ItemRarity) []StatModifier { +func generateItemStatModifiers(itemMetaTypes []ItemMetaType, rarity ItemRarity) []StatModifier { points := pointPerRarity(rarity) modifiers := make(map[Stat]*StatModifier, 0) @@ -218,7 +225,7 @@ func generateItemStatModifiers(itemType RPGItemType, rarity ItemRarity) []StatMo continue } - stat := randomStat(itemType.MetaTypes()) + stat := randomStat(itemMetaTypes) existingForStat := modifiers[stat] @@ -257,14 +264,32 @@ func generateItemStatModifiers(itemType RPGItemType, rarity ItemRarity) []StatMo // Each rarity gets an amount of generation points, the higher the rarity, the more points // Each stat modifier consumes points. The higher the stat bonus, the more points it consumes. -func GenerateItemOfTypeAndRarity(itemType RPGItemType, rarity ItemRarity) RPGItem { - // points := pointPerRarity(rarity) - name, style := generateItemName(itemType, rarity) +func GenerateItemOfTypeAndRarity(prototype Item_V2, rarity ItemRarity) Item_V2 { + if prototype.Named() == nil { + return prototype + } - return CreateRPGItem( - name, - style, - itemType, - generateItemStatModifiers(itemType, rarity), + if prototype.MetaTypes() == nil { + return prototype + } + + existingName := prototype.Named().Name + metaTypes := prototype.MetaTypes().Types + + // points := pointPerRarity(rarity) + name, style := generateItemName(existingName, rarity) + statModifiers := generateItemStatModifiers(metaTypes, rarity) + + return createBaseItem( + prototype.Type(), + prototype.TileIcon(), + prototype.Icon(), + prototype.Style(), + item_WithName(name, style), + item_WithDescription(prototype.Described().Description, prototype.Described().Style), + item_WithDamaging(prototype.Damaging().DamageRoll), + item_WithEquippable(prototype.Equippable().Slot), + item_WithStatModifiers(statModifiers), + item_WithMetaTypes(metaTypes), ) } diff --git a/game/model/rpg_items.go b/game/model/rpg_items.go new file mode 100644 index 0000000..9f926f1 --- /dev/null +++ b/game/model/rpg_items.go @@ -0,0 +1,73 @@ +package model + +// type RPGItemType interface { +// RollDamage(victim, attacker RPGEntity) (damage int, dmgType DamageType) +// Use(eventLogger *engine.GameEventLog, user RPGEntity) +// MetaTypes() []ItemMetaType + +// item.ItemType +// } + +// type BasicRPGItemType struct { +// damageRollFunc func(victim, attacker RPGEntity) (damage int, dmgType DamageType) +// useFunc func(eventLogger *engine.GameEventLog, user RPGEntity) + +// metaTypes []ItemMetaType + +// *item.BasicItemType +// } + +// func (it *BasicRPGItemType) Use(eventLogger *engine.GameEventLog, user RPGEntity) { +// if it.useFunc == nil { +// return +// } + +// it.useFunc(eventLogger, user) +// } + +// func (it *BasicRPGItemType) RollDamage(victim, attacker RPGEntity) (damage int, dmgType DamageType) { +// if it.damageRollFunc == nil { +// return 0, DamageType_Physical_Unarmed +// } + +// return it.damageRollFunc(victim, attacker) +// } + +// func (it *BasicRPGItemType) MetaTypes() []ItemMetaType { +// return it.metaTypes +// } + +// type RPGItem interface { +// Modifiers() []StatModifier +// RPGType() RPGItemType + +// item.Item +// } + +// type BasicRPGItem struct { +// modifiers []StatModifier +// rpgType RPGItemType + +// item.BasicItem +// } + +// func (i *BasicRPGItem) Modifiers() []StatModifier { +// return i.modifiers +// } + +// func (i *BasicRPGItem) RPGType() RPGItemType { +// return i.rpgType +// } + +// func CreateRPGItem(name string, style tcell.Style, itemType RPGItemType, modifiers []StatModifier) RPGItem { +// return &BasicRPGItem{ +// modifiers: modifiers, +// rpgType: itemType, +// BasicItem: item.CreateBasicItemWithName( +// name, +// style, +// itemType, +// 1, +// ), +// } +// } diff --git a/game/rpg/rpg_system.go b/game/model/rpg_system.go similarity index 62% rename from game/rpg/rpg_system.go rename to game/model/rpg_system.go index 132af13..2dc078a 100644 --- a/game/rpg/rpg_system.go +++ b/game/model/rpg_system.go @@ -1,4 +1,4 @@ -package rpg +package model import ( "math/rand" @@ -34,6 +34,18 @@ const ( Stat_DamageBonus_Magic_Acid Stat = 120 Stat_DamageBonus_Magic_Poison Stat = 130 + Stat_ResistanceBonus_Physical_Unarmed Stat = 200 + Stat_ResistanceBonus_Physical_Slashing Stat = 210 + Stat_ResistanceBonus_Physical_Piercing Stat = 220 + Stat_ResistanceBonus_Physical_Bludgeoning Stat = 230 + + Stat_ResistanceBonus_Magic_Fire Stat = 240 + Stat_ResistanceBonus_Magic_Cold Stat = 250 + Stat_ResistanceBonus_Magic_Necrotic Stat = 260 + Stat_ResistanceBonus_Magic_Thunder Stat = 270 + Stat_ResistanceBonus_Magic_Acid Stat = 280 + Stat_ResistanceBonus_Magic_Poison Stat = 290 + Stat_MaxHealthBonus Stat = 140 ) @@ -214,52 +226,99 @@ func DamageTypeToBonusStat(dmgType DamageType) Stat { } } +func DamageTypeToResistanceStat(dmgType DamageType) Stat { + switch dmgType { + case DamageType_Physical_Unarmed: + return Stat_ResistanceBonus_Physical_Unarmed + case DamageType_Physical_Slashing: + return Stat_ResistanceBonus_Physical_Slashing + case DamageType_Physical_Piercing: + return Stat_ResistanceBonus_Physical_Piercing + case DamageType_Physical_Bludgeoning: + return Stat_ResistanceBonus_Physical_Bludgeoning + case DamageType_Magic_Fire: + return Stat_ResistanceBonus_Magic_Fire + case DamageType_Magic_Cold: + return Stat_ResistanceBonus_Magic_Cold + case DamageType_Magic_Necrotic: + return Stat_ResistanceBonus_Magic_Necrotic + case DamageType_Magic_Thunder: + return Stat_ResistanceBonus_Magic_Thunder + case DamageType_Magic_Acid: + return Stat_ResistanceBonus_Magic_Acid + case DamageType_Magic_Poison: + return Stat_ResistanceBonus_Magic_Poison + default: + return Stat_NonExtant + } +} + func LuckRoll() int { return RollD10(1) } -func TotalModifierForStat(entity RPGEntity, stat Stat) int { +func TotalModifierForStat(stats *Item_StatModifierComponent, stat Stat) int { agg := 0 - for _, m := range entity.CollectModifiersForStat(stat) { - agg += m.Bonus + for _, m := range stats.StatModifiers { + if m.Stat == stat { + agg += m.Bonus + } } return agg } -func StatValue(entity RPGEntity, stat Stat) int { - return entity.BaseStat(stat) + TotalModifierForStat(entity, stat) +func statValue(stats *Entity_StatsHolderComponent, stat Stat) int { + return stats.BaseStats[stat] } // Base Max Health is determined from constitution: // 5*Constitution + Max Health Bonus -func BaseMaxHealth(entity RPGEntity) int { - return 5*StatValue(entity, Stat_Attributes_Constitution) + StatValue(entity, Stat_MaxHealthBonus) +func BaseMaxHealth(entity Entity_V2) int { + stats := entity.Stats() + + if stats == nil { + return 0 + } + + return 5*statValue(stats, Stat_Attributes_Constitution) + statValue(stats, Stat_MaxHealthBonus) } // Dexterity + Evasion bonus + luck roll -func EvasionRoll(victim RPGEntity) int { - return StatValue(victim, Stat_Attributes_Dexterity) + StatValue(victim, Stat_EvasionBonus) + LuckRoll() +func EvasionRoll(victim Entity_V2) int { + if victim.Stats() == nil { + return 0 + } + + return statValue(victim.Stats(), Stat_Attributes_Dexterity) + statValue(victim.Stats(), Stat_EvasionBonus) + LuckRoll() } // Strength + Precision bonus ( melee + total ) + luck roll -func PhysicalPrecisionRoll(attacker RPGEntity) int { - return StatValue(attacker, Stat_Attributes_Strength) + StatValue(attacker, Stat_PhysicalPrecisionBonus) + StatValue(attacker, Stat_TotalPrecisionBonus) + LuckRoll() +func PhysicalPrecisionRoll(attacker Entity_V2) int { + if attacker.Stats() == nil { + return 0 + } + + return statValue(attacker.Stats(), Stat_Attributes_Strength) + statValue(attacker.Stats(), Stat_PhysicalPrecisionBonus) + statValue(attacker.Stats(), Stat_TotalPrecisionBonus) + LuckRoll() } // Intelligence + Precision bonus ( magic + total ) + luck roll -func MagicPrecisionRoll(attacker RPGEntity) int { - return StatValue(attacker, Stat_Attributes_Intelligence) + StatValue(attacker, Stat_MagicPrecisionBonus) + StatValue(attacker, Stat_TotalPrecisionBonus) + LuckRoll() +func MagicPrecisionRoll(attacker Entity_V2) int { + if attacker.Stats() == nil { + return 0 + } + + return statValue(attacker.Stats(), Stat_Attributes_Intelligence) + statValue(attacker.Stats(), Stat_MagicPrecisionBonus) + statValue(attacker.Stats(), Stat_TotalPrecisionBonus) + LuckRoll() } // true = hit lands, false = hit does not land -func MagicHitRoll(attacker RPGEntity, victim RPGEntity) bool { +func MagicHitRoll(attacker Entity_V2, victim Entity_V2) bool { return hitRoll(EvasionRoll(victim), MagicPrecisionRoll(attacker)) } // true = hit lands, false = hit does not land -func PhysicalHitRoll(attacker RPGEntity, victim RPGEntity) (hit bool, evasion, precision int) { +func PhysicalHitRoll(attacker Entity_V2, victim Entity_V2) (hit bool, evasion, precision int) { evasion = EvasionRoll(victim) precision = PhysicalPrecisionRoll(attacker) hit = hitRoll(evasion, precision) @@ -271,21 +330,30 @@ func hitRoll(evasionRoll, precisionRoll int) bool { return evasionRoll < precisionRoll } -func UnarmedDamage(attacker RPGEntity) int { - return RollD4(1) + StatValue(attacker, Stat_DamageBonus_Physical_Unarmed) +func UnarmedDamage(attacker Entity_V2) int { + if attacker.Stats() == nil { + return 0 + } + + return RollD4(1) + statValue(attacker.Stats(), Stat_DamageBonus_Physical_Unarmed) } -func PhysicalWeaponDamange(attacker RPGEntity, weapon RPGItem, victim RPGEntity) (totalDamage int, dmgType DamageType) { - totalDamage, dmgType = weapon.RPGType().RollDamage()(victim, attacker) +func PhysicalWeaponDamage(attacker Entity_V2, weapon Item_V2, victim Entity_V2) (totalDamage int, dmgType DamageType) { + if attacker.Stats() == nil || weapon.Damaging() == nil || victim.Stats() == nil { + return 0, DamageType_Physical_Unarmed + } + + totalDamage, dmgType = weapon.Damaging().DamageRoll() bonusDmgStat := DamageTypeToBonusStat(dmgType) + dmgResistStat := DamageTypeToResistanceStat(dmgType) - totalDamage = totalDamage + StatValue(attacker, bonusDmgStat) + totalDamage = totalDamage + statValue(attacker.Stats(), bonusDmgStat) - statValue(victim.Stats(), dmgResistStat) return } -func UnarmedAttack(attacker RPGEntity, victim RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) { +func UnarmedAttack(attacker Entity_V2, victim Entity_V2) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) { hit, evasionRoll, precisionRoll = PhysicalHitRoll(attacker, victim) if !hit { @@ -298,14 +366,14 @@ func UnarmedAttack(attacker RPGEntity, victim RPGEntity) (hit bool, precisionRol return } -func PhysicalWeaponAttack(attacker RPGEntity, weapon RPGItem, victim RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) { +func PhysicalWeaponAttack(attacker Entity_V2, weapon Item_V2, victim Entity_V2) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) { hit, evasionRoll, precisionRoll = PhysicalHitRoll(attacker, victim) if !hit { return } - damage, damageType = PhysicalWeaponDamange(attacker, weapon, victim) + damage, damageType = PhysicalWeaponDamage(attacker, weapon, victim) return } diff --git a/game/model/world_dungeon.go b/game/model/world_dungeon.go new file mode 100644 index 0000000..b3b4322 --- /dev/null +++ b/game/model/world_dungeon.go @@ -0,0 +1,328 @@ +package model + +import ( + "math/rand" + "mvvasilev/last_light/engine" + "slices" + + "github.com/google/uuid" +) + +type DungeonType int + +const ( + DungeonTypeBSP DungeonType = iota + DungeonTypeCaverns + DungeonTypeMine + DungeonTypeUndercity +) + +func randomDungeonType() DungeonType { + return DungeonType(rand.Intn(4)) +} + +type Dungeon struct { + levels []*DungeonLevel + + current int +} + +func CreateDungeon(width, height int, depth int) *Dungeon { + levels := make([]*DungeonLevel, 0, depth) + + for range depth { + levels = append(levels, CreateDungeonLevel(width, height, randomDungeonType())) + } + + return &Dungeon{ + levels: levels, + current: 0, + } +} + +func (d *Dungeon) CurrentLevel() *DungeonLevel { + return d.levels[d.current] +} + +func (d *Dungeon) MoveToNextLevel() (moved bool) { + if !d.HasNextLevel() { + return false + } + + d.current++ + + return true +} + +func (d *Dungeon) MoveToPreviousLevel() (moved bool) { + if !d.HasPreviousLevel() { + return false + } + + d.current-- + + return true +} + +func (d *Dungeon) NextLevel() *DungeonLevel { + if !d.HasNextLevel() { + return nil + } + + return d.levels[d.current+1] +} + +func (d *Dungeon) PreviousLevel() *DungeonLevel { + if !d.HasPreviousLevel() { + return nil + } + + return d.levels[d.current-1] +} + +func (d *Dungeon) HasPreviousLevel() bool { + return d.current-1 >= 0 +} + +func (d *Dungeon) HasNextLevel() bool { + return d.current+1 < len(d.levels) +} + +type DungeonLevel struct { + ground Map_V2 + entitiesByPosition map[engine.Position]Entity_V2 + entities map[uuid.UUID]Entity_V2 +} + +func CreateDungeonLevel(width, height int, dungeonType DungeonType) (dLevel *DungeonLevel) { + + genTable := CreateLootTable() + + genTable.Add(1, func() Item_V2 { + return Item_HealthPotion() + }) + + itemPool := []Item_V2{ + Item_Bow(), + Item_Longsword(), + Item_Club(), + Item_Dagger(), + Item_Handaxe(), + Item_Javelin(), + Item_LightHammer(), + Item_Mace(), + Item_Quarterstaff(), + Item_Sickle(), + Item_Spear(), + } + + genTable.Add(1, func() Item_V2 { + item := itemPool[rand.Intn(len(itemPool))] + + rarities := []ItemRarity{ + ItemRarity_Common, + ItemRarity_Uncommon, + ItemRarity_Rare, + ItemRarity_Epic, + ItemRarity_Legendary, + } + + return GenerateItemOfTypeAndRarity(item, rarities[rand.Intn(len(rarities))]) + }) + + var groundLevel Map_V2 + + switch dungeonType { + case DungeonTypeBSP: + groundLevel = CreateBSPDungeonMap(width, height, 4) + default: + groundLevel = CreateBSPDungeonMap(width, height, 4) + } + + dLevel = &DungeonLevel{ + ground: groundLevel, + entities: map[uuid.UUID]Entity_V2{}, + entitiesByPosition: map[engine.Position]Entity_V2{}, + } + + if groundLevel.Rooms() == nil { + return dLevel + } + + forbiddenItemPositions := make([]engine.Position, 0) + + if groundLevel.NextLevelStaircase() != nil { + forbiddenItemPositions = append(forbiddenItemPositions, groundLevel.NextLevelStaircase().Position) + } + + if groundLevel.PreviousLevelStaircase() != nil { + forbiddenItemPositions = append(forbiddenItemPositions, groundLevel.PreviousLevelStaircase().Position) + } + + if groundLevel.PlayerSpawnPoint() != nil { + forbiddenItemPositions = append(forbiddenItemPositions, groundLevel.PreviousLevelStaircase().Position) + } + + items := SpawnItems(groundLevel.Rooms().Rooms, 0.01, genTable, forbiddenItemPositions) + + for pos, it := range items { + tile := Map_TileAt(groundLevel, pos.X(), pos.Y()) + + if !tile.Passable() { + continue + } + + Map_SetTileAt( + groundLevel, + pos.X(), + pos.Y(), + CreateTileFromPrototype(tile, Tile_WithItem(it)), + ) + } + + return dLevel +} + +func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTable *LootTable, forbiddenPositions []engine.Position) map[engine.Position]Item_V2 { + rooms := spawnableAreas + + itemLocations := make(map[engine.Position]Item_V2, 0) + + for _, r := range rooms { + maxItems := int(maxItemRatio * float32(r.Size().Area())) + + if maxItems < 1 { + continue + } + + numItems := rand.Intn(maxItems) + + for range numItems { + item := genTable.Generate() + + if item == nil { + continue + } + + pos := engine.PositionAt( + engine.RandInt(r.Position().X()+1, r.Position().X()+r.Size().Width()-1), + engine.RandInt(r.Position().Y()+1, r.Position().Y()+r.Size().Height()-1), + ) + + if slices.Contains(forbiddenPositions, pos) { + continue + } + + itemLocations[pos] = item + } + } + + return itemLocations +} + +func (d *DungeonLevel) Ground() Map_V2 { + return d.ground +} + +func (d *DungeonLevel) DropEntity(uuid uuid.UUID) { + ent := d.entities[uuid] + + if ent != nil { + delete(d.entitiesByPosition, ent.Positioned().Position) + } + + delete(d.entities, uuid) +} + +func (d *DungeonLevel) AddEntity(entity Entity_V2) { + d.entities[entity.UniqueId()] = entity + + if entity.Positioned() != nil { + d.entitiesByPosition[entity.Positioned().Position] = entity + } +} + +func (d *DungeonLevel) MoveEntityTo(uuid uuid.UUID, x, y int) { + ent := d.entities[uuid] + + if ent == nil || ent.Positioned() == nil { + return + } + + d.RemoveEntityAt(ent.Positioned().Position.XY()) + + ent.Positioned().Position = engine.PositionAt(x, y) + + d.entitiesByPosition[ent.Positioned().Position] = ent +} + +func (d *DungeonLevel) RemoveEntityAt(x, y int) { + delete(d.entitiesByPosition, engine.PositionAt(x, y)) +} + +func (d *DungeonLevel) RemoveItemAt(x, y int) (item Item_V2) { + if !Map_IsInBounds(d.ground, x, y) { + return nil + } + + tile := Map_TileAt(d.ground, x, y) + + if tile.Item() == nil { + return nil + } + + item = tile.Item().Item + + tile.RemoveItem() + + return +} + +func (d *DungeonLevel) SetItemAt(x, y int, it Item_V2) (success bool) { + if !d.TileAt(x, y).Passable() { + return false + } + + tile := d.TileAt(x, y) + + tile.WithItem(it) + + return true +} + +func (d *DungeonLevel) TileAt(x, y int) Tile_V2 { + entity := d.entitiesByPosition[engine.PositionAt(x, y)] + tile := Map_TileAt(d.ground, x, y) + + if entity != nil { + return CreateTileFromPrototype(tile, Tile_WithEntity(entity)) + } + + return tile +} + +func (d *DungeonLevel) IsTilePassable(x, y int) bool { + if !Map_IsInBounds(d.ground, x, y) { + return false + } + + tile := d.TileAt(x, y) + + if tile.Entity() != nil { + return false + } + + return tile.Passable() +} + +func (d *DungeonLevel) EntityAt(x, y int) (e Entity_V2) { + return d.entitiesByPosition[engine.PositionAt(x, y)] +} + +func (d *DungeonLevel) IsGroundTileOpaque(x, y int) bool { + if !Map_IsInBounds(d.ground, x, y) { + return false + } + + return Map_TileAt(d.ground, x, y).Opaque() +} diff --git a/game/model/world_empty_map.go b/game/model/world_empty_map.go new file mode 100644 index 0000000..fcdf448 --- /dev/null +++ b/game/model/world_empty_map.go @@ -0,0 +1,50 @@ +package model + +// import "mvvasilev/last_light/engine" + +// type EmptyDungeonMap struct { +// level *BasicMap +// } + +// func (edl *EmptyDungeonMap) Size() engine.Size { +// return edl.level.Size() +// } + +// func (edl *EmptyDungeonMap) SetTileAt(x int, y int, t Tile) Tile { +// return edl.level.SetTileAt(x, y, t) +// } + +// func (edl *EmptyDungeonMap) TileAt(x int, y int) Tile { +// return edl.level.TileAt(x, y) +// } + +// func (edl *EmptyDungeonMap) IsInBounds(x, y int) bool { +// return edl.level.IsInBounds(x, y) +// } + +// func (edl *EmptyDungeonMap) Tick(dt int64) { + +// } + +// func (edl *EmptyDungeonMap) Rooms() []engine.BoundingBox { +// rooms := make([]engine.BoundingBox, 1) + +// rooms = append(rooms, engine.BoundingBox{ +// Sized: engine.WithSize(edl.Size()), +// Positioned: engine.WithPosition(engine.PositionAt(0, 0)), +// }) + +// return rooms +// } + +// func (edl *EmptyDungeonMap) PlayerSpawnPoint() engine.Position { +// return engine.PositionAt(edl.Size().Width()/2, edl.Size().Height()/2) +// } + +// func (edl *EmptyDungeonMap) NextLevelStaircasePosition() engine.Position { +// return engine.PositionAt(edl.Size().Width()/3, edl.Size().Height()/3) +// } + +// func (bsp *EmptyDungeonMap) PreviousLevelStaircasePosition() engine.Position { +// return bsp.PlayerSpawnPoint() +// } diff --git a/game/model/world_entity_map.go b/game/model/world_entity_map.go new file mode 100644 index 0000000..1543ec5 --- /dev/null +++ b/game/model/world_entity_map.go @@ -0,0 +1,140 @@ +package model + +// import ( +// "maps" +// "mvvasilev/last_light/engine" +// "mvvasilev/last_light/game/npc" + +// "github.com/google/uuid" +// ) + +// type EntityMap struct { +// entities map[int]EntityTile + +// engine.Sized +// } + +// func CreateEntityMap(width, height int) *EntityMap { +// return &EntityMap{ +// entities: make(map[int]EntityTile, 0), +// Sized: engine.WithSize(engine.SizeOf(width, height)), +// } +// } + +// func (em *EntityMap) SetTileAt(x int, y int, t Tile) Tile { +// return nil +// // if !em.FitsWithin(x, y) { +// // return +// // } + +// // index := em.Size().AsArrayIndex(x, y) + +// // TODO? May not be necessary +// } + +// func (em *EntityMap) FindEntityByUuid(uuid uuid.UUID) (key int, entity EntityTile) { +// for i, e := range em.entities { +// if e.Entity().UniqueId() == uuid { +// return i, e +// } +// } + +// return -1, nil +// } + +// func (em *EntityMap) AddEntity(entity Entity_V2) { +// if entity.Positioned() == nil { +// return +// } + +// if !em.FitsWithin(entity.Positioned().Position.XY()) { +// return +// } + +// key := em.Size().AsArrayIndex(entity.Positioned().Position.XY()) +// et := CreateBasicEntityTile(entity) + +// em.entities[key] = et +// } + +// func (em *EntityMap) DropEntity(uuid uuid.UUID) { +// maps.DeleteFunc(em.entities, func(i int, et EntityTile) bool { +// return et.Entity().UniqueId() == uuid +// }) +// } + +// func (em *EntityMap) MoveEntity(uuid uuid.UUID, dx, dy int) { +// oldKey, e := em.FindEntityByUuid(uuid) + +// if e == nil { +// return +// } + +// if !em.FitsWithin(e.Entity().Positioned().Position.WithOffset(dx, dy).XY()) { +// return +// } + +// delete(em.entities, oldKey) + +// newPos := e.Entity().Position().WithOffset(dx, dy) +// e.Entity().MoveTo(newPos) + +// newKey := em.Size().AsArrayIndex(e.Entity().Position().XY()) + +// em.entities[newKey] = e +// } + +// func (em *EntityMap) MoveEntityTo(uuid uuid.UUID, x, y int) { +// oldKey, e := em.FindEntityByUuid(uuid) + +// if e == nil { +// return +// } + +// if !em.FitsWithin(x, y) { +// return +// } + +// delete(em.entities, oldKey) + +// e.Entity().MoveTo(engine.PositionAt(x, y)) + +// newKey := em.Size().AsArrayIndex(e.Entity().Position().XY()) + +// em.entities[newKey] = e +// } + +// func (em *EntityMap) TileAt(x int, y int) Tile { +// if !em.FitsWithin(x, y) { +// return CreateStaticTile(x, y, TileTypeVoid()) +// } + +// key := em.Size().AsArrayIndex(x, y) + +// return em.entities[key] +// } + +// func (em *EntityMap) EntityAt(x, y int) (ent npc.MovableEntity) { +// tile := em.TileAt(x, y) + +// if tile == nil { +// return nil +// } + +// return tile.(EntityTile).Entity() +// } + +// func (em *EntityMap) IsInBounds(x, y int) bool { +// return em.FitsWithin(x, y) +// } + +// func (em *EntityMap) MarkExplored(x, y int) { + +// } + +// func (em *EntityMap) ExploredTileAt(x, y int) Tile { +// return CreateStaticTile(x, y, TileTypeVoid()) +// } + +// func (em *EntityMap) Tick(dt int64) { +// } diff --git a/game/world/generate_bsp_map.go b/game/model/world_generate_bsp_map.go similarity index 71% rename from game/world/generate_bsp_map.go rename to game/model/world_generate_bsp_map.go index 5e4b02f..70e6f2c 100644 --- a/game/world/generate_bsp_map.go +++ b/game/model/world_generate_bsp_map.go @@ -1,4 +1,4 @@ -package world +package model import ( "math/rand" @@ -27,7 +27,7 @@ type bspNode struct { splitDir splitDirection } -func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap { +func CreateBSPDungeonMap(width, height int, numSplits int) Map_V2 { root := new(bspNode) root.origin = engine.PositionAt(0, 0) @@ -35,10 +35,10 @@ func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap { split(root, numSplits) - tiles := make([][]Tile, height) + tiles := make([][]Tile_V2, height) for h := range height { - tiles[h] = make([]Tile, width) + tiles[h] = make([]Tile_V2, width) } rooms := make([]engine.BoundingBox, 0, numSplits*numSplits) @@ -88,28 +88,50 @@ func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap { ) }) - bsp := new(BSPDungeonMap) - spawnRoom := findRoom(root.left) staircaseRoom := findRoom(root.right) - bsp.rooms = rooms - bsp.level = CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey)) - - bsp.playerSpawnPoint = engine.PositionAt( + playerPos := engine.PositionAt( spawnRoom.Position().X()+spawnRoom.Size().Width()/2, spawnRoom.Position().Y()+spawnRoom.Size().Height()/2, ) - bsp.nextLevelStaircase = engine.PositionAt( - staircaseRoom.Position().X()+staircaseRoom.Size().Width()/2, - staircaseRoom.Position().Y()+staircaseRoom.Size().Height()/2, + newBsp := CreateMap( + root.size, + tiles, + tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey), + Tile_Void(), + Map_WithRooms(rooms), + Map_WithPlayerSpawnPoint(playerPos), + Map_WithNextLevelStaircase(engine.PositionAt( + staircaseRoom.Position().X()+staircaseRoom.Size().Width()/2, + staircaseRoom.Position().Y()+staircaseRoom.Size().Height()/2, + )), + Map_WithPreviousLevelStaircase(playerPos), ) - bsp.level.SetTileAt(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), CreateStaticTile(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), TileTypeStaircaseDown())) - bsp.level.SetTileAt(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), CreateStaticTile(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), TileTypeStaircaseUp())) + Map_SetTileAt(newBsp, newBsp.NextLevelStaircase().Position.X(), newBsp.NextLevelStaircase().Position.Y(), Tile_StaircaseDown()) + Map_SetTileAt(newBsp, newBsp.PreviousLevelStaircase().Position.X(), newBsp.PreviousLevelStaircase().Position.Y(), Tile_StaircaseUp()) - return bsp + // bsp := new(BSPDungeonMap) + + // bsp.rooms = rooms + // bsp.level = CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey)) + + // bsp.playerSpawnPoint = engine.PositionAt( + // spawnRoom.Position().X()+spawnRoom.Size().Width()/2, + // spawnRoom.Position().Y()+spawnRoom.Size().Height()/2, + // ) + + // bsp.nextLevelStaircase = engine.PositionAt( + // staircaseRoom.Position().X()+staircaseRoom.Size().Width()/2, + // staircaseRoom.Position().Y()+staircaseRoom.Size().Height()/2, + // ) + + // bsp.level.SetTileAt(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), CreateStaticTile(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), TileTypeStaircaseDown())) + // bsp.level.SetTileAt(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), CreateStaticTile(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), TileTypeStaircaseUp())) + + return newBsp } func findRoom(parent *bspNode) engine.BoundingBox { @@ -124,7 +146,7 @@ func findRoom(parent *bspNode) engine.BoundingBox { } } -func zCorridor(tiles [][]Tile, from engine.Position, to engine.Position, direction splitDirection) { +func zCorridor(tiles [][]Tile_V2, from engine.Position, to engine.Position, direction splitDirection) { switch direction { case splitDirectionHorizontal: xMidPoint := (from.X() + to.X()) / 2 @@ -206,7 +228,7 @@ func split(parent *bspNode, numSplits int) { split(parent.right, numSplits-1) } -func horizontalTunnel(tiles [][]Tile, x1, x2, y int) { +func horizontalTunnel(tiles [][]Tile_V2, x1, x2, y int) { if x1 > x2 { tx := x2 x2 = x1 @@ -222,7 +244,7 @@ func horizontalTunnel(tiles [][]Tile, x1, x2, y int) { continue } - tiles[y][x] = CreateStaticTile(x, y, TileTypeGround()) + tiles[y][x] = Tile_Ground() placeWallAtIfNotPassable(tiles, x, y-1) placeWallAtIfNotPassable(tiles, x, y+1) @@ -233,7 +255,7 @@ func horizontalTunnel(tiles [][]Tile, x1, x2, y int) { placeWallAtIfNotPassable(tiles, x2, y+1) } -func verticalTunnel(tiles [][]Tile, y1, y2, x int) { +func verticalTunnel(tiles [][]Tile_V2, y1, y2, x int) { if y1 > y2 { ty := y2 y2 = y1 @@ -249,7 +271,7 @@ func verticalTunnel(tiles [][]Tile, y1, y2, x int) { continue } - tiles[y][x] = CreateStaticTile(x, y, TileTypeGround()) + tiles[y][x] = Tile_Ground() placeWallAtIfNotPassable(tiles, x-1, y) placeWallAtIfNotPassable(tiles, x+1, y) @@ -260,33 +282,33 @@ func verticalTunnel(tiles [][]Tile, y1, y2, x int) { placeWallAtIfNotPassable(tiles, x+1, y2) } -func placeWallAtIfNotPassable(tiles [][]Tile, x, y int) { +func placeWallAtIfNotPassable(tiles [][]Tile_V2, x, y int) { if tiles[y][x] != nil && tiles[y][x].Passable() { return } - tiles[y][x] = CreateStaticTile(x, y, TileTypeWall()) + tiles[y][x] = Tile_Wall() } -func makeRoom(tiles [][]Tile, room engine.BoundingBox) { +func makeRoom(tiles [][]Tile_V2, room engine.BoundingBox) { width := room.Size().Width() height := room.Size().Height() x := room.Position().X() y := room.Position().Y() for w := x; w < x+width+1; w++ { - tiles[y][w] = CreateStaticTile(w, y, TileTypeWall()) - tiles[y+height][w] = CreateStaticTile(w, y+height, TileTypeWall()) + tiles[y][w] = Tile_Wall() + tiles[y+height][w] = Tile_Wall() } for h := y; h < y+height+1; h++ { - tiles[h][x] = CreateStaticTile(x, h, TileTypeWall()) - tiles[h][x+width] = CreateStaticTile(x+width, h, TileTypeWall()) + tiles[h][x] = Tile_Wall() + tiles[h][x+width] = Tile_Wall() } for h := y + 1; h < y+height; h++ { for w := x + 1; w < x+width; w++ { - tiles[h][w] = CreateStaticTile(w, h, TileTypeGround()) + tiles[h][w] = Tile_Ground() } } } diff --git a/game/model/world_generate_empty_map.go b/game/model/world_generate_empty_map.go new file mode 100644 index 0000000..f29022f --- /dev/null +++ b/game/model/world_generate_empty_map.go @@ -0,0 +1,13 @@ +package model + +// import "github.com/gdamore/tcell/v2" + +// func CreateEmptyDungeonLevel(width, height int) *BasicMap { +// tiles := make([][]Tile, height) + +// for h := range height { +// tiles[h] = make([]Tile, width) +// } + +// return CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey)) +// } diff --git a/game/model/world_map.go b/game/model/world_map.go new file mode 100644 index 0000000..58ab022 --- /dev/null +++ b/game/model/world_map.go @@ -0,0 +1,188 @@ +package model + +import ( + "mvvasilev/last_light/engine" + + "github.com/gdamore/tcell/v2" +) + +type Map_V2 interface { + Size() engine.Size + Tiles() [][]Tile_V2 + ExploredTiles() map[engine.Position]Tile_V2 + ExploredTileStyle() tcell.Style + DefaultTile() Tile_V2 + + PlayerSpawnPoint() *Map_PlayerSpawnPointComponent + Rooms() *Map_RoomsComponent + NextLevelStaircase() *Map_NextLevelStaircaseComponent + PreviousLevelStaircase() *Map_PreviousLevelStaircaseComponent +} + +type Map_PlayerSpawnPointComponent struct { + Position engine.Position +} + +type Map_RoomsComponent struct { + Rooms []engine.BoundingBox +} + +type Map_NextLevelStaircaseComponent struct { + Position engine.Position +} + +type Map_PreviousLevelStaircaseComponent struct { + Position engine.Position +} + +type BaseMap struct { + size engine.Size + tiles [][]Tile_V2 + exploredTiles map[engine.Position]Tile_V2 + exploredStyle tcell.Style + defaultTile Tile_V2 + + playerSpawnPos *Map_PlayerSpawnPointComponent + rooms *Map_RoomsComponent + nextLevel *Map_NextLevelStaircaseComponent + prevLevel *Map_PreviousLevelStaircaseComponent +} + +func CreateMap(size engine.Size, tiles [][]Tile_V2, exploredStyle tcell.Style, defaultTile Tile_V2, components ...func(*BaseMap)) Map_V2 { + m := &BaseMap{ + size: size, + tiles: tiles, + exploredTiles: make(map[engine.Position]Tile_V2, 0), + exploredStyle: exploredStyle, + defaultTile: defaultTile, + } + + for _, c := range components { + c(m) + } + + return m +} + +func (m *BaseMap) Size() engine.Size { + return m.size +} + +func (m *BaseMap) Tiles() [][]Tile_V2 { + return m.tiles +} + +func (m *BaseMap) ExploredTiles() map[engine.Position]Tile_V2 { + return m.exploredTiles +} + +func (m *BaseMap) ExploredTileStyle() tcell.Style { + return m.exploredStyle +} + +func (m *BaseMap) DefaultTile() Tile_V2 { + return m.defaultTile +} + +func (m *BaseMap) PlayerSpawnPoint() *Map_PlayerSpawnPointComponent { + return m.playerSpawnPos +} + +func (m *BaseMap) Rooms() *Map_RoomsComponent { + return m.rooms +} + +func (m *BaseMap) NextLevelStaircase() *Map_NextLevelStaircaseComponent { + return m.nextLevel +} + +func (m *BaseMap) PreviousLevelStaircase() *Map_PreviousLevelStaircaseComponent { + return m.prevLevel +} + +func Map_WithRooms(rooms []engine.BoundingBox) func(*BaseMap) { + return func(bm *BaseMap) { + bm.rooms = &Map_RoomsComponent{ + Rooms: rooms, + } + } +} + +func Map_WithPlayerSpawnPoint(pos engine.Position) func(*BaseMap) { + return func(bm *BaseMap) { + bm.playerSpawnPos = &Map_PlayerSpawnPointComponent{ + Position: pos, + } + } +} + +func Map_WithNextLevelStaircase(pos engine.Position) func(*BaseMap) { + return func(bm *BaseMap) { + bm.nextLevel = &Map_NextLevelStaircaseComponent{ + Position: pos, + } + } +} + +func Map_WithPreviousLevelStaircase(pos engine.Position) func(*BaseMap) { + return func(bm *BaseMap) { + bm.prevLevel = &Map_PreviousLevelStaircaseComponent{ + Position: pos, + } + } +} + +func Map_SetTileAt(bm Map_V2, x int, y int, t Tile_V2) Tile_V2 { + if !Map_IsInBounds(bm, x, y) { + return bm.DefaultTile() + } + + bm.Tiles()[y][x] = t + + return bm.Tiles()[y][x] +} + +func Map_TileAt(bm Map_V2, x int, y int) Tile_V2 { + if !Map_IsInBounds(bm, x, y) { + return bm.DefaultTile() + } + + tile := bm.Tiles()[y][x] + + if tile == nil { + return bm.DefaultTile() + } + + return tile +} + +func Map_IsInBounds(bm Map_V2, x, y int) bool { + if x < 0 || y < 0 { + return false + } + + if x >= bm.Size().Width() || y >= bm.Size().Height() { + return false + } + + return true +} + +func Map_ExploredTileAt(bm Map_V2, x, y int) Tile_V2 { + return bm.ExploredTiles()[engine.PositionAt(x, y)] +} + +func Map_MarkExplored(bm Map_V2, x, y int) { + if !Map_IsInBounds(bm, x, y) { + return + } + + tile := Map_TileAt(bm, x, y) + + symbol, _ := tile.DefaultPresentation() + + bm.ExploredTiles()[engine.PositionAt(x, y)] = &BaseTile{ + defaultSymbol: symbol, + defaultStyle: bm.ExploredTileStyle(), + } +} diff --git a/game/model/world_multilevel_map.go b/game/model/world_multilevel_map.go new file mode 100644 index 0000000..fd8ec43 --- /dev/null +++ b/game/model/world_multilevel_map.go @@ -0,0 +1,127 @@ +package model + +// import "mvvasilev/last_light/engine" + +// type MultilevelMap struct { +// layers []Map +// } + +// func CreateMultilevelMap(maps ...Map) *MultilevelMap { +// m := new(MultilevelMap) + +// m.layers = maps + +// return m +// } + +// func (mm *MultilevelMap) Size() engine.Size { +// if len(mm.layers) == 0 { +// return engine.SizeOf(0, 0) +// } + +// return mm.layers[0].Size() +// } + +// func (mm *MultilevelMap) SetTileAt(x, y int, t Tile) Tile { +// return mm.layers[0].SetTileAt(x, y, t) +// } + +// func (mm *MultilevelMap) UnsetTileAtHeight(x, y, height int) { +// if len(mm.layers) < height { +// return +// } + +// mm.layers[height].SetTileAt(x, y, nil) +// } + +// func (mm *MultilevelMap) SetTileAtHeight(x, y, height int, t Tile) { +// if len(mm.layers) < height { +// return +// } + +// mm.layers[height].SetTileAt(x, y, t) +// } + +// func (mm *MultilevelMap) CollectTilesAt(x, y int, filter func(t Tile) bool) []Tile { +// tiles := make([]Tile, len(mm.layers)) + +// if !mm.IsInBounds(x, y) { +// return tiles +// } + +// for i := len(mm.layers) - 1; i >= 0; i-- { +// tile := mm.layers[i].TileAt(x, y) + +// if tile != nil && !tile.Transparent() && filter(tile) { +// tiles = append(tiles, tile) +// } + +// } + +// return tiles +// } + +// func (mm *MultilevelMap) TileAt(x int, y int) Tile { +// if !mm.IsInBounds(x, y) { +// return CreateStaticTile(x, y, TileTypeVoid()) +// } + +// for i := len(mm.layers) - 1; i >= 0; i-- { +// tile := mm.layers[i].TileAt(x, y) + +// if tile != nil && !tile.Transparent() { +// return tile +// } + +// } + +// return CreateStaticTile(x, y, TileTypeVoid()) +// } + +// func (mm *MultilevelMap) IsInBounds(x, y int) bool { +// if x < 0 || y < 0 { +// return false +// } + +// if x >= mm.Size().Width() || y >= mm.Size().Height() { +// return false +// } + +// return true +// } + +// func (mm *MultilevelMap) MarkExplored(x, y int) { +// for _, m := range mm.layers { +// m.MarkExplored(x, y) +// } +// } + +// func (mm *MultilevelMap) ExploredTileAt(x, y int) Tile { +// for i := len(mm.layers) - 1; i >= 0; i-- { +// tile := mm.layers[i].ExploredTileAt(x, y) + +// if tile != nil && !tile.Transparent() { +// return tile +// } +// } + +// return CreateStaticTile(x, y, TileTypeVoid()) +// } + +// func (mm *MultilevelMap) TileAtHeight(x, y, height int) Tile { +// if !mm.IsInBounds(x, y) { +// return CreateStaticTile(x, y, TileTypeVoid()) +// } + +// if height > len(mm.layers)-1 { +// return CreateStaticTile(x, y, TileTypeVoid()) +// } + +// return mm.layers[height].TileAt(x, y) +// } + +// func (mm *MultilevelMap) Tick(dt int64) { +// for _, l := range mm.layers { +// l.Tick(dt) +// } +// } diff --git a/game/model/world_tile.go b/game/model/world_tile.go new file mode 100644 index 0000000..9821470 --- /dev/null +++ b/game/model/world_tile.go @@ -0,0 +1,368 @@ +package model + +import ( + "github.com/gdamore/tcell/v2" +) + +type Material uint + +const ( + MaterialGround Material = iota + MaterialRock + MaterialWall + MaterialGrass + MaterialVoid + MaterialClosedDoor + MaterialOpenDoor + MaterialStaircaseDown + MaterialStaircaseUp +) + +type Tile_ItemComponent struct { + Item Item_V2 +} + +type Tile_EntityComponent struct { + Entity Entity_V2 +} + +type Tile_V2 interface { + DefaultPresentation() (rune, tcell.Style) + Material() Material + Passable() bool + Opaque() bool + Transparent() bool + + Item() *Tile_ItemComponent + RemoveItem() + WithItem(item Item_V2) + + Entity() *Tile_EntityComponent + RemoveEntity() + WithEntity(entity Entity_V2) +} + +type BaseTile struct { + defaultSymbol rune + defaultStyle tcell.Style + + material Material + passable, opaque, transparent bool + + item *Tile_ItemComponent + entity *Tile_EntityComponent +} + +func CreateTileFromPrototype(prototype Tile_V2, components ...func(*BaseTile)) Tile_V2 { + defaultSymbol, defaultStyle := prototype.DefaultPresentation() + + return CreateTile( + defaultSymbol, + defaultStyle, + prototype.Material(), + prototype.Passable(), + prototype.Opaque(), + prototype.Transparent(), + components..., + ) +} + +func CreateTile(defaultSymbol rune, defaultStyle tcell.Style, material Material, passable, opaque, transparent bool, components ...func(*BaseTile)) Tile_V2 { + t := &BaseTile{ + defaultSymbol: defaultSymbol, + defaultStyle: defaultStyle, + material: material, + passable: passable, + opaque: opaque, + transparent: transparent, + } + + for _, c := range components { + c(t) + } + + return t +} + +func (t *BaseTile) DefaultPresentation() (rune, tcell.Style) { + return t.defaultSymbol, t.defaultStyle +} + +func (t *BaseTile) Material() Material { + return t.material +} + +func (t *BaseTile) Passable() bool { + return t.passable +} + +func (t *BaseTile) Opaque() bool { + return t.opaque +} + +func (t *BaseTile) Transparent() bool { + return t.transparent +} + +func (t *BaseTile) Item() *Tile_ItemComponent { + return t.item +} + +func (t *BaseTile) RemoveItem() { + t.item = nil +} + +func (t *BaseTile) WithItem(item Item_V2) { + t.item = &Tile_ItemComponent{ + Item: item, + } +} + +func (t *BaseTile) Entity() *Tile_EntityComponent { + return t.entity +} + +func (t *BaseTile) RemoveEntity() { + t.entity = nil +} + +func (t *BaseTile) WithEntity(entity Entity_V2) { + t.entity = &Tile_EntityComponent{ + Entity: entity, + } +} + +func Tile_WithEntity(entity Entity_V2) func(*BaseTile) { + return func(bt *BaseTile) { + bt.entity = &Tile_EntityComponent{ + Entity: entity, + } + } +} + +func Tile_WithItem(item Item_V2) func(*BaseTile) { + return func(bt *BaseTile) { + bt.item = &Tile_ItemComponent{ + Item: item, + } + } +} + +func Tile_Void() Tile_V2 { + return CreateTile( + ' ', + tcell.StyleDefault, + MaterialVoid, + false, true, true, + ) +} + +func Tile_Ground() Tile_V2 { + return CreateTile( + '.', + tcell.StyleDefault, + MaterialGround, + true, false, false, + ) +} + +func Tile_Rock() Tile_V2 { + return CreateTile( + '█', + tcell.StyleDefault, + MaterialRock, + false, true, false, + ) +} + +func Tile_Wall() Tile_V2 { + return CreateTile( + '#', + tcell.StyleDefault.Background(tcell.ColorGray), + MaterialWall, + false, true, false, + ) +} + +// func TileTypeClosedDoor() TileType { +// return TileType{ +// Material: MaterialClosedDoor, +// Passable: false, +// Transparent: false, +// Presentation: '[', +// Opaque: true, +// Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue).Background(tcell.ColorSaddleBrown), +// } +// } + +// func TileTypeOpenDoor() TileType { +// return TileType{ +// Material: MaterialClosedDoor, +// Passable: false, +// Transparent: false, +// Presentation: '_', +// Opaque: false, +// Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue), +// } +// } + +func Tile_StaircaseDown() Tile_V2 { + return CreateTile( + '≡', + tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold), + MaterialStaircaseDown, + true, false, false, + ) +} + +func Tile_StaircaseUp() Tile_V2 { + return CreateTile( + '^', + tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold), + MaterialStaircaseDown, + true, false, false, + ) +} + +// type Tile interface { +// Position() engine.Position +// Presentation() (rune, tcell.Style) +// Passable() bool +// Transparent() bool +// Opaque() bool +// Type() TileType +// } + +// type StaticTile struct { +// position engine.Position +// t TileType + +// style tcell.Style +// } + +// func CreateStaticTile(x, y int, t TileType) Tile { +// st := new(StaticTile) + +// st.position = engine.PositionAt(x, y) +// st.t = t +// st.style = t.Style + +// return st +// } + +// func CreateStaticTileWithStyleOverride(x, y int, t TileType, style tcell.Style) Tile { +// return &StaticTile{ +// position: engine.PositionAt(x, y), +// t: t, +// style: style, +// } +// } + +// func (st *StaticTile) Position() engine.Position { +// return st.position +// } + +// func (st *StaticTile) Presentation() (rune, tcell.Style) { +// return st.t.Presentation, st.style +// } + +// func (st *StaticTile) Passable() bool { +// return st.t.Passable +// } + +// func (st *StaticTile) Transparent() bool { +// return st.t.Transparent +// } + +// func (st *StaticTile) Opaque() bool { +// return st.t.Opaque +// } + +// func (st *StaticTile) Type() TileType { +// return st.t +// } + +// type ItemTile struct { +// position engine.Position +// item item.Item +// } + +// func CreateItemTile(position engine.Position, item item.Item) *ItemTile { +// it := new(ItemTile) + +// it.position = position +// it.item = item + +// return it +// } + +// func (it *ItemTile) Item() item.Item { +// return it.item +// } + +// func (it *ItemTile) Position() engine.Position { +// return it.position +// } + +// func (it *ItemTile) Presentation() (rune, tcell.Style) { +// return it.item.Type().TileIcon(), it.item.Type().Style() +// } + +// func (it *ItemTile) Passable() bool { +// return true +// } + +// func (it *ItemTile) Transparent() bool { +// return false +// } + +// func (it *ItemTile) Opaque() bool { +// return false +// } + +// func (it *ItemTile) Type() TileType { +// return TileType{} +// } + +// type EntityTile interface { +// Entity() npc.MovableEntity +// Tile +// } + +// type BasicEntityTile struct { +// entity npc.MovableEntity +// } + +// func CreateBasicEntityTile(entity npc.MovableEntity) *BasicEntityTile { +// return &BasicEntityTile{ +// entity: entity, +// } +// } + +// func (bet *BasicEntityTile) Entity() npc.MovableEntity { +// return bet.entity +// } + +// func (bet *BasicEntityTile) Position() engine.Position { +// return bet.entity.Position() +// } + +// func (bet *BasicEntityTile) Presentation() (rune, tcell.Style) { +// return bet.entity.Presentation() +// } + +// func (bet *BasicEntityTile) Passable() bool { +// return false +// } + +// func (bet *BasicEntityTile) Transparent() bool { +// return false +// } + +// func (bet *BasicEntityTile) Opaque() bool { +// return false +// } + +// func (bet *BasicEntityTile) Type() TileType { +// return TileType{} +// } diff --git a/game/npc/entity.go b/game/npc/entity.go deleted file mode 100644 index 1c528b6..0000000 --- a/game/npc/entity.go +++ /dev/null @@ -1,67 +0,0 @@ -package npc - -import ( - "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/item" - - "github.com/gdamore/tcell/v2" - "github.com/google/uuid" -) - -type Direction int - -const ( - DirectionNone Direction = iota - North - South - West - East -) - -func DirectionName(dir Direction) string { - switch dir { - case North: - return "North" - case South: - return "South" - case West: - return "West" - case East: - return "East" - default: - return "Unknown" - } -} - -func MovementDirectionOffset(dir Direction) (int, int) { - switch dir { - case North: - return 0, -1 - case South: - return 0, 1 - case West: - return -1, 0 - case East: - return 1, 0 - } - - return 0, 0 -} - -type Entity interface { - UniqueId() uuid.UUID - Presentation() (rune, tcell.Style) -} - -type MovableEntity interface { - Position() engine.Position - MoveTo(newPosition engine.Position) - - Entity -} - -type EquippedEntity interface { - Inventory() *item.EquippedInventory - - Entity -} diff --git a/game/npc/npc.go b/game/npc/npc.go deleted file mode 100644 index 58a2ae5..0000000 --- a/game/npc/npc.go +++ /dev/null @@ -1,48 +0,0 @@ -package npc - -import ( - "mvvasilev/last_light/engine" - - "github.com/gdamore/tcell/v2" - "github.com/google/uuid" -) - -type NPC interface { - Name() string - - MovableEntity -} - -type BasicNPC struct { - id uuid.UUID - name string - presentation rune - style tcell.Style - engine.Positioned -} - -func CreateNPC(pos engine.Position, name string, presentation rune, style tcell.Style) *BasicNPC { - return &BasicNPC{ - id: uuid.New(), - name: name, - presentation: presentation, - style: style, - Positioned: engine.WithPosition(pos), - } -} - -func (c *BasicNPC) Name() string { - return c.name -} - -func (c *BasicNPC) MoveTo(newPosition engine.Position) { - c.Positioned.SetPosition(newPosition) -} - -func (c *BasicNPC) UniqueId() uuid.UUID { - return c.id -} - -func (c *BasicNPC) Presentation() (rune, tcell.Style) { - return c.presentation, c.style -} diff --git a/game/npc/rpg_npc.go b/game/npc/rpg_npc.go deleted file mode 100644 index 559008e..0000000 --- a/game/npc/rpg_npc.go +++ /dev/null @@ -1,58 +0,0 @@ -package npc - -import ( - "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/item" - "mvvasilev/last_light/game/rpg" - - "github.com/gdamore/tcell/v2" -) - -type RPGNPC interface { - NPC - rpg.RPGEntity - EquippedEntity -} - -type BasicRPGNPC struct { - inventory *item.EquippedInventory - - *BasicNPC - *rpg.BasicRPGEntity -} - -func CreateRPGNPC(x, y int, name string, representation rune, style tcell.Style, stats map[rpg.Stat]int) *BasicRPGNPC { - rpgnpc := &BasicRPGNPC{ - inventory: item.CreateEquippedInventory(), - BasicNPC: CreateNPC( - engine.PositionAt(x, y), - name, - representation, - style, - ), - BasicRPGEntity: rpg.CreateBasicRPGEntity( - 0, - stats, - map[rpg.Stat][]rpg.StatModifier{}, - ), - } - - rpgnpc.Heal(rpg.BaseMaxHealth(rpgnpc)) - - return rpgnpc -} - -func (rnpc *BasicRPGNPC) Inventory() *item.EquippedInventory { - return rnpc.inventory -} - -func (p *BasicRPGNPC) CalculateAttack(other rpg.RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType rpg.DamageType) { - mainHand := p.inventory.AtSlot(item.EquippedSlotDominantHand) - - switch mh := mainHand.(type) { - case rpg.RPGItem: - return rpg.PhysicalWeaponAttack(p, mh, other) - default: - return rpg.UnarmedAttack(p, other) - } -} diff --git a/game/player/player.go b/game/player/player.go deleted file mode 100644 index 4507560..0000000 --- a/game/player/player.go +++ /dev/null @@ -1,67 +0,0 @@ -package player - -import ( - "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/item" - "mvvasilev/last_light/game/rpg" - - "github.com/gdamore/tcell/v2" - "github.com/google/uuid" -) - -type Player struct { - id uuid.UUID - position engine.Position - - inventory *item.EquippedInventory - - *rpg.BasicRPGEntity -} - -func CreatePlayer(x, y int, playerStats map[rpg.Stat]int) *Player { - p := new(Player) - - p.id = uuid.New() - p.position = engine.PositionAt(x, y) - p.inventory = item.CreateEquippedInventory() - p.BasicRPGEntity = rpg.CreateBasicRPGEntity( - 0, - playerStats, - map[rpg.Stat][]rpg.StatModifier{}, - ) - - p.Heal(rpg.BaseMaxHealth(p)) - - return p -} - -func (p *Player) UniqueId() uuid.UUID { - return p.id -} - -func (p *Player) Position() engine.Position { - return p.position -} - -func (p *Player) MoveTo(newPos engine.Position) { - p.position = newPos -} - -func (p *Player) Presentation() (rune, tcell.Style) { - return '@', tcell.StyleDefault -} - -func (p *Player) Inventory() *item.EquippedInventory { - return p.inventory -} - -func (p *Player) CalculateAttack(other rpg.RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType rpg.DamageType) { - mainHand := p.inventory.AtSlot(item.EquippedSlotDominantHand) - - switch mh := mainHand.(type) { - case rpg.RPGItem: - return rpg.PhysicalWeaponAttack(p, mh, other) - default: - return rpg.UnarmedAttack(p, other) - } -} diff --git a/game/rpg/rpg_entity.go b/game/rpg/rpg_entity.go deleted file mode 100644 index ba4f565..0000000 --- a/game/rpg/rpg_entity.go +++ /dev/null @@ -1,104 +0,0 @@ -package rpg - -import "slices" - -type RPGEntity interface { - BaseStat(stat Stat) int - SetBaseStat(stat Stat, value int) - - CollectModifiersForStat(stat Stat) []StatModifier - AddStatModifier(modifier StatModifier) - RemoveStatModifier(id StatModifierId) - - CurrentHealth() int - Heal(health int) - Damage(damage int) - - CalculateAttack(other RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) -} - -type BasicRPGEntity struct { - stats map[Stat]int - - statModifiers map[Stat][]StatModifier - - currentHealth int -} - -func CreateBasicRPGEntity(health int, baseStats map[Stat]int, statModifiers map[Stat][]StatModifier) *BasicRPGEntity { - return &BasicRPGEntity{ - stats: baseStats, - statModifiers: statModifiers, - currentHealth: health, - } -} - -func (brpg *BasicRPGEntity) BaseStat(stat Stat) int { - return brpg.stats[stat] -} - -func (brpg *BasicRPGEntity) SetBaseStat(stat Stat, value int) { - brpg.stats[stat] = value -} - -func (brpg *BasicRPGEntity) CollectModifiersForStat(stat Stat) []StatModifier { - modifiers := brpg.statModifiers[stat] - - if modifiers == nil { - return []StatModifier{} - } - - return modifiers -} - -func (brpg *BasicRPGEntity) AddStatModifier(modifier StatModifier) { - existing := brpg.statModifiers[modifier.Stat] - - if existing == nil { - existing = make([]StatModifier, 0) - } - - existing = append(existing, modifier) - - brpg.statModifiers[modifier.Stat] = existing -} - -func (brpg *BasicRPGEntity) RemoveStatModifier(id StatModifierId) { - for k, v := range brpg.statModifiers { - for i, sm := range v { - if sm.Id == id { - brpg.statModifiers[k] = slices.Delete(v, i, i+1) - } - } - } -} - -func (brpg *BasicRPGEntity) CurrentHealth() int { - return brpg.currentHealth -} - -func (brpg *BasicRPGEntity) Heal(health int) { - maxHealth := BaseMaxHealth(brpg) - - if brpg.currentHealth+health > maxHealth { - brpg.currentHealth = maxHealth - - return - } - - brpg.currentHealth += health -} - -func (brpg *BasicRPGEntity) Damage(damage int) { - if brpg.currentHealth-damage < 0 { - brpg.currentHealth = 0 - - return - } - - brpg.currentHealth -= damage -} - -func (brpg *BasicRPGEntity) CalculateAttack(other RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) { - return UnarmedAttack(brpg, other) -} diff --git a/game/rpg/rpg_items.go b/game/rpg/rpg_items.go deleted file mode 100644 index 62dfdb1..0000000 --- a/game/rpg/rpg_items.go +++ /dev/null @@ -1,288 +0,0 @@ -package rpg - -import ( - "mvvasilev/last_light/game/item" - - "github.com/gdamore/tcell/v2" -) - -type RPGItemMetaType int - -const ( - MetaItemType_Physical_Weapon RPGItemMetaType = iota - MetaItemType_Magic_Weapon - MetaItemType_Weapon - MetaItemType_Physical_Armour - MetaItemType_Magic_Armour - MetaItemType_Armour -) - -type RPGItemType interface { - RollDamage() func(victim, attacker RPGEntity) (damage int, dmgType DamageType) - MetaTypes() []RPGItemMetaType - - item.ItemType -} - -type BasicRPGItemType struct { - damageRollFunc func(victim, attacker RPGEntity) (damage int, dmgType DamageType) - - metaTypes []RPGItemMetaType - - *item.BasicItemType -} - -func (it *BasicRPGItemType) RollDamage() func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return it.damageRollFunc -} - -func (it *BasicRPGItemType) MetaTypes() []RPGItemMetaType { - return it.metaTypes -} - -func ItemTypeBow() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - // TODO: Ranged - return RollD8(1), DamageType_Physical_Piercing - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1000, - "Bow", - "To shoot arrows with", - ')', - " |)", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorBrown), - ), - } -} - -func ItemTypeLongsword() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return RollD8(1), DamageType_Physical_Slashing - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1001, - "Longsword", - "You know nothing.", - '/', - "╪══", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSilver), - ), - } -} - -func ItemTypeClub() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return RollD8(1), DamageType_Physical_Bludgeoning - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1002, - "Club", - "Bonk", - '!', - "-══", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSaddleBrown), - ), - } -} - -func ItemTypeDagger() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return RollD6(1), DamageType_Physical_Piercing - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1003, - "Dagger", - "Stabby, stabby", - '-', - " +─", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSilver), - ), - } -} - -func ItemTypeHandaxe() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return RollD6(1), DamageType_Physical_Slashing - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1004, - "Handaxe", - "Choppy, choppy", - '¶', - " ─╗", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSilver), - ), - } -} - -func ItemTypeJavelin() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - // TODO: Ranged - return RollD6(1), DamageType_Physical_Piercing - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1005, - "Javelin", - "Ranged pokey, pokey", - 'Î', - " ─>", - 20, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSilver), - ), - } -} - -func ItemTypeLightHammer() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return RollD6(1), DamageType_Physical_Bludgeoning - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1006, - "Handaxe", - "Choppy, choppy", - '¶', - " ─╗", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSilver), - ), - } -} - -func ItemTypeMace() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return RollD6(1), DamageType_Physical_Bludgeoning - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1007, - "Mace", - "Smashey, smashey", - 'i', - " ─¤", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSilver), - ), - } -} - -func ItemTypeQuarterstaff() RPGItemType { - - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return RollD6(1), DamageType_Physical_Bludgeoning - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1008, - "Quarterstaff", - "Whacky, whacky", - '|', - "───", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSaddleBrown), - ), - } -} - -func ItemTypeSickle() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return RollD6(1), DamageType_Physical_Slashing - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1009, - "Sickle", - "Slicey, slicey?", - '?', - " ─U", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSilver), - ), - } -} - -func ItemTypeSpear() RPGItemType { - return &BasicRPGItemType{ - damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) { - return RollD8(1), DamageType_Physical_Piercing - }, - metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon}, - BasicItemType: item.CreateBasicItemType( - 1010, - "Spear", - "Pokey, pokey", - 'Î', - "──>", - 1, - item.EquippedSlotDominantHand, - tcell.StyleDefault.Foreground(tcell.ColorSilver), - ), - } -} - -type RPGItem interface { - Modifiers() []StatModifier - RPGType() RPGItemType - - item.Item -} - -type BasicRPGItem struct { - modifiers []StatModifier - rpgType RPGItemType - - item.BasicItem -} - -func (i *BasicRPGItem) Modifiers() []StatModifier { - return i.modifiers -} - -func (i *BasicRPGItem) RPGType() RPGItemType { - return i.rpgType -} - -func CreateRPGItem(name string, style tcell.Style, itemType RPGItemType, modifiers []StatModifier) RPGItem { - return &BasicRPGItem{ - modifiers: modifiers, - rpgType: itemType, - BasicItem: item.CreateBasicItemWithName( - name, - style, - itemType, - 1, - ), - } -} diff --git a/game/state/character_creation_state.go b/game/state/character_creation_state.go index 003291f..060aae4 100644 --- a/game/state/character_creation_state.go +++ b/game/state/character_creation_state.go @@ -2,9 +2,8 @@ package state import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" - "mvvasilev/last_light/game/rpg" - "mvvasilev/last_light/game/turns" + "mvvasilev/last_light/game/model" + "mvvasilev/last_light/game/systems" "mvvasilev/last_light/game/ui/menu" "github.com/gdamore/tcell/v2" @@ -16,8 +15,8 @@ const ( ) type CharacterCreationState struct { - turnSystem *turns.TurnSystem - inputSystem *input.InputSystem + turnSystem *systems.TurnSystem + inputSystem *systems.InputSystem startGame bool @@ -25,26 +24,26 @@ type CharacterCreationState struct { ccMenu *menu.CharacterCreationMenu } -func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *CharacterCreationState { +func CreateCharacterCreationState(turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem) *CharacterCreationState { menuState := &menu.CharacterCreationMenuState{ AvailablePoints: 21, CurrentHighlight: 0, Stats: []*menu.StatState{ { - Stat: rpg.Stat_Attributes_Strength, + Stat: model.Stat_Attributes_Strength, Value: 1, }, { - Stat: rpg.Stat_Attributes_Dexterity, + Stat: model.Stat_Attributes_Dexterity, Value: 1, }, { - Stat: rpg.Stat_Attributes_Intelligence, + Stat: model.Stat_Attributes_Intelligence, Value: 1, }, { - Stat: rpg.Stat_Attributes_Constitution, + Stat: model.Stat_Attributes_Constitution, Value: 1, }, }, @@ -58,7 +57,12 @@ func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *inp } ccs.menuState.RandomizeCharacter = func() { - stats := rpg.RandomStats(21, 1, 20, []rpg.Stat{rpg.Stat_Attributes_Strength, rpg.Stat_Attributes_Constitution, rpg.Stat_Attributes_Intelligence, rpg.Stat_Attributes_Dexterity}) + stats := model.RandomStats(21, 1, 20, []model.Stat{ + model.Stat_Attributes_Strength, + model.Stat_Attributes_Constitution, + model.Stat_Attributes_Intelligence, + model.Stat_Attributes_Dexterity, + }) ccs.menuState.AvailablePoints = 0 ccs.menuState.Stats = []*menu.StatState{} @@ -84,8 +88,8 @@ func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *inp return ccs } -func (ccs *CharacterCreationState) InputContext() input.Context { - return input.InputContext_Menu +func (ccs *CharacterCreationState) InputContext() systems.InputContext { + return systems.InputContext_Menu } func (ccs *CharacterCreationState) IncreaseStatValue() { @@ -125,7 +129,7 @@ func (ccs *CharacterCreationState) DecreaseStatValue() { func (ccs *CharacterCreationState) OnTick(dt int64) GameState { if ccs.startGame { - stats := map[rpg.Stat]int{} + stats := map[model.Stat]int{} for _, s := range ccs.menuState.Stats { stats[s.Stat] = s.Value @@ -137,23 +141,23 @@ func (ccs *CharacterCreationState) OnTick(dt int64) GameState { action := ccs.inputSystem.NextAction() switch action { - case input.InputAction_Menu_HighlightRight: + case systems.InputAction_Menu_HighlightRight: ccs.IncreaseStatValue() - case input.InputAction_Menu_HighlightLeft: + case systems.InputAction_Menu_HighlightLeft: ccs.DecreaseStatValue() - case input.InputAction_Menu_HighlightDown: + case systems.InputAction_Menu_HighlightDown: if ccs.menuState.CurrentHighlight > len(ccs.menuState.Stats) { break } ccs.menuState.CurrentHighlight++ - case input.InputAction_Menu_HighlightUp: + case systems.InputAction_Menu_HighlightUp: if ccs.menuState.CurrentHighlight == 0 { break } ccs.menuState.CurrentHighlight-- - case input.InputAction_Menu_Select: + case systems.InputAction_Menu_Select: ccs.ccMenu.SelectHighlight() } diff --git a/game/state/dialog_state.go b/game/state/dialog_state.go index e20056a..c415013 100644 --- a/game/state/dialog_state.go +++ b/game/state/dialog_state.go @@ -2,14 +2,13 @@ package state import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" - "mvvasilev/last_light/game/turns" + "mvvasilev/last_light/game/systems" "mvvasilev/last_light/game/ui" ) type DialogState struct { - inputSystem *input.InputSystem - turnSystem *turns.TurnSystem + inputSystem *systems.InputSystem + turnSystem *systems.TurnSystem prevState GameState @@ -18,7 +17,7 @@ type DialogState struct { returnToPreviousState bool } -func CreateDialogState(inputSystem *input.InputSystem, turnSystem *turns.TurnSystem, dialog *ui.UIDialog, prevState GameState) *DialogState { +func CreateDialogState(inputSystem *systems.InputSystem, turnSystem *systems.TurnSystem, dialog *ui.UIDialog, prevState GameState) *DialogState { return &DialogState{ inputSystem: inputSystem, turnSystem: turnSystem, @@ -28,12 +27,12 @@ func CreateDialogState(inputSystem *input.InputSystem, turnSystem *turns.TurnSys } } -func (s *DialogState) InputContext() input.Context { - return input.InputContext_Menu +func (s *DialogState) InputContext() systems.InputContext { + return systems.InputContext_Menu } func (ds *DialogState) OnTick(dt int64) GameState { - if ds.inputSystem.NextAction() == input.InputAction_Menu_Select { + if ds.inputSystem.NextAction() == systems.InputAction_Menu_Select { ds.returnToPreviousState = true ds.dialog.Select() } diff --git a/game/state/game_over_state.go b/game/state/game_over_state.go new file mode 100644 index 0000000..454d5c6 --- /dev/null +++ b/game/state/game_over_state.go @@ -0,0 +1,84 @@ +package state + +import ( + "fmt" + "mvvasilev/last_light/engine" + "mvvasilev/last_light/game/systems" + "mvvasilev/last_light/game/ui" + + "github.com/gdamore/tcell/v2" +) + +type GameOverState struct { + inputSystem *systems.InputSystem + + gameOverTitle *engine.Raw + deathText *ui.UILabel + + backToMainMenuBtn *ui.UISimpleButton + + returnToMainMenu bool +} + +func CreateGameOverState(inputSystem *systems.InputSystem) *GameOverState { + gos := &GameOverState{ + inputSystem: inputSystem, + gameOverTitle: engine.CreateRawDrawable( + 14, 1, tcell.StyleDefault.Attributes(tcell.AttrBold).Foreground(tcell.ColorYellow), + "_____ _____ ", + "| __ \\ | _ | ", + "| | \\/ __ _ _ __ ___ ___ | | | |_ _____ _ __ ", + "| | __ / _` | '_ ` _ \\ / _ \\ | | | \\ \\ / / _ \\ '__|", + "| |_\\ \\ (_| | | | | | | __/ \\ \\_/ /\\ V / __/ | ", + " \\____/\\__,_|_| |_| |_|\\___| \\___/ \\_/ \\___|_| ", + ), + deathText: ui.CreateUILabel( + 14, 8, 51, 5, + fmt.Sprintf( + "For all your efforts, your endeavour was ultimately cut short. "+ + "You have been left bleeding out on the dungeon floor. Your remains "+ + "will serve as a warning to future seekers of the last light.", + ), + tcell.StyleDefault, + ), + } + + gos.backToMainMenuBtn = ui.CreateSimpleButton( + engine.TERMINAL_SIZE_WIDTH/2-len("Back to Main Menu")/2, + 16, + "Back to Main Menu", + tcell.StyleDefault, + tcell.StyleDefault.Attributes(tcell.AttrBold), + func() { + gos.returnToMainMenu = true + }, + ) + + gos.backToMainMenuBtn.Highlight() + + return gos +} + +func (gos *GameOverState) InputContext() systems.InputContext { + return systems.InputContext_Menu +} + +func (gos *GameOverState) OnTick(dt int64) GameState { + if gos.inputSystem.NextAction() == systems.InputAction_Menu_Select { + gos.backToMainMenuBtn.Select() + } + + if gos.returnToMainMenu { + return CreateMainMenuState(systems.CreateTurnSystem(), gos.inputSystem) + } + + return gos +} + +func (gos *GameOverState) CollectDrawables() []engine.Drawable { + return []engine.Drawable{ + gos.gameOverTitle, + gos.deathText, + gos.backToMainMenuBtn, + } +} diff --git a/game/state/game_state.go b/game/state/game_state.go index b04fcf9..d01244e 100644 --- a/game/state/game_state.go +++ b/game/state/game_state.go @@ -2,11 +2,11 @@ package state import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/systems" ) type GameState interface { - InputContext() input.Context + InputContext() systems.InputContext OnTick(dt int64) GameState CollectDrawables() []engine.Drawable } diff --git a/game/state/inventory_screen_state.go b/game/state/inventory_screen_state.go index e225c61..70c22e0 100644 --- a/game/state/inventory_screen_state.go +++ b/game/state/inventory_screen_state.go @@ -2,17 +2,18 @@ package state import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" - "mvvasilev/last_light/game/player" - "mvvasilev/last_light/game/turns" + "mvvasilev/last_light/game/model" + "mvvasilev/last_light/game/systems" "mvvasilev/last_light/game/ui/menu" "github.com/gdamore/tcell/v2" ) type InventoryScreenState struct { - inputSystem *input.InputSystem - turnSystem *turns.TurnSystem + eventLog *engine.GameEventLog + inputSystem *systems.InputSystem + turnSystem *systems.TurnSystem + dungeon *model.Dungeon prevState GameState exitMenu bool @@ -20,12 +21,13 @@ type InventoryScreenState struct { inventoryMenu *menu.PlayerInventoryMenu selectedInventorySlot engine.Position - player *player.Player + player *model.Player_V2 } -func CreateInventoryScreenState(inputSystem *input.InputSystem, turnSystem *turns.TurnSystem, player *player.Player, prevState GameState) *InventoryScreenState { +func CreateInventoryScreenState(eventLog *engine.GameEventLog, dungeon *model.Dungeon, inputSystem *systems.InputSystem, turnSystem *systems.TurnSystem, player *model.Player_V2, prevState GameState) *InventoryScreenState { iss := new(InventoryScreenState) + iss.eventLog = eventLog iss.inputSystem = inputSystem iss.turnSystem = turnSystem iss.prevState = prevState @@ -33,12 +35,13 @@ func CreateInventoryScreenState(inputSystem *input.InputSystem, turnSystem *turn iss.selectedInventorySlot = engine.PositionAt(0, 0) iss.exitMenu = false iss.inventoryMenu = menu.CreatePlayerInventoryMenu(43, 0, player.Inventory(), tcell.StyleDefault, tcell.StyleDefault.Background(tcell.ColorDarkSlateGray)) + iss.dungeon = dungeon return iss } -func (s *InventoryScreenState) InputContext() input.Context { - return input.InputContext_Inventory +func (s *InventoryScreenState) InputContext() systems.InputContext { + return systems.InputContext_Inventory } func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) { @@ -46,32 +49,53 @@ func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) { nextState = iss switch nextAction { - case input.InputAction_Menu_Exit: + case systems.InputAction_Menu_Exit: nextState = iss.prevState - case input.InputAction_DropItem: + case systems.InputAction_InteractItem: + item := iss.player.Inventory().ItemAt(iss.selectedInventorySlot.XY()) + + if item == nil { + break + } + + if item.Usable() != nil { + item.Usable().Use(iss.eventLog, iss.dungeon, iss.player) + } + + if item.Equippable() != nil { + if iss.player.Inventory().AtSlot(item.Equippable().Slot) != nil { + iss.player.Inventory().Push(iss.player.Inventory().AtSlot(item.Equippable().Slot)) + } + + iss.player.Inventory().Equip(item, item.Equippable().Slot) + } + + iss.player.Inventory().ReduceQuantityAt(iss.selectedInventorySlot.X(), iss.selectedInventorySlot.Y(), 1) + + case systems.InputAction_DropItem: iss.player.Inventory().Drop(iss.selectedInventorySlot.XY()) - case input.InputAction_Menu_HighlightUp: + case systems.InputAction_Menu_HighlightUp: if iss.selectedInventorySlot.Y() == 0 { break } iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, -1) iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY()) - case input.InputAction_Menu_HighlightDown: + case systems.InputAction_Menu_HighlightDown: if iss.selectedInventorySlot.Y() == iss.player.Inventory().Shape().Height()-1 { break } iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, +1) iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY()) - case input.InputAction_Menu_HighlightLeft: + case systems.InputAction_Menu_HighlightLeft: if iss.selectedInventorySlot.X() == 0 { break } iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(-1, 0) iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY()) - case input.InputAction_Menu_HighlightRight: + case systems.InputAction_Menu_HighlightRight: if iss.selectedInventorySlot.X() == iss.player.Inventory().Shape().Width()-1 { break } diff --git a/game/state/main_menu_state.go b/game/state/main_menu_state.go index 23e473e..a99c872 100644 --- a/game/state/main_menu_state.go +++ b/game/state/main_menu_state.go @@ -2,16 +2,15 @@ package state import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" - "mvvasilev/last_light/game/turns" + "mvvasilev/last_light/game/systems" "mvvasilev/last_light/game/ui" "github.com/gdamore/tcell/v2" ) type MainMenuState struct { - turnSystem *turns.TurnSystem - inputSystem *input.InputSystem + turnSystem *systems.TurnSystem + inputSystem *systems.InputSystem menuTitle *engine.Raw buttons []*ui.UISimpleButton @@ -21,7 +20,7 @@ type MainMenuState struct { startNewGame bool } -func CreateMainMenuState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *MainMenuState { +func CreateMainMenuState(turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem) *MainMenuState { turnSystem.Clear() state := new(MainMenuState) @@ -43,7 +42,7 @@ func CreateMainMenuState(turnSystem *turns.TurnSystem, inputSystem *input.InputS state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 7, "New Game", tcell.StyleDefault, highlightStyle, func() { state.startNewGame = true })) - state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 9, "Load Game", tcell.StyleDefault, highlightStyle, func() { + state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 9, "Key Bindings // TODO", tcell.StyleDefault, highlightStyle, func() { })) state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 11, "Quit", tcell.StyleDefault, highlightStyle, func() { @@ -56,26 +55,26 @@ func CreateMainMenuState(turnSystem *turns.TurnSystem, inputSystem *input.InputS return state } -func (s *MainMenuState) InputContext() input.Context { - return input.InputContext_Menu +func (s *MainMenuState) InputContext() systems.InputContext { + return systems.InputContext_Menu } func (mms *MainMenuState) OnTick(dt int64) GameState { nextAction := mms.inputSystem.NextAction() - if nextAction == input.InputAction_Menu_HighlightDown { + if nextAction == systems.InputAction_Menu_HighlightDown { mms.buttons[mms.currButtonSelected].Unhighlight() mms.currButtonSelected = engine.LimitIncrement(mms.currButtonSelected, 2) mms.buttons[mms.currButtonSelected].Highlight() } - if nextAction == input.InputAction_Menu_HighlightUp { + if nextAction == systems.InputAction_Menu_HighlightUp { mms.buttons[mms.currButtonSelected].Unhighlight() mms.currButtonSelected = engine.LimitDecrement(mms.currButtonSelected, 0) mms.buttons[mms.currButtonSelected].Highlight() } - if nextAction == input.InputAction_Menu_Select { + if nextAction == systems.InputAction_Menu_Select { mms.buttons[mms.currButtonSelected].Select() } diff --git a/game/state/pause_game_state.go b/game/state/pause_game_state.go index 92c3f00..48f0706 100644 --- a/game/state/pause_game_state.go +++ b/game/state/pause_game_state.go @@ -2,16 +2,15 @@ package state import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" - "mvvasilev/last_light/game/turns" + "mvvasilev/last_light/game/systems" "mvvasilev/last_light/game/ui" "github.com/gdamore/tcell/v2" ) type PauseGameState struct { - turnSystem *turns.TurnSystem - inputSystem *input.InputSystem + turnSystem *systems.TurnSystem + inputSystem *systems.InputSystem prevState GameState @@ -23,7 +22,7 @@ type PauseGameState struct { currButtonSelected int } -func PauseGame(prevState GameState, turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *PauseGameState { +func PauseGame(prevState GameState, turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem) *PauseGameState { s := new(PauseGameState) s.turnSystem = turnSystem @@ -67,23 +66,23 @@ func PauseGame(prevState GameState, turnSystem *turns.TurnSystem, inputSystem *i return s } -func (s *PauseGameState) InputContext() input.Context { - return input.InputContext_Menu +func (s *PauseGameState) InputContext() systems.InputContext { + return systems.InputContext_Menu } func (pg *PauseGameState) OnTick(dt int64) GameState { switch pg.inputSystem.NextAction() { - case input.InputAction_Menu_Exit: + case systems.InputAction_Menu_Exit: pg.unpauseGame = true - case input.InputAction_Menu_HighlightDown: + case systems.InputAction_Menu_HighlightDown: pg.buttons[pg.currButtonSelected].Unhighlight() pg.currButtonSelected = engine.LimitIncrement(pg.currButtonSelected, 1) pg.buttons[pg.currButtonSelected].Highlight() - case input.InputAction_Menu_HighlightUp: + case systems.InputAction_Menu_HighlightUp: pg.buttons[pg.currButtonSelected].Unhighlight() pg.currButtonSelected = engine.LimitDecrement(pg.currButtonSelected, 0) pg.buttons[pg.currButtonSelected].Highlight() - case input.InputAction_Menu_Select: + case systems.InputAction_Menu_Select: pg.buttons[pg.currButtonSelected].Select() } diff --git a/game/state/playing_state.go b/game/state/playing_state.go index 7ad0686..967c383 100644 --- a/game/state/playing_state.go +++ b/game/state/playing_state.go @@ -3,31 +3,27 @@ package state import ( "fmt" "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" - "mvvasilev/last_light/game/npc" - "mvvasilev/last_light/game/player" - "mvvasilev/last_light/game/rpg" - "mvvasilev/last_light/game/turns" + "mvvasilev/last_light/game/model" + "mvvasilev/last_light/game/systems" "mvvasilev/last_light/game/ui" - "mvvasilev/last_light/game/world" "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/views" ) type PlayingState struct { - turnSystem *turns.TurnSystem - inputSystem *input.InputSystem + turnSystem *systems.TurnSystem + inputSystem *systems.InputSystem - player *player.Player - someNPC npc.RPGNPC + player *model.Player_V2 + someNPC model.Entity_V2 eventLog *engine.GameEventLog uiEventLog *ui.UIEventLog healthBar *ui.UIHealthBar - dungeon *world.Dungeon + dungeon *model.Dungeon viewport *engine.Viewport @@ -36,7 +32,7 @@ type PlayingState struct { nextGameState GameState } -func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem, playerStats map[rpg.Stat]int) *PlayingState { +func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem, playerStats map[model.Stat]int) *PlayingState { turnSystem.Clear() s := new(PlayingState) @@ -46,11 +42,11 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy mapSize := engine.SizeOf(128, 128) - s.dungeon = world.CreateDungeon(mapSize.Width(), mapSize.Height(), 1) + s.dungeon = model.CreateDungeon(mapSize.Width(), mapSize.Height(), 1) - s.player = player.CreatePlayer( - s.dungeon.CurrentLevel().PlayerSpawnPoint().X(), - s.dungeon.CurrentLevel().PlayerSpawnPoint().Y(), + s.player = model.CreatePlayer_V2( + s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position.X(), + s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position.Y(), playerStats, ) @@ -58,30 +54,34 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy requeue = true complete = false + if s.player.HealthData().IsDead { + s.nextGameState = CreateGameOverState(inputSystem) + } + switch inputSystem.NextAction() { - case input.InputAction_PauseGame: + case systems.InputAction_PauseGame: s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem) - case input.InputAction_OpenInventory: - s.nextGameState = CreateInventoryScreenState(s.inputSystem, s.turnSystem, s.player, s) - case input.InputAction_PickUpItem: + case systems.InputAction_OpenInventory: + s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s) + case systems.InputAction_PickUpItem: s.PickUpItemUnderPlayer() complete = true - case input.InputAction_Interact: + case systems.InputAction_Interact: s.InteractBelowPlayer() complete = true - case input.InputAction_OpenLogs: + case systems.InputAction_OpenLogs: s.viewShortLogs = !s.viewShortLogs - case input.InputAction_MovePlayer_East: - s.MovePlayer(npc.East) + case systems.InputAction_MovePlayer_East: + s.MovePlayer(model.East) complete = true - case input.InputAction_MovePlayer_West: - s.MovePlayer(npc.West) + case systems.InputAction_MovePlayer_West: + s.MovePlayer(model.West) complete = true - case input.InputAction_MovePlayer_North: - s.MovePlayer(npc.North) + case systems.InputAction_MovePlayer_North: + s.MovePlayer(model.North) complete = true - case input.InputAction_MovePlayer_South: - s.MovePlayer(npc.South) + case systems.InputAction_MovePlayer_South: + s.MovePlayer(model.South) complete = true default: } @@ -89,13 +89,12 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy return }) - s.someNPC = npc.CreateRPGNPC( - s.dungeon.CurrentLevel().NextLevelStaircase().X(), - s.dungeon.CurrentLevel().NextLevelStaircase().Y(), - "NPC", - 'n', - tcell.StyleDefault, - rpg.RandomStats(21, 1, 20, []rpg.Stat{rpg.Stat_Attributes_Strength, rpg.Stat_Attributes_Constitution, rpg.Stat_Attributes_Intelligence, rpg.Stat_Attributes_Dexterity}), + 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) { @@ -107,14 +106,14 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy s.eventLog = engine.CreateGameEventLog(100) s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault) - s.healthBar = ui.CreateHealthBar(68, 0, 12, 3, s.player.CurrentHealth(), rpg.BaseMaxHealth(s.player), tcell.StyleDefault) + s.healthBar = ui.CreateHealthBar(68, 0, 12, 3, s.player, tcell.StyleDefault) s.dungeon.CurrentLevel().AddEntity(s.player) s.dungeon.CurrentLevel().AddEntity(s.someNPC) s.viewport = engine.CreateViewport( engine.PositionAt(0, 0), - s.dungeon.CurrentLevel().PlayerSpawnPoint(), + s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position, engine.SizeOf(80, 24), tcell.StyleDefault, ) @@ -124,53 +123,89 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy return s } -func (s *PlayingState) InputContext() input.Context { - return input.InputContext_Play +func (s *PlayingState) InputContext() systems.InputContext { + return systems.InputContext_Play } -func (ps *PlayingState) MovePlayer(direction npc.Direction) { - if direction == npc.DirectionNone { +func (ps *PlayingState) MovePlayer(direction model.Direction) { + if direction == model.DirectionNone { return } - newPlayerPos := ps.player.Position().WithOffset(npc.MovementDirectionOffset(direction)) - - if ps.dungeon.CurrentLevel().IsTilePassable(newPlayerPos.XY()) { - dx, dy := npc.MovementDirectionOffset(direction) - ps.dungeon.CurrentLevel().MoveEntity(ps.player.UniqueId(), dx, dy) - ps.viewport.SetCenter(ps.player.Position()) - - ps.eventLog.Log("You moved " + npc.DirectionName(direction)) - } + newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction)) ent := ps.dungeon.CurrentLevel().EntityAt(newPlayerPos.XY()) - // We are moving into an entity. Attack it. - if ent != nil { - switch rpge := ent.(type) { - case npc.RPGNPC: - hit, precision, evasion, dmg, dmgType := ps.player.CalculateAttack(rpge) - - if !hit { - ps.eventLog.Log(fmt.Sprintf("You attacked %v, but missed ( %v Evasion vs %v Precision)", rpge.Name(), evasion, precision)) - return - } - - rpge.Damage(dmg) - ps.eventLog.Log(fmt.Sprintf("You attacked %v, and hit for %v %v damage", rpge.Name(), dmg, rpg.DamageTypeName(dmgType))) + // We are moving into an entity with health data. Attack it. + if ent != nil && ent.HealthData() != nil { + if ent.HealthData().IsDead { + // TODO: If the entity is dead, the player should be able to move through it. + return } + + ExecuteAttack(ps.eventLog, ps.player, ent) + + return + } + + if ps.dungeon.CurrentLevel().IsTilePassable(newPlayerPos.XY()) { + ps.dungeon.CurrentLevel().MoveEntityTo(ps.player.UniqueId(), newPlayerPos.X(), newPlayerPos.Y()) + ps.viewport.SetCenter(ps.player.Position()) + + ps.eventLog.Log("You moved " + model.DirectionName(direction)) + } +} + +func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim model.Entity_V2) { + 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_V2) (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() { playerPos := ps.player.Position() - if playerPos == ps.dungeon.CurrentLevel().NextLevelStaircase() { + if playerPos == ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position { ps.SwitchToNextLevel() return } - if playerPos == ps.dungeon.CurrentLevel().PreviousLevelStaircase() { + if playerPos == ps.dungeon.CurrentLevel().Ground().PreviousLevelStaircase().Position { ps.SwitchToPreviousLevel() return } @@ -200,11 +235,11 @@ func (ps *PlayingState) SwitchToNextLevel() { ps.dungeon.MoveToNextLevel() - ps.player.MoveTo(ps.dungeon.CurrentLevel().PlayerSpawnPoint()) + ps.player.Positioned().Position = ps.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position ps.viewport = engine.CreateViewport( engine.PositionAt(0, 0), - ps.dungeon.CurrentLevel().PlayerSpawnPoint(), + ps.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position, engine.SizeOf(80, 24), tcell.StyleDefault, ) @@ -236,11 +271,11 @@ func (ps *PlayingState) SwitchToPreviousLevel() { ps.dungeon.MoveToPreviousLevel() - ps.player.MoveTo(ps.dungeon.CurrentLevel().NextLevelStaircase()) + ps.player.Positioned().Position = ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position ps.viewport = engine.CreateViewport( engine.PositionAt(0, 0), - ps.dungeon.CurrentLevel().NextLevelStaircase(), + ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position, engine.SizeOf(80, 24), tcell.StyleDefault, ) @@ -263,9 +298,12 @@ func (ps *PlayingState) PickUpItemUnderPlayer() { return } - itemName, _ := item.Name() - - ps.eventLog.Log("You picked up " + itemName) + if item.Named() != nil { + itemName := item.Named().Name + ps.eventLog.Log("You picked up " + itemName) + } else { + ps.eventLog.Log("You picked up an item") + } } func (ps *PlayingState) HasLineOfSight(start, end engine.Position) bool { @@ -285,25 +323,30 @@ func (ps *PlayingState) PlayerWithinHitRange(pos engine.Position) bool { } func (ps *PlayingState) CalcPathToPlayerAndMove() { + if ps.someNPC.HealthData().IsDead { + ps.dungeon.CurrentLevel().DropEntity(ps.someNPC.UniqueId()) + return + } + playerVisibleAndInRange := false - if ps.someNPC.Position().Distance(ps.player.Position()) < 20 && ps.HasLineOfSight(ps.someNPC.Position(), ps.player.Position()) { + if ps.someNPC.Positioned().Position.Distance(ps.player.Position()) < 20 && ps.HasLineOfSight(ps.someNPC.Positioned().Position, ps.player.Position()) { playerVisibleAndInRange = true } if !playerVisibleAndInRange { - randomMove := npc.Direction(engine.RandInt(int(npc.DirectionNone), int(npc.East))) + randomMove := model.Direction(engine.RandInt(int(model.DirectionNone), int(model.East))) - nextPos := ps.someNPC.Position() + nextPos := ps.someNPC.Positioned().Position switch randomMove { - case npc.North: + case model.North: nextPos = nextPos.WithOffset(0, -1) - case npc.South: + case model.South: nextPos = nextPos.WithOffset(0, +1) - case npc.West: + case model.West: nextPos = nextPos.WithOffset(-1, 0) - case npc.East: + case model.East: nextPos = nextPos.WithOffset(+1, 0) default: return @@ -320,23 +363,12 @@ func (ps *PlayingState) CalcPathToPlayerAndMove() { return } - if ps.PlayerWithinHitRange(ps.someNPC.Position()) { - hit, precision, evasion, dmg, dmgType := ps.player.CalculateAttack(ps.player) - - if !hit { - ps.eventLog.Log(fmt.Sprintf("%v attacked you, but missed ( %v Evasion vs %v Precision)", ps.someNPC.Name(), evasion, precision)) - return - } - - ps.player.Damage(dmg) - ps.healthBar.SetHealth(ps.player.CurrentHealth()) - ps.eventLog.Log(fmt.Sprintf("%v attacked you, and hit for %v %v damage", ps.someNPC.Name(), dmg, rpg.DamageTypeName(dmgType))) - - return + if ps.PlayerWithinHitRange(ps.someNPC.Positioned().Position) { + ExecuteAttack(ps.eventLog, ps.someNPC, ps.player) } pathToPlayer := engine.FindPath( - ps.someNPC.Position(), + ps.someNPC.Positioned().Position, ps.player.Position(), func(x, y int) bool { if x == ps.player.Position().X() && y == ps.player.Position().Y() { @@ -371,13 +403,13 @@ func (ps *PlayingState) OnTick(dt int64) (nextState GameState) { func (ps *PlayingState) CollectDrawables() []engine.Drawable { mainCameraDrawingInstructions := engine.CreateDrawingInstructions(func(v views.View) { visibilityMap := engine.ComputeFOV( - func(x, y int) world.Tile { - ps.dungeon.CurrentLevel().Flatten().MarkExplored(x, y) + func(x, y int) model.Tile_V2 { + model.Map_MarkExplored(ps.dungeon.CurrentLevel().Ground(), x, y) return ps.dungeon.CurrentLevel().TileAt(x, y) }, - func(x, y int) bool { return ps.dungeon.CurrentLevel().Flatten().IsInBounds(x, y) }, - func(x, y int) bool { return ps.dungeon.CurrentLevel().Flatten().TileAt(x, y).Opaque() }, + func(x, y int) bool { return model.Map_IsInBounds(ps.dungeon.CurrentLevel().Ground(), x, y) }, + func(x, y int) bool { return ps.dungeon.CurrentLevel().TileAt(x, y).Opaque() }, ps.player.Position().X(), ps.player.Position().Y(), 13, ) @@ -386,13 +418,21 @@ func (ps *PlayingState) CollectDrawables() []engine.Drawable { tile := visibilityMap[engine.PositionAt(x, y)] if tile != nil { - return tile.Presentation() + if tile.Entity() != nil { + return tile.Entity().Entity.Presentable().Rune, tile.Entity().Entity.Presentable().Style + } + + if tile.Item() != nil { + return tile.Item().Item.TileIcon(), tile.Item().Item.Style() + } + + return tile.DefaultPresentation() } - explored := ps.dungeon.CurrentLevel().Flatten().ExploredTileAt(x, y) + explored := model.Map_ExploredTileAt(ps.dungeon.CurrentLevel().Ground(), x, y) if explored != nil { - return explored.Presentation() + return explored.DefaultPresentation() } return ' ', tcell.StyleDefault diff --git a/game/state/quit_state.go b/game/state/quit_state.go index 60bb702..2646d5e 100644 --- a/game/state/quit_state.go +++ b/game/state/quit_state.go @@ -2,14 +2,14 @@ package state import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/systems" ) type QuitState struct { } -func (s *QuitState) InputContext() input.Context { - return input.InputContext_Menu +func (s *QuitState) InputContext() systems.InputContext { + return systems.InputContext_Menu } func (q *QuitState) OnTick(dt int64) GameState { diff --git a/game/input/input_system.go b/game/systems/input_system.go similarity index 94% rename from game/input/input_system.go rename to game/systems/input_system.go index 7347e1c..5347695 100644 --- a/game/input/input_system.go +++ b/game/systems/input_system.go @@ -1,4 +1,4 @@ -package input +package systems import ( "fmt" @@ -6,7 +6,7 @@ import ( "github.com/gdamore/tcell/v2" ) -type Context string +type InputContext string const ( InputContext_Play = "play" @@ -16,7 +16,7 @@ const ( type InputKey string -func InputKeyOf(context Context, mod tcell.ModMask, key tcell.Key, r rune) InputKey { +func InputKeyOf(context InputContext, mod tcell.ModMask, key tcell.Key, r rune) InputKey { return InputKey(fmt.Sprintf("%v-%v-%v-%v", context, mod, key, r)) } @@ -97,7 +97,7 @@ func (kb *InputSystem) Bind(key InputKey, action InputAction) { kb.keyBindings[key] = action } -func (kb *InputSystem) Input(context Context, ev *tcell.EventKey) { +func (kb *InputSystem) Input(context InputContext, ev *tcell.EventKey) { kb.nextAction = kb.keyBindings[InputKeyOf(context, ev.Modifiers(), ev.Key(), ev.Rune())] } diff --git a/game/turns/turn_system.go b/game/systems/turn_system.go similarity index 98% rename from game/turns/turn_system.go rename to game/systems/turn_system.go index 3cf3a36..c574ce2 100644 --- a/game/turns/turn_system.go +++ b/game/systems/turn_system.go @@ -1,4 +1,4 @@ -package turns +package systems import "mvvasilev/last_light/engine" diff --git a/game/ui/item.go b/game/ui/item.go deleted file mode 100644 index e9be428..0000000 --- a/game/ui/item.go +++ /dev/null @@ -1,166 +0,0 @@ -package ui - -import ( - "fmt" - "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" - "mvvasilev/last_light/game/item" - "mvvasilev/last_light/game/rpg" - - "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" - "github.com/google/uuid" -) - -type UIBasicItem struct { - id uuid.UUID - - item item.Item - - window UIWindow - itemName UILabel - - engine.Positioned - engine.Sized -} - -func CreateUIBasicItem(x, y int, item item.Item, style tcell.Style) *UIBasicItem { - - name, nameStyle := item.Name() - - return &UIBasicItem{ - id: uuid.New(), - item: item, - window: *CreateWindow(x, y, 33, 8, "Item", style), - itemName: *CreateSingleLineUILabel(x+1, y+1, name, nameStyle), - Positioned: engine.WithPosition(engine.PositionAt(x, y)), - Sized: engine.WithSize(engine.SizeOf(33, 8)), - } -} - -func (uibi *UIBasicItem) Input(e *tcell.EventKey) { -} - -func (uibi *UIBasicItem) UniqueId() uuid.UUID { - return uibi.id -} - -func (uibi *UIBasicItem) Draw(v views.View) { - uibi.window.Draw(v) - uibi.itemName.Draw(v) -} - -type UIRPGItem struct { - id uuid.UUID - - item rpg.RPGItem - - window UIWindow - itemName UILabel - - engine.Positioned - engine.Sized -} - -func CreateUIRPGItem(x, y int, item rpg.RPGItem, style tcell.Style) *UIRPGItem { - - name, nameStyle := item.Name() - - return &UIRPGItem{ - id: uuid.New(), - item: item, - window: *CreateWindow(x, y, 33, 8, "Item", style), - itemName: *CreateSingleLineUILabel(x+1, y+1, name, nameStyle), - Positioned: engine.WithPosition(engine.PositionAt(x, y)), - Sized: engine.WithSize(engine.SizeOf(33, 8)), - } -} - -func (uiri *UIRPGItem) Input(inputAction input.InputAction) { -} - -func (uiri *UIRPGItem) UniqueId() uuid.UUID { - return uiri.id -} - -func (uiri *UIRPGItem) Draw(v views.View) { - uiri.window.Draw(v) - uiri.itemName.Draw(v) - - statModifiers := uiri.item.Modifiers() - - x, y := uiri.itemName.Position().XY() - y++ - - for i, sm := range statModifiers { - - drawRPGItemStatModifier(x, y, tcell.StyleDefault, v, &sm) - - x += 9 + 2 // each stat is 9 characters long, with 2 characters separating the stats - - // Only 3 stats per line - if i > 0 && (i+1)%3 == 0 { - x = uiri.itemName.Position().X() - y++ - } - } -} - -func drawRPGItemStatModifier(x, y int, style tcell.Style, view views.View, sm *rpg.StatModifier) { - - // 5 characters per stat name - // 1 separating character - // 3 characters for bonus ( including sign, modifiers are limited to -99 and +99) - - const SEPARATING_CHARACTER rune = ':' - - switch sm.Stat { - case rpg.Stat_Attributes_Strength: - engine.DrawText(x, y, "STR", style, view) - case rpg.Stat_Attributes_Dexterity: - engine.DrawText(x, y, "DEX", style, view) - case rpg.Stat_Attributes_Intelligence: - engine.DrawText(x, y, "INT", style, view) - case rpg.Stat_Attributes_Constitution: - engine.DrawText(x, y, "CON", style, view) - case rpg.Stat_PhysicalPrecisionBonus: - engine.DrawText(x, y, "pPrcs", style, view) - case rpg.Stat_EvasionBonus: - engine.DrawText(x, y, "Evasn", style, view) - case rpg.Stat_MagicPrecisionBonus: - engine.DrawText(x, y, "mPrcs", style, view) - case rpg.Stat_TotalPrecisionBonus: - engine.DrawText(x, y, "tPrcs", style, view) - case rpg.Stat_DamageBonus_Physical_Unarmed: - engine.DrawText(x, y, "Unrmd", style, view) - case rpg.Stat_DamageBonus_Physical_Slashing: - engine.DrawText(x, y, "Slshn", style, view) - case rpg.Stat_DamageBonus_Physical_Piercing: - engine.DrawText(x, y, "Prcng", style, view) - case rpg.Stat_DamageBonus_Physical_Bludgeoning: - engine.DrawText(x, y, "Bldgn", style, view) - case rpg.Stat_DamageBonus_Magic_Fire: - engine.DrawText(x, y, "Fire", style, view) - case rpg.Stat_DamageBonus_Magic_Cold: - engine.DrawText(x, y, "Cold", style, view) - case rpg.Stat_DamageBonus_Magic_Necrotic: - engine.DrawText(x, y, "Ncrtc", style, view) - case rpg.Stat_DamageBonus_Magic_Thunder: - engine.DrawText(x, y, "Thndr", style, view) - case rpg.Stat_DamageBonus_Magic_Acid: - engine.DrawText(x, y, "Acid", style, view) - case rpg.Stat_DamageBonus_Magic_Poison: - engine.DrawText(x, y, "Poisn", style, view) - case rpg.Stat_MaxHealthBonus: - engine.DrawText(x, y, "maxHP", style, view) - default: - } - - view.SetContent(x+5, y, SEPARATING_CHARACTER, nil, style) - - if sm.Bonus < 0 { - engine.DrawText(x+6, y, fmt.Sprintf("-%02d", -sm.Bonus), tcell.StyleDefault.Foreground(tcell.ColorIndianRed), view) - } else { - engine.DrawText(x+6, y, fmt.Sprintf("+%02d", sm.Bonus), tcell.StyleDefault.Foreground(tcell.ColorLime), view) - } -} diff --git a/game/ui/menu/character_creation_menu.go b/game/ui/menu/menu_character_creation_menu.go similarity index 94% rename from game/ui/menu/character_creation_menu.go rename to game/ui/menu/menu_character_creation_menu.go index 74c930c..a190bfb 100644 --- a/game/ui/menu/character_creation_menu.go +++ b/game/ui/menu/menu_character_creation_menu.go @@ -3,8 +3,8 @@ package menu import ( "fmt" "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" - "mvvasilev/last_light/game/rpg" + "mvvasilev/last_light/game/model" + "mvvasilev/last_light/game/systems" "mvvasilev/last_light/game/ui" "github.com/gdamore/tcell/v2" @@ -13,7 +13,7 @@ import ( ) type statSelection struct { - stat rpg.Stat + stat model.Stat label *ui.UILabel plusButton *ui.UILabel statNumberLabel *ui.UILabel @@ -21,7 +21,7 @@ type statSelection struct { } type StatState struct { - Stat rpg.Stat + Stat model.Stat Value int } @@ -93,7 +93,7 @@ func (ccm *CharacterCreationMenu) UpdateState(state *CharacterCreationMenuState) label: ui.CreateSingleLineUILabel( statX, 3+i, - rpg.StatLongName(s.Stat), + model.StatLongName(s.Stat), labelStyle, ), minusButton: ui.CreateSingleLineUILabel( @@ -172,7 +172,7 @@ func (ccm *CharacterCreationMenu) Size() engine.Size { return engine.SizeOf(engine.TERMINAL_SIZE_WIDTH, engine.TERMINAL_SIZE_HEIGHT) } -func (ccm *CharacterCreationMenu) Input(inputAction input.InputAction) { +func (ccm *CharacterCreationMenu) Input(inputAction systems.InputAction) { } diff --git a/game/ui/menu/player_inventory_menu.go b/game/ui/menu/menu_player_inventory_menu.go similarity index 78% rename from game/ui/menu/player_inventory_menu.go rename to game/ui/menu/menu_player_inventory_menu.go index f66a62b..66f9aa6 100644 --- a/game/ui/menu/player_inventory_menu.go +++ b/game/ui/menu/menu_player_inventory_menu.go @@ -3,9 +3,8 @@ package menu import ( "fmt" "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" - "mvvasilev/last_light/game/item" - "mvvasilev/last_light/game/rpg" + "mvvasilev/last_light/game/model" + "mvvasilev/last_light/game/systems" "mvvasilev/last_light/game/ui" "github.com/gdamore/tcell/v2" @@ -14,7 +13,7 @@ import ( ) type PlayerInventoryMenu struct { - inventory *item.EquippedInventory + inventory *model.EquippedInventory inventoryMenu *ui.UIWindow armourLabel *ui.UILabel @@ -26,12 +25,11 @@ type PlayerInventoryMenu struct { inventoryGrid *engine.Grid playerItems *engine.ArbitraryDrawable selectedItem *engine.ArbitraryDrawable - help *ui.UILabel selectedInventorySlot engine.Position } -func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory, style tcell.Style, highlightStyle tcell.Style) *PlayerInventoryMenu { +func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventory, style tcell.Style, highlightStyle tcell.Style) *PlayerInventoryMenu { menu := new(PlayerInventoryMenu) menu.inventory = playerInventory @@ -94,25 +92,19 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory continue } - style := item.Type().Style() + style := item.Style() if isHighlighted { style = highlightStyle } - ui.CreateSingleLineUILabel( + menu.drawItemSlot( menu.inventoryGrid.Position().X()+1+x*4, menu.inventoryGrid.Position().Y()+y*2, - fmt.Sprintf("%03d", item.Quantity()), + item, style, - ).Draw(v) - - ui.CreateSingleLineUILabel( - menu.inventoryGrid.Position().X()+1+x*4, - menu.inventoryGrid.Position().Y()+1+y*2, - item.Type().Icon(), - style, - ).Draw(v) + v, + ) } } }) @@ -124,19 +116,30 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory return } - switch it := item.(type) { - case rpg.RPGItem: - ui.CreateUIRPGItem(x+2, y+14, it, style).Draw(v) - default: - ui.CreateUIBasicItem(x+2, y+14, it, style).Draw(v) - } + ui.CreateUIItem(x+2, y+14, item, style).Draw(v) }) - menu.help = ui.CreateSingleLineUILabel(x+2, y+22, "hjkl - move, x - drop, e - equip", style) - return menu } +func (pim *PlayerInventoryMenu) drawItemSlot(screenX, screenY int, item model.Item_V2, style tcell.Style, v views.View) { + if item.Quantifiable() != nil { + ui.CreateSingleLineUILabel( + screenX, + screenY, + fmt.Sprintf("%03d", item.Quantifiable().CurrentQuantity), + style, + ).Draw(v) + } + + ui.CreateSingleLineUILabel( + screenX, + screenY+1, + item.Icon(), + style, + ).Draw(v) +} + func (pim *PlayerInventoryMenu) MoveTo(x int, y int) { } @@ -149,7 +152,7 @@ func (pim *PlayerInventoryMenu) Size() engine.Size { return pim.inventoryMenu.Size() } -func (pim *PlayerInventoryMenu) Input(inputAction input.InputAction) { +func (pim *PlayerInventoryMenu) Input(inputAction systems.InputAction) { } @@ -178,5 +181,4 @@ func (pim *PlayerInventoryMenu) Draw(v views.View) { pim.inventoryGrid.Draw(v) pim.playerItems.Draw(v) pim.selectedItem.Draw(v) - pim.help.Draw(v) } diff --git a/game/ui/ui.go b/game/ui/ui.go index 42bcff4..c9e3354 100644 --- a/game/ui/ui.go +++ b/game/ui/ui.go @@ -2,14 +2,14 @@ package ui import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/systems" ) type UIElement interface { MoveTo(x, y int) Position() engine.Position Size() engine.Size - Input(inputAction input.InputAction) + Input(inputAction systems.InputAction) engine.Drawable } diff --git a/game/ui/dialog.go b/game/ui/ui_dialog.go similarity index 92% rename from game/ui/dialog.go rename to game/ui/ui_dialog.go index a3f0d76..f15178f 100644 --- a/game/ui/dialog.go +++ b/game/ui/ui_dialog.go @@ -2,7 +2,7 @@ package ui import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/systems" "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/views" @@ -91,13 +91,13 @@ func (d *UIDialog) Size() engine.Size { return d.window.Size() } -func (d *UIDialog) Input(inputAction input.InputAction) { - if inputAction == input.InputAction_Menu_HighlightLeft { +func (d *UIDialog) Input(inputAction systems.InputAction) { + if inputAction == systems.InputAction_Menu_HighlightLeft { if !d.yesBtn.IsHighlighted() { d.noBtn.Unhighlight() d.yesBtn.Highlight() } - } else if inputAction == input.InputAction_Menu_HighlightRight { + } else if inputAction == systems.InputAction_Menu_HighlightRight { if d.noBtn == nil { return } diff --git a/game/ui/event_logger.go b/game/ui/ui_event_logger.go similarity index 91% rename from game/ui/event_logger.go rename to game/ui/ui_event_logger.go index a395bf4..e39c66d 100644 --- a/game/ui/event_logger.go +++ b/game/ui/ui_event_logger.go @@ -2,7 +2,7 @@ package ui import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/systems" "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/views" @@ -40,7 +40,7 @@ func (uie *UIEventLog) Size() engine.Size { return uie.window.Size() } -func (uie *UIEventLog) Input(inputAction input.InputAction) { +func (uie *UIEventLog) Input(inputAction systems.InputAction) { } diff --git a/game/ui/health_bar.go b/game/ui/ui_health_bar.go similarity index 65% rename from game/ui/health_bar.go rename to game/ui/ui_health_bar.go index fc41104..cb53923 100644 --- a/game/ui/health_bar.go +++ b/game/ui/ui_health_bar.go @@ -4,7 +4,8 @@ import ( "fmt" "math" "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/model" + "mvvasilev/last_light/game/systems" "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/views" @@ -12,35 +13,22 @@ import ( ) type UIHealthBar struct { - id uuid.UUID - health int - maxHealth int + id uuid.UUID + player *model.Player_V2 window *UIWindow style tcell.Style } -// TODO: style for health bar fill -// TODO: 'HP' title -// TODO: test different percentages -func CreateHealthBar(x, y, w, h, health, maxHealth int, style tcell.Style) *UIHealthBar { +func CreateHealthBar(x, y, w, h int, player *model.Player_V2, style tcell.Style) *UIHealthBar { return &UIHealthBar{ - window: CreateWindow(x, y, w, h, "HP", style), - health: health, - maxHealth: maxHealth, - style: style, + window: CreateWindow(x, y, w, h, "HP", style), + player: player, + style: style, } } -func (uihp *UIHealthBar) SetHealth(health int) { - uihp.health = health -} - -func (uihp *UIHealthBar) SetMaxHealth(maxHealth int) { - uihp.maxHealth = maxHealth -} - func (uihp *UIHealthBar) MoveTo(x int, y int) { uihp.window.MoveTo(x, y) } @@ -53,7 +41,7 @@ func (uihp *UIHealthBar) Size() engine.Size { return uihp.window.Size() } -func (uihp *UIHealthBar) Input(inputAction input.InputAction) { +func (uihp *UIHealthBar) Input(inputAction systems.InputAction) { } func (uihp *UIHealthBar) UniqueId() uuid.UUID { @@ -67,7 +55,7 @@ func (uihp *UIHealthBar) Draw(v views.View) { stages := []rune{'█', '▓', '▒', '░'} // 0 = 1.0, 1 = 0.75, 2 = 0.5, 3 = 0.25 - percentage := (float64(w) - 2.0) * (float64(uihp.health) / float64(uihp.maxHealth)) + percentage := (float64(w) - 2.0) * (float64(uihp.player.HealthData().Health) / float64(uihp.player.HealthData().MaxHealth)) whole := math.Trunc(percentage) last := percentage - whole @@ -92,7 +80,7 @@ func (uihp *UIHealthBar) Draw(v views.View) { } } - hpText := fmt.Sprintf("%v/%v", uihp.health, uihp.maxHealth) + hpText := fmt.Sprintf("%v/%v", uihp.player.HealthData().Health, uihp.player.HealthData().MaxHealth) engine.DrawText( x+w/2-len(hpText)/2, diff --git a/game/ui/ui_item.go b/game/ui/ui_item.go new file mode 100644 index 0000000..640d6cb --- /dev/null +++ b/game/ui/ui_item.go @@ -0,0 +1,133 @@ +package ui + +import ( + "fmt" + "mvvasilev/last_light/engine" + "mvvasilev/last_light/game/model" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type UIItem struct { + id uuid.UUID + + item model.Item_V2 + + window UIWindow + + engine.Positioned + engine.Sized +} + +func CreateUIItem(x, y int, item model.Item_V2, style tcell.Style) *UIItem { + return &UIItem{ + id: uuid.New(), + item: item, + window: *CreateWindow(x, y, 33, 8, "Item", style), + Positioned: engine.WithPosition(engine.PositionAt(x, y)), + Sized: engine.WithSize(engine.SizeOf(33, 8)), + } +} + +func (uibi *UIItem) Input(e *tcell.EventKey) { +} + +func (uibi *UIItem) UniqueId() uuid.UUID { + return uibi.id +} + +func (uibi *UIItem) Draw(v views.View) { + uibi.window.Draw(v) + + if uibi.item.Named() != nil { + engine.DrawText(uibi.Position().X()+1, uibi.Position().Y()+1, uibi.item.Named().Name, uibi.item.Named().Style, v) + } + + if uibi.item.Described() != nil { + engine.DrawText(uibi.Position().X()+1, uibi.Position().Y()+2, uibi.item.Described().Description, uibi.item.Described().Style, v) + } + + if uibi.item.StatModifier() == nil { + return + } + + statModifiers := uibi.item.StatModifier().StatModifiers + + originalX, y := uibi.Position().XY() + x := originalX + 1 + y += 3 + + for i, sm := range statModifiers { + + drawRPGItemStatModifier(x, y, tcell.StyleDefault, v, &sm) + + x += 9 + 2 // each stat is 9 characters long, with 2 characters separating the stats + + // Only 3 stats per line + if i > 0 && (i+1)%3 == 0 { + x = originalX + 1 + y++ + } + } +} + +func drawRPGItemStatModifier(x, y int, style tcell.Style, view views.View, sm *model.StatModifier) { + + // 5 characters per stat name + // 1 separating character + // 3 characters for bonus ( including sign, modifiers are limited to -99 and +99) + + const SEPARATING_CHARACTER rune = ':' + + switch sm.Stat { + case model.Stat_Attributes_Strength: + engine.DrawText(x, y, "STR", style, view) + case model.Stat_Attributes_Dexterity: + engine.DrawText(x, y, "DEX", style, view) + case model.Stat_Attributes_Intelligence: + engine.DrawText(x, y, "INT", style, view) + case model.Stat_Attributes_Constitution: + engine.DrawText(x, y, "CON", style, view) + case model.Stat_PhysicalPrecisionBonus: + engine.DrawText(x, y, "pPrcs", style, view) + case model.Stat_EvasionBonus: + engine.DrawText(x, y, "Evasn", style, view) + case model.Stat_MagicPrecisionBonus: + engine.DrawText(x, y, "mPrcs", style, view) + case model.Stat_TotalPrecisionBonus: + engine.DrawText(x, y, "tPrcs", style, view) + case model.Stat_DamageBonus_Physical_Unarmed: + engine.DrawText(x, y, "Unrmd", style, view) + case model.Stat_DamageBonus_Physical_Slashing: + engine.DrawText(x, y, "Slshn", style, view) + case model.Stat_DamageBonus_Physical_Piercing: + engine.DrawText(x, y, "Prcng", style, view) + case model.Stat_DamageBonus_Physical_Bludgeoning: + engine.DrawText(x, y, "Bldgn", style, view) + case model.Stat_DamageBonus_Magic_Fire: + engine.DrawText(x, y, "Fire", style, view) + case model.Stat_DamageBonus_Magic_Cold: + engine.DrawText(x, y, "Cold", style, view) + case model.Stat_DamageBonus_Magic_Necrotic: + engine.DrawText(x, y, "Ncrtc", style, view) + case model.Stat_DamageBonus_Magic_Thunder: + engine.DrawText(x, y, "Thndr", style, view) + case model.Stat_DamageBonus_Magic_Acid: + engine.DrawText(x, y, "Acid", style, view) + case model.Stat_DamageBonus_Magic_Poison: + engine.DrawText(x, y, "Poisn", style, view) + case model.Stat_MaxHealthBonus: + engine.DrawText(x, y, "maxHP", style, view) + default: + } + + view.SetContent(x+5, y, SEPARATING_CHARACTER, nil, style) + + if sm.Bonus < 0 { + engine.DrawText(x+6, y, fmt.Sprintf("-%02d", -sm.Bonus), tcell.StyleDefault.Foreground(tcell.ColorIndianRed), view) + } else { + engine.DrawText(x+6, y, fmt.Sprintf("+%02d", sm.Bonus), tcell.StyleDefault.Foreground(tcell.ColorLime), view) + } +} diff --git a/game/ui/label.go b/game/ui/ui_label.go similarity index 93% rename from game/ui/label.go rename to game/ui/ui_label.go index a4c3813..6abbb0d 100644 --- a/game/ui/label.go +++ b/game/ui/ui_label.go @@ -2,7 +2,7 @@ package ui import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/systems" "unicode/utf8" "github.com/gdamore/tcell/v2" @@ -57,4 +57,4 @@ func (t *UILabel) Draw(v views.View) { t.text.Draw(v) } -func (t *UILabel) Input(inputAction input.InputAction) {} +func (t *UILabel) Input(inputAction systems.InputAction) {} diff --git a/game/ui/simple_button.go b/game/ui/ui_simple_button.go similarity index 95% rename from game/ui/simple_button.go rename to game/ui/ui_simple_button.go index 2302eff..79a05b7 100644 --- a/game/ui/simple_button.go +++ b/game/ui/ui_simple_button.go @@ -2,7 +2,7 @@ package ui import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/systems" "strings" "unicode/utf8" @@ -97,6 +97,6 @@ func (sb *UISimpleButton) Draw(v views.View) { sb.text.Draw(v) } -func (sb *UISimpleButton) Input(inputAction input.InputAction) { +func (sb *UISimpleButton) Input(inputAction systems.InputAction) { } diff --git a/game/ui/window.go b/game/ui/ui_window.go similarity index 91% rename from game/ui/window.go rename to game/ui/ui_window.go index c47184b..689cd16 100644 --- a/game/ui/window.go +++ b/game/ui/ui_window.go @@ -2,7 +2,7 @@ package ui import ( "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/systems" "unicode/utf8" "github.com/gdamore/tcell/v2" @@ -63,5 +63,5 @@ func (w *UIWindow) Draw(v views.View) { } } -func (w *UIWindow) Input(inputAction input.InputAction) { +func (w *UIWindow) Input(inputAction systems.InputAction) { } diff --git a/game/world/bsp_map.go b/game/world/bsp_map.go deleted file mode 100644 index 63c9280..0000000 --- a/game/world/bsp_map.go +++ /dev/null @@ -1,56 +0,0 @@ -package world - -import ( - "mvvasilev/last_light/engine" -) - -type BSPDungeonMap struct { - level *BasicMap - - playerSpawnPoint engine.Position - nextLevelStaircase engine.Position - rooms []engine.BoundingBox -} - -func (bsp *BSPDungeonMap) PlayerSpawnPoint() engine.Position { - return bsp.playerSpawnPoint -} - -func (bsp *BSPDungeonMap) NextLevelStaircasePosition() engine.Position { - return bsp.nextLevelStaircase -} - -func (bsp *BSPDungeonMap) Size() engine.Size { - return bsp.level.Size() -} - -func (bsp *BSPDungeonMap) SetTileAt(x int, y int, t Tile) Tile { - return bsp.level.SetTileAt(x, y, t) -} - -func (bsp *BSPDungeonMap) TileAt(x int, y int) Tile { - return bsp.level.TileAt(x, y) -} - -func (bsp *BSPDungeonMap) IsInBounds(x, y int) bool { - return bsp.level.IsInBounds(x, y) -} - -func (bsp *BSPDungeonMap) ExploredTileAt(x, y int) Tile { - return bsp.level.ExploredTileAt(x, y) -} - -func (bsp *BSPDungeonMap) MarkExplored(x, y int) { - bsp.level.MarkExplored(x, y) -} - -func (bsp *BSPDungeonMap) Tick(dt int64) { -} - -func (bsp *BSPDungeonMap) Rooms() []engine.BoundingBox { - return bsp.rooms -} - -func (bsp *BSPDungeonMap) PreviousLevelStaircasePosition() engine.Position { - return bsp.playerSpawnPoint -} diff --git a/game/world/dungeon.go b/game/world/dungeon.go deleted file mode 100644 index c7abb1d..0000000 --- a/game/world/dungeon.go +++ /dev/null @@ -1,307 +0,0 @@ -package world - -import ( - "math/rand" - "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/item" - "mvvasilev/last_light/game/npc" - "mvvasilev/last_light/game/rpg" - "slices" - - "github.com/google/uuid" -) - -type DungeonType int - -const ( - DungeonTypeBSP DungeonType = iota - DungeonTypeCaverns - DungeonTypeMine - DungeonTypeUndercity -) - -func randomDungeonType() DungeonType { - return DungeonType(rand.Intn(4)) -} - -type Dungeon struct { - levels []*DungeonLevel - - current int -} - -func CreateDungeon(width, height int, depth int) *Dungeon { - levels := make([]*DungeonLevel, 0, depth) - - for range depth { - levels = append(levels, CreateDungeonLevel(width, height, randomDungeonType())) - } - - return &Dungeon{ - levels: levels, - current: 0, - } -} - -func (d *Dungeon) CurrentLevel() *DungeonLevel { - return d.levels[d.current] -} - -func (d *Dungeon) MoveToNextLevel() (moved bool) { - if !d.HasNextLevel() { - return false - } - - d.current++ - - return true -} - -func (d *Dungeon) MoveToPreviousLevel() (moved bool) { - if !d.HasPreviousLevel() { - return false - } - - d.current-- - - return true -} - -func (d *Dungeon) NextLevel() *DungeonLevel { - if !d.HasNextLevel() { - return nil - } - - return d.levels[d.current+1] -} - -func (d *Dungeon) PreviousLevel() *DungeonLevel { - if !d.HasPreviousLevel() { - return nil - } - - return d.levels[d.current-1] -} - -func (d *Dungeon) HasPreviousLevel() bool { - return d.current-1 >= 0 -} - -func (d *Dungeon) HasNextLevel() bool { - return d.current+1 < len(d.levels) -} - -type DungeonLevel struct { - groundLevel interface { - Map - WithPlayerSpawnPoint - WithNextLevelStaircasePosition - WithPreviousLevelStaircasePosition - } - entityLevel *EntityMap - itemLevel Map - - multilevel Map -} - -func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLevel { - - genTable := rpg.CreateLootTable() - - genTable.Add(10, func() item.Item { - return item.CreateBasicItem(item.ItemTypeFish(), 1) - }) - - itemTypes := []rpg.RPGItemType{ - rpg.ItemTypeBow(), - rpg.ItemTypeLongsword(), - rpg.ItemTypeClub(), - rpg.ItemTypeDagger(), - rpg.ItemTypeHandaxe(), - rpg.ItemTypeJavelin(), - rpg.ItemTypeLightHammer(), - rpg.ItemTypeMace(), - rpg.ItemTypeQuarterstaff(), - rpg.ItemTypeSickle(), - rpg.ItemTypeSpear(), - } - - genTable.Add(1, func() item.Item { - itemType := itemTypes[rand.Intn(len(itemTypes))] - - rarities := []rpg.ItemRarity{ - rpg.ItemRarity_Common, - rpg.ItemRarity_Uncommon, - rpg.ItemRarity_Rare, - rpg.ItemRarity_Epic, - rpg.ItemRarity_Legendary, - } - - return rpg.GenerateItemOfTypeAndRarity(itemType, rarities[rand.Intn(len(rarities))]) - }) - - var groundLevel interface { - Map - WithRooms - WithPlayerSpawnPoint - WithNextLevelStaircasePosition - WithPreviousLevelStaircasePosition - } - - switch dungeonType { - case DungeonTypeBSP: - groundLevel = CreateBSPDungeonMap(width, height, 4) - default: - groundLevel = CreateBSPDungeonMap(width, height, 4) - } - - items := SpawnItems(groundLevel.Rooms(), 0.1, genTable, []engine.Position{ - groundLevel.NextLevelStaircasePosition(), - groundLevel.PlayerSpawnPoint(), - groundLevel.PreviousLevelStaircasePosition(), - }) - - itemLevel := CreateEmptyDungeonLevel(width, height) - - for _, it := range items { - if !groundLevel.TileAt(it.Position().XY()).Passable() { - continue - } - - itemLevel.SetTileAt(it.Position().X(), it.Position().Y(), it) - } - - d := &DungeonLevel{ - groundLevel: groundLevel, - entityLevel: CreateEntityMap(width, height), - itemLevel: itemLevel, - } - - d.multilevel = CreateMultilevelMap( - d.groundLevel, - d.itemLevel, - d.entityLevel, - ) - - return d -} - -func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTable *rpg.LootTable, forbiddenPositions []engine.Position) []Tile { - rooms := spawnableAreas - - itemTiles := make([]Tile, 0, 10) - - for _, r := range rooms { - maxItems := int(maxItemRatio * float32(r.Size().Area())) - - if maxItems < 1 { - continue - } - - numItems := rand.Intn(maxItems) - - for range numItems { - item := genTable.Generate() - - if item == nil { - continue - } - - pos := engine.PositionAt( - engine.RandInt(r.Position().X()+1, r.Position().X()+r.Size().Width()-1), - engine.RandInt(r.Position().Y()+1, r.Position().Y()+r.Size().Height()-1), - ) - - if slices.Contains(forbiddenPositions, pos) { - continue - } - - itemTiles = append(itemTiles, CreateItemTile(pos, item)) - } - } - - return itemTiles -} - -func (d *DungeonLevel) PlayerSpawnPoint() engine.Position { - return d.groundLevel.PlayerSpawnPoint() -} - -func (d *DungeonLevel) NextLevelStaircase() engine.Position { - return d.groundLevel.NextLevelStaircasePosition() -} - -func (d *DungeonLevel) PreviousLevelStaircase() engine.Position { - return d.groundLevel.PreviousLevelStaircasePosition() -} - -func (d *DungeonLevel) DropEntity(uuid uuid.UUID) { - d.entityLevel.DropEntity(uuid) -} - -func (d *DungeonLevel) AddEntity(entity npc.MovableEntity) { - d.entityLevel.AddEntity(entity) -} - -func (d *DungeonLevel) MoveEntity(uuid uuid.UUID, dx, dy int) { - d.entityLevel.MoveEntity(uuid, dx, dy) -} - -func (d *DungeonLevel) MoveEntityTo(uuid uuid.UUID, x, y int) { - d.entityLevel.MoveEntityTo(uuid, x, y) -} - -func (d *DungeonLevel) RemoveItemAt(x, y int) item.Item { - if !d.groundLevel.Size().Contains(x, y) { - return nil - } - - tile := d.itemLevel.TileAt(x, y) - itemTile, ok := tile.(*ItemTile) - - if !ok { - return nil - } - - d.itemLevel.SetTileAt(x, y, nil) - - return itemTile.Item() -} - -func (d *DungeonLevel) SetItemAt(x, y int, it item.Item) (success bool) { - if !d.TileAt(x, y).Passable() { - return false - } - - d.itemLevel.SetTileAt(x, y, CreateItemTile(engine.PositionAt(x, y), it)) - - return true -} - -func (d *DungeonLevel) TileAt(x, y int) Tile { - return d.multilevel.TileAt(x, y) -} - -func (d *DungeonLevel) IsTilePassable(x, y int) bool { - if !d.groundLevel.Size().Contains(x, y) { - return false - } - - return d.TileAt(x, y).Passable() -} - -func (d *DungeonLevel) EntityAt(x, y int) (e npc.MovableEntity) { - return d.entityLevel.EntityAt(x, y) -} - -func (d *DungeonLevel) IsGroundTileOpaque(x, y int) bool { - if !d.groundLevel.Size().Contains(x, y) { - return false - } - - return d.TileAt(x, y).Opaque() -} - -func (d *DungeonLevel) Flatten() Map { - return d.multilevel -} diff --git a/game/world/empty_map.go b/game/world/empty_map.go deleted file mode 100644 index 160ae73..0000000 --- a/game/world/empty_map.go +++ /dev/null @@ -1,50 +0,0 @@ -package world - -import "mvvasilev/last_light/engine" - -type EmptyDungeonMap struct { - level *BasicMap -} - -func (edl *EmptyDungeonMap) Size() engine.Size { - return edl.level.Size() -} - -func (edl *EmptyDungeonMap) SetTileAt(x int, y int, t Tile) Tile { - return edl.level.SetTileAt(x, y, t) -} - -func (edl *EmptyDungeonMap) TileAt(x int, y int) Tile { - return edl.level.TileAt(x, y) -} - -func (edl *EmptyDungeonMap) IsInBounds(x, y int) bool { - return edl.level.IsInBounds(x, y) -} - -func (edl *EmptyDungeonMap) Tick(dt int64) { - -} - -func (edl *EmptyDungeonMap) Rooms() []engine.BoundingBox { - rooms := make([]engine.BoundingBox, 1) - - rooms = append(rooms, engine.BoundingBox{ - Sized: engine.WithSize(edl.Size()), - Positioned: engine.WithPosition(engine.PositionAt(0, 0)), - }) - - return rooms -} - -func (edl *EmptyDungeonMap) PlayerSpawnPoint() engine.Position { - return engine.PositionAt(edl.Size().Width()/2, edl.Size().Height()/2) -} - -func (edl *EmptyDungeonMap) NextLevelStaircasePosition() engine.Position { - return engine.PositionAt(edl.Size().Width()/3, edl.Size().Height()/3) -} - -func (bsp *EmptyDungeonMap) PreviousLevelStaircasePosition() engine.Position { - return bsp.PlayerSpawnPoint() -} diff --git a/game/world/entity_map.go b/game/world/entity_map.go deleted file mode 100644 index c7ab689..0000000 --- a/game/world/entity_map.go +++ /dev/null @@ -1,136 +0,0 @@ -package world - -import ( - "maps" - "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/npc" - - "github.com/google/uuid" -) - -type EntityMap struct { - entities map[int]EntityTile - - engine.Sized -} - -func CreateEntityMap(width, height int) *EntityMap { - return &EntityMap{ - entities: make(map[int]EntityTile, 0), - Sized: engine.WithSize(engine.SizeOf(width, height)), - } -} - -func (em *EntityMap) SetTileAt(x int, y int, t Tile) Tile { - return nil - // if !em.FitsWithin(x, y) { - // return - // } - - // index := em.Size().AsArrayIndex(x, y) - - // TODO? May not be necessary -} - -func (em *EntityMap) FindEntityByUuid(uuid uuid.UUID) (key int, entity EntityTile) { - for i, e := range em.entities { - if e.Entity().UniqueId() == uuid { - return i, e - } - } - - return -1, nil -} - -func (em *EntityMap) AddEntity(entity npc.MovableEntity) { - if !em.FitsWithin(entity.Position().XY()) { - return - } - - key := em.Size().AsArrayIndex(entity.Position().XY()) - et := CreateBasicEntityTile(entity) - - em.entities[key] = et -} - -func (em *EntityMap) DropEntity(uuid uuid.UUID) { - maps.DeleteFunc(em.entities, func(i int, et EntityTile) bool { - return et.Entity().UniqueId() == uuid - }) -} - -func (em *EntityMap) MoveEntity(uuid uuid.UUID, dx, dy int) { - oldKey, e := em.FindEntityByUuid(uuid) - - if e == nil { - return - } - - if !em.FitsWithin(e.Entity().Position().WithOffset(dx, dy).XY()) { - return - } - - delete(em.entities, oldKey) - - newPos := e.Entity().Position().WithOffset(dx, dy) - e.Entity().MoveTo(newPos) - - newKey := em.Size().AsArrayIndex(e.Entity().Position().XY()) - - em.entities[newKey] = e -} - -func (em *EntityMap) MoveEntityTo(uuid uuid.UUID, x, y int) { - oldKey, e := em.FindEntityByUuid(uuid) - - if e == nil { - return - } - - if !em.FitsWithin(x, y) { - return - } - - delete(em.entities, oldKey) - - e.Entity().MoveTo(engine.PositionAt(x, y)) - - newKey := em.Size().AsArrayIndex(e.Entity().Position().XY()) - - em.entities[newKey] = e -} - -func (em *EntityMap) TileAt(x int, y int) Tile { - if !em.FitsWithin(x, y) { - return CreateStaticTile(x, y, TileTypeVoid()) - } - - key := em.Size().AsArrayIndex(x, y) - - return em.entities[key] -} - -func (em *EntityMap) EntityAt(x, y int) (ent npc.MovableEntity) { - tile := em.TileAt(x, y) - - if tile == nil { - return nil - } - - return tile.(EntityTile).Entity() -} - -func (em *EntityMap) IsInBounds(x, y int) bool { - return em.FitsWithin(x, y) -} - -func (em *EntityMap) MarkExplored(x, y int) { - -} - -func (em *EntityMap) ExploredTileAt(x, y int) Tile { - return CreateStaticTile(x, y, TileTypeVoid()) -} - -func (em *EntityMap) Tick(dt int64) { -} diff --git a/game/world/generate_empty_map.go b/game/world/generate_empty_map.go deleted file mode 100644 index 61fe5ef..0000000 --- a/game/world/generate_empty_map.go +++ /dev/null @@ -1,13 +0,0 @@ -package world - -import "github.com/gdamore/tcell/v2" - -func CreateEmptyDungeonLevel(width, height int) *BasicMap { - tiles := make([][]Tile, height) - - for h := range height { - tiles[h] = make([]Tile, width) - } - - return CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey)) -} diff --git a/game/world/map.go b/game/world/map.go deleted file mode 100644 index c2b7c9d..0000000 --- a/game/world/map.go +++ /dev/null @@ -1,107 +0,0 @@ -package world - -import ( - "mvvasilev/last_light/engine" - - "github.com/gdamore/tcell/v2" -) - -type Map interface { - Size() engine.Size - SetTileAt(x, y int, t Tile) Tile - TileAt(x, y int) Tile - IsInBounds(x, y int) bool - ExploredTileAt(x, y int) Tile - MarkExplored(x, y int) - Tick(dt int64) -} - -type WithPlayerSpawnPoint interface { - PlayerSpawnPoint() engine.Position -} - -type WithRooms interface { - Rooms() []engine.BoundingBox -} - -type WithNextLevelStaircasePosition interface { - NextLevelStaircasePosition() engine.Position -} - -type WithPreviousLevelStaircasePosition interface { - PreviousLevelStaircasePosition() engine.Position -} - -type BasicMap struct { - tiles [][]Tile - exploredTiles map[engine.Position]Tile - - exploredStyle tcell.Style -} - -func CreateBasicMap(tiles [][]Tile, exploredStyle tcell.Style) *BasicMap { - bm := new(BasicMap) - - bm.tiles = tiles - bm.exploredTiles = make(map[engine.Position]Tile, 0) - bm.exploredStyle = exploredStyle - - return bm -} - -func (bm *BasicMap) Tick(dt int64) { -} - -func (bm *BasicMap) Size() engine.Size { - return engine.SizeOf(len(bm.tiles[0]), len(bm.tiles)) -} - -func (bm *BasicMap) SetTileAt(x int, y int, t Tile) Tile { - if !bm.IsInBounds(x, y) { - return CreateStaticTile(x, y, TileTypeVoid()) - } - - bm.tiles[y][x] = t - - return bm.tiles[y][x] -} - -func (bm *BasicMap) TileAt(x int, y int) Tile { - if !bm.IsInBounds(x, y) { - return CreateStaticTile(x, y, TileTypeVoid()) - } - - tile := bm.tiles[y][x] - - if tile == nil { - return CreateStaticTile(x, y, TileTypeVoid()) - } - - return tile -} - -func (bm *BasicMap) IsInBounds(x, y int) bool { - if x < 0 || y < 0 { - return false - } - - if x >= bm.Size().Width() || y >= bm.Size().Height() { - return false - } - - return true -} - -func (bm *BasicMap) ExploredTileAt(x, y int) Tile { - return bm.exploredTiles[engine.PositionAt(x, y)] -} - -func (bm *BasicMap) MarkExplored(x, y int) { - if !bm.IsInBounds(x, y) { - return - } - - tile := bm.TileAt(x, y) - - bm.exploredTiles[engine.PositionAt(x, y)] = CreateStaticTileWithStyleOverride(tile.Position().X(), tile.Position().Y(), tile.Type(), bm.exploredStyle) -} diff --git a/game/world/multilevel_map.go b/game/world/multilevel_map.go deleted file mode 100644 index f4ddafc..0000000 --- a/game/world/multilevel_map.go +++ /dev/null @@ -1,127 +0,0 @@ -package world - -import "mvvasilev/last_light/engine" - -type MultilevelMap struct { - layers []Map -} - -func CreateMultilevelMap(maps ...Map) *MultilevelMap { - m := new(MultilevelMap) - - m.layers = maps - - return m -} - -func (mm *MultilevelMap) Size() engine.Size { - if len(mm.layers) == 0 { - return engine.SizeOf(0, 0) - } - - return mm.layers[0].Size() -} - -func (mm *MultilevelMap) SetTileAt(x, y int, t Tile) Tile { - return mm.layers[0].SetTileAt(x, y, t) -} - -func (mm *MultilevelMap) UnsetTileAtHeight(x, y, height int) { - if len(mm.layers) < height { - return - } - - mm.layers[height].SetTileAt(x, y, nil) -} - -func (mm *MultilevelMap) SetTileAtHeight(x, y, height int, t Tile) { - if len(mm.layers) < height { - return - } - - mm.layers[height].SetTileAt(x, y, t) -} - -func (mm *MultilevelMap) CollectTilesAt(x, y int, filter func(t Tile) bool) []Tile { - tiles := make([]Tile, len(mm.layers)) - - if !mm.IsInBounds(x, y) { - return tiles - } - - for i := len(mm.layers) - 1; i >= 0; i-- { - tile := mm.layers[i].TileAt(x, y) - - if tile != nil && !tile.Transparent() && filter(tile) { - tiles = append(tiles, tile) - } - - } - - return tiles -} - -func (mm *MultilevelMap) TileAt(x int, y int) Tile { - if !mm.IsInBounds(x, y) { - return CreateStaticTile(x, y, TileTypeVoid()) - } - - for i := len(mm.layers) - 1; i >= 0; i-- { - tile := mm.layers[i].TileAt(x, y) - - if tile != nil && !tile.Transparent() { - return tile - } - - } - - return CreateStaticTile(x, y, TileTypeVoid()) -} - -func (mm *MultilevelMap) IsInBounds(x, y int) bool { - if x < 0 || y < 0 { - return false - } - - if x >= mm.Size().Width() || y >= mm.Size().Height() { - return false - } - - return true -} - -func (mm *MultilevelMap) MarkExplored(x, y int) { - for _, m := range mm.layers { - m.MarkExplored(x, y) - } -} - -func (mm *MultilevelMap) ExploredTileAt(x, y int) Tile { - for i := len(mm.layers) - 1; i >= 0; i-- { - tile := mm.layers[i].ExploredTileAt(x, y) - - if tile != nil && !tile.Transparent() { - return tile - } - } - - return CreateStaticTile(x, y, TileTypeVoid()) -} - -func (mm *MultilevelMap) TileAtHeight(x, y, height int) Tile { - if !mm.IsInBounds(x, y) { - return CreateStaticTile(x, y, TileTypeVoid()) - } - - if height > len(mm.layers)-1 { - return CreateStaticTile(x, y, TileTypeVoid()) - } - - return mm.layers[height].TileAt(x, y) -} - -func (mm *MultilevelMap) Tick(dt int64) { - for _, l := range mm.layers { - l.Tick(dt) - } -} diff --git a/game/world/tile.go b/game/world/tile.go deleted file mode 100644 index 48b8f31..0000000 --- a/game/world/tile.go +++ /dev/null @@ -1,274 +0,0 @@ -package world - -import ( - "mvvasilev/last_light/engine" - "mvvasilev/last_light/game/item" - "mvvasilev/last_light/game/npc" - - "github.com/gdamore/tcell/v2" -) - -type Material uint - -const ( - MaterialGround Material = iota - MaterialRock - MaterialWall - MaterialGrass - MaterialVoid - MaterialClosedDoor - MaterialOpenDoor - MaterialStaircaseDown - MaterialStaircaseUp -) - -type TileType struct { - Material Material - Passable bool - Presentation rune - Transparent bool - Opaque bool - Style tcell.Style -} - -func TileTypeGround() TileType { - return TileType{ - Material: MaterialGround, - Passable: true, - Presentation: '.', - Transparent: false, - Opaque: false, - Style: tcell.StyleDefault, - } -} - -func TileTypeRock() TileType { - return TileType{ - Material: MaterialRock, - Passable: false, - Presentation: '█', - Transparent: false, - Opaque: true, - Style: tcell.StyleDefault, - } -} - -func TileTypeGrass() TileType { - return TileType{ - Material: MaterialGrass, - Passable: true, - Presentation: ',', - Transparent: false, - Opaque: false, - Style: tcell.StyleDefault, - } -} - -func TileTypeVoid() TileType { - return TileType{ - Material: MaterialVoid, - Passable: false, - Presentation: ' ', - Transparent: true, - Opaque: true, - Style: tcell.StyleDefault, - } -} - -func TileTypeWall() TileType { - return TileType{ - Material: MaterialWall, - Passable: false, - Presentation: '#', - Transparent: false, - Opaque: true, - Style: tcell.StyleDefault.Background(tcell.ColorGray), - } -} - -func TileTypeClosedDoor() TileType { - return TileType{ - Material: MaterialClosedDoor, - Passable: false, - Transparent: false, - Presentation: '[', - Opaque: true, - Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue).Background(tcell.ColorSaddleBrown), - } -} - -func TileTypeOpenDoor() TileType { - return TileType{ - Material: MaterialClosedDoor, - Passable: false, - Transparent: false, - Presentation: '_', - Opaque: false, - Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue), - } -} - -func TileTypeStaircaseDown() TileType { - return TileType{ - Material: MaterialStaircaseDown, - Passable: true, - Transparent: false, - Presentation: '≡', - Opaque: false, - Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold), - } -} - -func TileTypeStaircaseUp() TileType { - return TileType{ - Material: MaterialStaircaseUp, - Passable: true, - Transparent: false, - Presentation: '^', - Opaque: false, - Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold), - } -} - -type Tile interface { - Position() engine.Position - Presentation() (rune, tcell.Style) - Passable() bool - Transparent() bool - Opaque() bool - Type() TileType -} - -type StaticTile struct { - position engine.Position - t TileType - - style tcell.Style -} - -func CreateStaticTile(x, y int, t TileType) Tile { - st := new(StaticTile) - - st.position = engine.PositionAt(x, y) - st.t = t - st.style = t.Style - - return st -} - -func CreateStaticTileWithStyleOverride(x, y int, t TileType, style tcell.Style) Tile { - return &StaticTile{ - position: engine.PositionAt(x, y), - t: t, - style: style, - } -} - -func (st *StaticTile) Position() engine.Position { - return st.position -} - -func (st *StaticTile) Presentation() (rune, tcell.Style) { - return st.t.Presentation, st.style -} - -func (st *StaticTile) Passable() bool { - return st.t.Passable -} - -func (st *StaticTile) Transparent() bool { - return st.t.Transparent -} - -func (st *StaticTile) Opaque() bool { - return st.t.Opaque -} - -func (st *StaticTile) Type() TileType { - return st.t -} - -type ItemTile struct { - position engine.Position - item item.Item -} - -func CreateItemTile(position engine.Position, item item.Item) *ItemTile { - it := new(ItemTile) - - it.position = position - it.item = item - - return it -} - -func (it *ItemTile) Item() item.Item { - return it.item -} - -func (it *ItemTile) Position() engine.Position { - return it.position -} - -func (it *ItemTile) Presentation() (rune, tcell.Style) { - return it.item.Type().TileIcon(), it.item.Type().Style() -} - -func (it *ItemTile) Passable() bool { - return true -} - -func (it *ItemTile) Transparent() bool { - return false -} - -func (it *ItemTile) Opaque() bool { - return false -} - -func (it *ItemTile) Type() TileType { - return TileType{} -} - -type EntityTile interface { - Entity() npc.MovableEntity - Tile -} - -type BasicEntityTile struct { - entity npc.MovableEntity -} - -func CreateBasicEntityTile(entity npc.MovableEntity) *BasicEntityTile { - return &BasicEntityTile{ - entity: entity, - } -} - -func (bet *BasicEntityTile) Entity() npc.MovableEntity { - return bet.entity -} - -func (bet *BasicEntityTile) Position() engine.Position { - return bet.entity.Position() -} - -func (bet *BasicEntityTile) Presentation() (rune, tcell.Style) { - return bet.entity.Presentation() -} - -func (bet *BasicEntityTile) Passable() bool { - return false -} - -func (bet *BasicEntityTile) Transparent() bool { - return false -} - -func (bet *BasicEntityTile) Opaque() bool { - return false -} - -func (bet *BasicEntityTile) Type() TileType { - return TileType{} -}