small rewrite

This commit is contained in:
Miroslav Vasilev 2024-06-06 23:17:22 +03:00
parent 6bcb59867e
commit ec51edb7c0
77 changed files with 3407 additions and 2522 deletions

View file

@ -114,6 +114,14 @@ func (s Size) Contains(x, y int) bool {
return 0 <= x && x < s.width && 0 <= y && y < s.height 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 { func LimitIncrement(i int, limit int) int {
if (i + 1) > limit { if (i + 1) > limit {
return i return i
@ -122,6 +130,14 @@ func LimitIncrement(i int, limit int) int {
return i + 1 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 { func LimitDecrement(i int, limit int) int {
if (i - 1) < limit { if (i - 1) < limit {
return i return i

View file

@ -2,16 +2,15 @@ package game
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/state" "mvvasilev/last_light/game/state"
"mvvasilev/last_light/game/turns" "mvvasilev/last_light/game/systems"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
type Game struct { type Game struct {
turnSystem *turns.TurnSystem turnSystem *systems.TurnSystem
inputSystem *input.InputSystem inputSystem *systems.InputSystem
state state.GameState state state.GameState
@ -21,9 +20,9 @@ type Game struct {
func CreateGame() *Game { func CreateGame() *Game {
game := new(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) game.state = state.CreateMainMenuState(game.turnSystem, game.inputSystem)

View file

@ -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]
}

View file

@ -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
}

View file

@ -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,
}
}

226
game/model/entity.go Normal file
View file

@ -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,
}
}
}

1
game/model/entity_npc.go Normal file
View file

@ -0,0 +1 @@
package model

View file

@ -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()
}

View file

@ -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)
// }
// }

136
game/model/inventory.go Normal file
View file

@ -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]
}

View file

@ -1,4 +1,4 @@
package item package model
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
@ -18,13 +18,13 @@ const (
) )
type EquippedInventory struct { type EquippedInventory struct {
offHand Item offHand Item_V2
dominantHand Item dominantHand Item_V2
head Item head Item_V2
chestplate Item chestplate Item_V2
leggings Item leggings Item_V2
shoes Item shoes Item_V2
*BasicInventory *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 { switch slot {
case EquippedSlotOffhand: case EquippedSlotOffhand:
return ei.offHand 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 { switch slot {
case EquippedSlotOffhand: case EquippedSlotOffhand:
ei.offHand = item ei.offHand = item

308
game/model/item.go Normal file
View file

@ -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
// }

486
game/model/items.go Normal file
View file

@ -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,
// }
// }

116
game/model/rpg_entity.go Normal file
View file

@ -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)
// }

View file

@ -1,9 +1,8 @@
package rpg package model
import ( import (
"math/rand" "math/rand"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item"
"slices" "slices"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
@ -12,11 +11,9 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
// TODO: figure out event logging. Need to inform the player of the things that are happening...
const MaxNumberOfModifiers = 6 const MaxNumberOfModifiers = 6
type ItemSupplier func() item.Item type ItemSupplier func() Item_V2
type LootTable struct { type LootTable struct {
table []ItemSupplier 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))]() return igt.table[rand.Intn(len(igt.table))]()
} }
@ -118,25 +115,25 @@ func randomSuffix() string {
return suffixes[rand.Intn(len(suffixes))] 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 { switch rarity {
case ItemRarity_Common: case ItemRarity_Common:
return itemType.Name(), tcell.StyleDefault return existingItemName, tcell.StyleDefault
case ItemRarity_Uncommon: case ItemRarity_Uncommon:
return randomAdjective() + " " + itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorLime) return randomAdjective() + " " + existingItemName, tcell.StyleDefault.Foreground(tcell.ColorLime)
case ItemRarity_Rare: case ItemRarity_Rare:
return itemType.Name() + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorBlue) return existingItemName + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorBlue)
case ItemRarity_Epic: 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: 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: default:
return itemType.Name(), tcell.StyleDefault return existingItemName, tcell.StyleDefault
} }
} }
func randomStat(metaItemTypes []RPGItemMetaType) Stat { func randomStat(metaItemTypes []ItemMetaType) Stat {
stats := make(map[RPGItemMetaType][]Stat, 0) stats := make(map[ItemMetaType][]Stat, 0)
stats[MetaItemType_Weapon] = []Stat{ stats[MetaItemType_Weapon] = []Stat{
Stat_Attributes_Strength, Stat_Attributes_Strength,
@ -183,6 +180,12 @@ func randomStat(metaItemTypes []RPGItemMetaType) Stat {
Stat_DamageBonus_Magic_Thunder, Stat_DamageBonus_Magic_Thunder,
Stat_DamageBonus_Magic_Acid, Stat_DamageBonus_Magic_Acid,
Stat_DamageBonus_Magic_Poison, 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{ stats[MetaItemType_Physical_Armour] = []Stat{
@ -190,6 +193,10 @@ func randomStat(metaItemTypes []RPGItemMetaType) Stat {
Stat_DamageBonus_Physical_Slashing, Stat_DamageBonus_Physical_Slashing,
Stat_DamageBonus_Physical_Piercing, Stat_DamageBonus_Physical_Piercing,
Stat_DamageBonus_Physical_Bludgeoning, 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) possibleStats := make([]Stat, 0, 10)
@ -201,7 +208,7 @@ func randomStat(metaItemTypes []RPGItemMetaType) Stat {
return slices.Compact(possibleStats)[rand.Intn(len(stats))] 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) points := pointPerRarity(rarity)
modifiers := make(map[Stat]*StatModifier, 0) modifiers := make(map[Stat]*StatModifier, 0)
@ -218,7 +225,7 @@ func generateItemStatModifiers(itemType RPGItemType, rarity ItemRarity) []StatMo
continue continue
} }
stat := randomStat(itemType.MetaTypes()) stat := randomStat(itemMetaTypes)
existingForStat := modifiers[stat] 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 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. // Each stat modifier consumes points. The higher the stat bonus, the more points it consumes.
func GenerateItemOfTypeAndRarity(itemType RPGItemType, rarity ItemRarity) RPGItem { func GenerateItemOfTypeAndRarity(prototype Item_V2, rarity ItemRarity) Item_V2 {
// points := pointPerRarity(rarity) if prototype.Named() == nil {
name, style := generateItemName(itemType, rarity) return prototype
}
return CreateRPGItem( if prototype.MetaTypes() == nil {
name, return prototype
style, }
itemType,
generateItemStatModifiers(itemType, rarity), 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),
) )
} }

73
game/model/rpg_items.go Normal file
View file

@ -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,
// ),
// }
// }

View file

@ -1,4 +1,4 @@
package rpg package model
import ( import (
"math/rand" "math/rand"
@ -34,6 +34,18 @@ const (
Stat_DamageBonus_Magic_Acid Stat = 120 Stat_DamageBonus_Magic_Acid Stat = 120
Stat_DamageBonus_Magic_Poison Stat = 130 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 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 { func LuckRoll() int {
return RollD10(1) return RollD10(1)
} }
func TotalModifierForStat(entity RPGEntity, stat Stat) int { func TotalModifierForStat(stats *Item_StatModifierComponent, stat Stat) int {
agg := 0 agg := 0
for _, m := range entity.CollectModifiersForStat(stat) { for _, m := range stats.StatModifiers {
if m.Stat == stat {
agg += m.Bonus agg += m.Bonus
} }
}
return agg return agg
} }
func StatValue(entity RPGEntity, stat Stat) int { func statValue(stats *Entity_StatsHolderComponent, stat Stat) int {
return entity.BaseStat(stat) + TotalModifierForStat(entity, stat) return stats.BaseStats[stat]
} }
// Base Max Health is determined from constitution: // Base Max Health is determined from constitution:
// 5*Constitution + Max Health Bonus // 5*Constitution + Max Health Bonus
func BaseMaxHealth(entity RPGEntity) int { func BaseMaxHealth(entity Entity_V2) int {
return 5*StatValue(entity, Stat_Attributes_Constitution) + StatValue(entity, Stat_MaxHealthBonus) stats := entity.Stats()
if stats == nil {
return 0
}
return 5*statValue(stats, Stat_Attributes_Constitution) + statValue(stats, Stat_MaxHealthBonus)
} }
// Dexterity + Evasion bonus + luck roll // Dexterity + Evasion bonus + luck roll
func EvasionRoll(victim RPGEntity) int { func EvasionRoll(victim Entity_V2) int {
return StatValue(victim, Stat_Attributes_Dexterity) + StatValue(victim, Stat_EvasionBonus) + LuckRoll() 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 // Strength + Precision bonus ( melee + total ) + luck roll
func PhysicalPrecisionRoll(attacker RPGEntity) int { func PhysicalPrecisionRoll(attacker Entity_V2) int {
return StatValue(attacker, Stat_Attributes_Strength) + StatValue(attacker, Stat_PhysicalPrecisionBonus) + StatValue(attacker, Stat_TotalPrecisionBonus) + LuckRoll() 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 // Intelligence + Precision bonus ( magic + total ) + luck roll
func MagicPrecisionRoll(attacker RPGEntity) int { func MagicPrecisionRoll(attacker Entity_V2) int {
return StatValue(attacker, Stat_Attributes_Intelligence) + StatValue(attacker, Stat_MagicPrecisionBonus) + StatValue(attacker, Stat_TotalPrecisionBonus) + LuckRoll() 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 // 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)) return hitRoll(EvasionRoll(victim), MagicPrecisionRoll(attacker))
} }
// true = hit lands, false = hit does not land // 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) evasion = EvasionRoll(victim)
precision = PhysicalPrecisionRoll(attacker) precision = PhysicalPrecisionRoll(attacker)
hit = hitRoll(evasion, precision) hit = hitRoll(evasion, precision)
@ -271,21 +330,30 @@ func hitRoll(evasionRoll, precisionRoll int) bool {
return evasionRoll < precisionRoll return evasionRoll < precisionRoll
} }
func UnarmedDamage(attacker RPGEntity) int { func UnarmedDamage(attacker Entity_V2) int {
return RollD4(1) + StatValue(attacker, Stat_DamageBonus_Physical_Unarmed) 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) { func PhysicalWeaponDamage(attacker Entity_V2, weapon Item_V2, victim Entity_V2) (totalDamage int, dmgType DamageType) {
totalDamage, dmgType = weapon.RPGType().RollDamage()(victim, attacker) if attacker.Stats() == nil || weapon.Damaging() == nil || victim.Stats() == nil {
return 0, DamageType_Physical_Unarmed
}
totalDamage, dmgType = weapon.Damaging().DamageRoll()
bonusDmgStat := DamageTypeToBonusStat(dmgType) bonusDmgStat := DamageTypeToBonusStat(dmgType)
dmgResistStat := DamageTypeToResistanceStat(dmgType)
totalDamage = totalDamage + StatValue(attacker, bonusDmgStat) totalDamage = totalDamage + statValue(attacker.Stats(), bonusDmgStat) - statValue(victim.Stats(), dmgResistStat)
return 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) hit, evasionRoll, precisionRoll = PhysicalHitRoll(attacker, victim)
if !hit { if !hit {
@ -298,14 +366,14 @@ func UnarmedAttack(attacker RPGEntity, victim RPGEntity) (hit bool, precisionRol
return 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) hit, evasionRoll, precisionRoll = PhysicalHitRoll(attacker, victim)
if !hit { if !hit {
return return
} }
damage, damageType = PhysicalWeaponDamange(attacker, weapon, victim) damage, damageType = PhysicalWeaponDamage(attacker, weapon, victim)
return return
} }

328
game/model/world_dungeon.go Normal file
View file

@ -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()
}

View file

@ -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()
// }

View file

@ -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) {
// }

View file

@ -1,4 +1,4 @@
package world package model
import ( import (
"math/rand" "math/rand"
@ -27,7 +27,7 @@ type bspNode struct {
splitDir splitDirection splitDir splitDirection
} }
func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap { func CreateBSPDungeonMap(width, height int, numSplits int) Map_V2 {
root := new(bspNode) root := new(bspNode)
root.origin = engine.PositionAt(0, 0) root.origin = engine.PositionAt(0, 0)
@ -35,10 +35,10 @@ func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap {
split(root, numSplits) split(root, numSplits)
tiles := make([][]Tile, height) tiles := make([][]Tile_V2, height)
for h := range height { for h := range height {
tiles[h] = make([]Tile, width) tiles[h] = make([]Tile_V2, width)
} }
rooms := make([]engine.BoundingBox, 0, numSplits*numSplits) 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) spawnRoom := findRoom(root.left)
staircaseRoom := findRoom(root.right) staircaseRoom := findRoom(root.right)
bsp.rooms = rooms playerPos := engine.PositionAt(
bsp.level = CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey))
bsp.playerSpawnPoint = engine.PositionAt(
spawnRoom.Position().X()+spawnRoom.Size().Width()/2, spawnRoom.Position().X()+spawnRoom.Size().Width()/2,
spawnRoom.Position().Y()+spawnRoom.Size().Height()/2, spawnRoom.Position().Y()+spawnRoom.Size().Height()/2,
) )
bsp.nextLevelStaircase = engine.PositionAt( 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().X()+staircaseRoom.Size().Width()/2,
staircaseRoom.Position().Y()+staircaseRoom.Size().Height()/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())) Map_SetTileAt(newBsp, newBsp.NextLevelStaircase().Position.X(), newBsp.NextLevelStaircase().Position.Y(), Tile_StaircaseDown())
bsp.level.SetTileAt(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), CreateStaticTile(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), TileTypeStaircaseUp())) 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 { 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 { switch direction {
case splitDirectionHorizontal: case splitDirectionHorizontal:
xMidPoint := (from.X() + to.X()) / 2 xMidPoint := (from.X() + to.X()) / 2
@ -206,7 +228,7 @@ func split(parent *bspNode, numSplits int) {
split(parent.right, numSplits-1) 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 { if x1 > x2 {
tx := x2 tx := x2
x2 = x1 x2 = x1
@ -222,7 +244,7 @@ func horizontalTunnel(tiles [][]Tile, x1, x2, y int) {
continue continue
} }
tiles[y][x] = CreateStaticTile(x, y, TileTypeGround()) tiles[y][x] = Tile_Ground()
placeWallAtIfNotPassable(tiles, x, y-1) placeWallAtIfNotPassable(tiles, x, y-1)
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) 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 { if y1 > y2 {
ty := y2 ty := y2
y2 = y1 y2 = y1
@ -249,7 +271,7 @@ func verticalTunnel(tiles [][]Tile, y1, y2, x int) {
continue continue
} }
tiles[y][x] = CreateStaticTile(x, y, TileTypeGround()) tiles[y][x] = Tile_Ground()
placeWallAtIfNotPassable(tiles, x-1, y) placeWallAtIfNotPassable(tiles, x-1, y)
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) 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() { if tiles[y][x] != nil && tiles[y][x].Passable() {
return 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() width := room.Size().Width()
height := room.Size().Height() height := room.Size().Height()
x := room.Position().X() x := room.Position().X()
y := room.Position().Y() y := room.Position().Y()
for w := x; w < x+width+1; w++ { for w := x; w < x+width+1; w++ {
tiles[y][w] = CreateStaticTile(w, y, TileTypeWall()) tiles[y][w] = Tile_Wall()
tiles[y+height][w] = CreateStaticTile(w, y+height, TileTypeWall()) tiles[y+height][w] = Tile_Wall()
} }
for h := y; h < y+height+1; h++ { for h := y; h < y+height+1; h++ {
tiles[h][x] = CreateStaticTile(x, h, TileTypeWall()) tiles[h][x] = Tile_Wall()
tiles[h][x+width] = CreateStaticTile(x+width, h, TileTypeWall()) tiles[h][x+width] = Tile_Wall()
} }
for h := y + 1; h < y+height; h++ { for h := y + 1; h < y+height; h++ {
for w := x + 1; w < x+width; w++ { for w := x + 1; w < x+width; w++ {
tiles[h][w] = CreateStaticTile(w, h, TileTypeGround()) tiles[h][w] = Tile_Ground()
} }
} }
} }

View file

@ -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))
// }

188
game/model/world_map.go Normal file
View file

@ -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(),
}
}

View file

@ -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)
// }
// }

368
game/model/world_tile.go Normal file
View file

@ -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{}
// }

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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,
),
}
}

View file

@ -2,9 +2,8 @@ package state
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/rpg" "mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui/menu" "mvvasilev/last_light/game/ui/menu"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -16,8 +15,8 @@ const (
) )
type CharacterCreationState struct { type CharacterCreationState struct {
turnSystem *turns.TurnSystem turnSystem *systems.TurnSystem
inputSystem *input.InputSystem inputSystem *systems.InputSystem
startGame bool startGame bool
@ -25,26 +24,26 @@ type CharacterCreationState struct {
ccMenu *menu.CharacterCreationMenu ccMenu *menu.CharacterCreationMenu
} }
func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *CharacterCreationState { func CreateCharacterCreationState(turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem) *CharacterCreationState {
menuState := &menu.CharacterCreationMenuState{ menuState := &menu.CharacterCreationMenuState{
AvailablePoints: 21, AvailablePoints: 21,
CurrentHighlight: 0, CurrentHighlight: 0,
Stats: []*menu.StatState{ Stats: []*menu.StatState{
{ {
Stat: rpg.Stat_Attributes_Strength, Stat: model.Stat_Attributes_Strength,
Value: 1, Value: 1,
}, },
{ {
Stat: rpg.Stat_Attributes_Dexterity, Stat: model.Stat_Attributes_Dexterity,
Value: 1, Value: 1,
}, },
{ {
Stat: rpg.Stat_Attributes_Intelligence, Stat: model.Stat_Attributes_Intelligence,
Value: 1, Value: 1,
}, },
{ {
Stat: rpg.Stat_Attributes_Constitution, Stat: model.Stat_Attributes_Constitution,
Value: 1, Value: 1,
}, },
}, },
@ -58,7 +57,12 @@ func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *inp
} }
ccs.menuState.RandomizeCharacter = func() { 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.AvailablePoints = 0
ccs.menuState.Stats = []*menu.StatState{} ccs.menuState.Stats = []*menu.StatState{}
@ -84,8 +88,8 @@ func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *inp
return ccs return ccs
} }
func (ccs *CharacterCreationState) InputContext() input.Context { func (ccs *CharacterCreationState) InputContext() systems.InputContext {
return input.InputContext_Menu return systems.InputContext_Menu
} }
func (ccs *CharacterCreationState) IncreaseStatValue() { func (ccs *CharacterCreationState) IncreaseStatValue() {
@ -125,7 +129,7 @@ func (ccs *CharacterCreationState) DecreaseStatValue() {
func (ccs *CharacterCreationState) OnTick(dt int64) GameState { func (ccs *CharacterCreationState) OnTick(dt int64) GameState {
if ccs.startGame { if ccs.startGame {
stats := map[rpg.Stat]int{} stats := map[model.Stat]int{}
for _, s := range ccs.menuState.Stats { for _, s := range ccs.menuState.Stats {
stats[s.Stat] = s.Value stats[s.Stat] = s.Value
@ -137,23 +141,23 @@ func (ccs *CharacterCreationState) OnTick(dt int64) GameState {
action := ccs.inputSystem.NextAction() action := ccs.inputSystem.NextAction()
switch action { switch action {
case input.InputAction_Menu_HighlightRight: case systems.InputAction_Menu_HighlightRight:
ccs.IncreaseStatValue() ccs.IncreaseStatValue()
case input.InputAction_Menu_HighlightLeft: case systems.InputAction_Menu_HighlightLeft:
ccs.DecreaseStatValue() ccs.DecreaseStatValue()
case input.InputAction_Menu_HighlightDown: case systems.InputAction_Menu_HighlightDown:
if ccs.menuState.CurrentHighlight > len(ccs.menuState.Stats) { if ccs.menuState.CurrentHighlight > len(ccs.menuState.Stats) {
break break
} }
ccs.menuState.CurrentHighlight++ ccs.menuState.CurrentHighlight++
case input.InputAction_Menu_HighlightUp: case systems.InputAction_Menu_HighlightUp:
if ccs.menuState.CurrentHighlight == 0 { if ccs.menuState.CurrentHighlight == 0 {
break break
} }
ccs.menuState.CurrentHighlight-- ccs.menuState.CurrentHighlight--
case input.InputAction_Menu_Select: case systems.InputAction_Menu_Select:
ccs.ccMenu.SelectHighlight() ccs.ccMenu.SelectHighlight()
} }

View file

@ -2,14 +2,13 @@ package state
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui" "mvvasilev/last_light/game/ui"
) )
type DialogState struct { type DialogState struct {
inputSystem *input.InputSystem inputSystem *systems.InputSystem
turnSystem *turns.TurnSystem turnSystem *systems.TurnSystem
prevState GameState prevState GameState
@ -18,7 +17,7 @@ type DialogState struct {
returnToPreviousState bool 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{ return &DialogState{
inputSystem: inputSystem, inputSystem: inputSystem,
turnSystem: turnSystem, turnSystem: turnSystem,
@ -28,12 +27,12 @@ func CreateDialogState(inputSystem *input.InputSystem, turnSystem *turns.TurnSys
} }
} }
func (s *DialogState) InputContext() input.Context { func (s *DialogState) InputContext() systems.InputContext {
return input.InputContext_Menu return systems.InputContext_Menu
} }
func (ds *DialogState) OnTick(dt int64) GameState { 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.returnToPreviousState = true
ds.dialog.Select() ds.dialog.Select()
} }

View file

@ -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,
}
}

View file

@ -2,11 +2,11 @@ package state
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
) )
type GameState interface { type GameState interface {
InputContext() input.Context InputContext() systems.InputContext
OnTick(dt int64) GameState OnTick(dt int64) GameState
CollectDrawables() []engine.Drawable CollectDrawables() []engine.Drawable
} }

View file

@ -2,17 +2,18 @@ package state
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/player" "mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui/menu" "mvvasilev/last_light/game/ui/menu"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
type InventoryScreenState struct { type InventoryScreenState struct {
inputSystem *input.InputSystem eventLog *engine.GameEventLog
turnSystem *turns.TurnSystem inputSystem *systems.InputSystem
turnSystem *systems.TurnSystem
dungeon *model.Dungeon
prevState GameState prevState GameState
exitMenu bool exitMenu bool
@ -20,12 +21,13 @@ type InventoryScreenState struct {
inventoryMenu *menu.PlayerInventoryMenu inventoryMenu *menu.PlayerInventoryMenu
selectedInventorySlot engine.Position 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 := new(InventoryScreenState)
iss.eventLog = eventLog
iss.inputSystem = inputSystem iss.inputSystem = inputSystem
iss.turnSystem = turnSystem iss.turnSystem = turnSystem
iss.prevState = prevState iss.prevState = prevState
@ -33,12 +35,13 @@ func CreateInventoryScreenState(inputSystem *input.InputSystem, turnSystem *turn
iss.selectedInventorySlot = engine.PositionAt(0, 0) iss.selectedInventorySlot = engine.PositionAt(0, 0)
iss.exitMenu = false iss.exitMenu = false
iss.inventoryMenu = menu.CreatePlayerInventoryMenu(43, 0, player.Inventory(), tcell.StyleDefault, tcell.StyleDefault.Background(tcell.ColorDarkSlateGray)) iss.inventoryMenu = menu.CreatePlayerInventoryMenu(43, 0, player.Inventory(), tcell.StyleDefault, tcell.StyleDefault.Background(tcell.ColorDarkSlateGray))
iss.dungeon = dungeon
return iss return iss
} }
func (s *InventoryScreenState) InputContext() input.Context { func (s *InventoryScreenState) InputContext() systems.InputContext {
return input.InputContext_Inventory return systems.InputContext_Inventory
} }
func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) { func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) {
@ -46,32 +49,53 @@ func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) {
nextState = iss nextState = iss
switch nextAction { switch nextAction {
case input.InputAction_Menu_Exit: case systems.InputAction_Menu_Exit:
nextState = iss.prevState 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()) iss.player.Inventory().Drop(iss.selectedInventorySlot.XY())
case input.InputAction_Menu_HighlightUp: case systems.InputAction_Menu_HighlightUp:
if iss.selectedInventorySlot.Y() == 0 { if iss.selectedInventorySlot.Y() == 0 {
break break
} }
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, -1) iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, -1)
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY()) 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 { if iss.selectedInventorySlot.Y() == iss.player.Inventory().Shape().Height()-1 {
break break
} }
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, +1) iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, +1)
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY()) iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY())
case input.InputAction_Menu_HighlightLeft: case systems.InputAction_Menu_HighlightLeft:
if iss.selectedInventorySlot.X() == 0 { if iss.selectedInventorySlot.X() == 0 {
break break
} }
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(-1, 0) iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(-1, 0)
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY()) 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 { if iss.selectedInventorySlot.X() == iss.player.Inventory().Shape().Width()-1 {
break break
} }

View file

@ -2,16 +2,15 @@ package state
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui" "mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
type MainMenuState struct { type MainMenuState struct {
turnSystem *turns.TurnSystem turnSystem *systems.TurnSystem
inputSystem *input.InputSystem inputSystem *systems.InputSystem
menuTitle *engine.Raw menuTitle *engine.Raw
buttons []*ui.UISimpleButton buttons []*ui.UISimpleButton
@ -21,7 +20,7 @@ type MainMenuState struct {
startNewGame bool startNewGame bool
} }
func CreateMainMenuState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *MainMenuState { func CreateMainMenuState(turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem) *MainMenuState {
turnSystem.Clear() turnSystem.Clear()
state := new(MainMenuState) 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.buttons = append(state.buttons, ui.CreateSimpleButton(11, 7, "New Game", tcell.StyleDefault, highlightStyle, func() {
state.startNewGame = true 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() { 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 return state
} }
func (s *MainMenuState) InputContext() input.Context { func (s *MainMenuState) InputContext() systems.InputContext {
return input.InputContext_Menu return systems.InputContext_Menu
} }
func (mms *MainMenuState) OnTick(dt int64) GameState { func (mms *MainMenuState) OnTick(dt int64) GameState {
nextAction := mms.inputSystem.NextAction() nextAction := mms.inputSystem.NextAction()
if nextAction == input.InputAction_Menu_HighlightDown { if nextAction == systems.InputAction_Menu_HighlightDown {
mms.buttons[mms.currButtonSelected].Unhighlight() mms.buttons[mms.currButtonSelected].Unhighlight()
mms.currButtonSelected = engine.LimitIncrement(mms.currButtonSelected, 2) mms.currButtonSelected = engine.LimitIncrement(mms.currButtonSelected, 2)
mms.buttons[mms.currButtonSelected].Highlight() mms.buttons[mms.currButtonSelected].Highlight()
} }
if nextAction == input.InputAction_Menu_HighlightUp { if nextAction == systems.InputAction_Menu_HighlightUp {
mms.buttons[mms.currButtonSelected].Unhighlight() mms.buttons[mms.currButtonSelected].Unhighlight()
mms.currButtonSelected = engine.LimitDecrement(mms.currButtonSelected, 0) mms.currButtonSelected = engine.LimitDecrement(mms.currButtonSelected, 0)
mms.buttons[mms.currButtonSelected].Highlight() mms.buttons[mms.currButtonSelected].Highlight()
} }
if nextAction == input.InputAction_Menu_Select { if nextAction == systems.InputAction_Menu_Select {
mms.buttons[mms.currButtonSelected].Select() mms.buttons[mms.currButtonSelected].Select()
} }

View file

@ -2,16 +2,15 @@ package state
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui" "mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
type PauseGameState struct { type PauseGameState struct {
turnSystem *turns.TurnSystem turnSystem *systems.TurnSystem
inputSystem *input.InputSystem inputSystem *systems.InputSystem
prevState GameState prevState GameState
@ -23,7 +22,7 @@ type PauseGameState struct {
currButtonSelected int 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 := new(PauseGameState)
s.turnSystem = turnSystem s.turnSystem = turnSystem
@ -67,23 +66,23 @@ func PauseGame(prevState GameState, turnSystem *turns.TurnSystem, inputSystem *i
return s return s
} }
func (s *PauseGameState) InputContext() input.Context { func (s *PauseGameState) InputContext() systems.InputContext {
return input.InputContext_Menu return systems.InputContext_Menu
} }
func (pg *PauseGameState) OnTick(dt int64) GameState { func (pg *PauseGameState) OnTick(dt int64) GameState {
switch pg.inputSystem.NextAction() { switch pg.inputSystem.NextAction() {
case input.InputAction_Menu_Exit: case systems.InputAction_Menu_Exit:
pg.unpauseGame = true pg.unpauseGame = true
case input.InputAction_Menu_HighlightDown: case systems.InputAction_Menu_HighlightDown:
pg.buttons[pg.currButtonSelected].Unhighlight() pg.buttons[pg.currButtonSelected].Unhighlight()
pg.currButtonSelected = engine.LimitIncrement(pg.currButtonSelected, 1) pg.currButtonSelected = engine.LimitIncrement(pg.currButtonSelected, 1)
pg.buttons[pg.currButtonSelected].Highlight() pg.buttons[pg.currButtonSelected].Highlight()
case input.InputAction_Menu_HighlightUp: case systems.InputAction_Menu_HighlightUp:
pg.buttons[pg.currButtonSelected].Unhighlight() pg.buttons[pg.currButtonSelected].Unhighlight()
pg.currButtonSelected = engine.LimitDecrement(pg.currButtonSelected, 0) pg.currButtonSelected = engine.LimitDecrement(pg.currButtonSelected, 0)
pg.buttons[pg.currButtonSelected].Highlight() pg.buttons[pg.currButtonSelected].Highlight()
case input.InputAction_Menu_Select: case systems.InputAction_Menu_Select:
pg.buttons[pg.currButtonSelected].Select() pg.buttons[pg.currButtonSelected].Select()
} }

View file

@ -3,31 +3,27 @@ package state
import ( import (
"fmt" "fmt"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/npc" "mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/player"
"mvvasilev/last_light/game/rpg"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui" "mvvasilev/last_light/game/ui"
"mvvasilev/last_light/game/world"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views" "github.com/gdamore/tcell/v2/views"
) )
type PlayingState struct { type PlayingState struct {
turnSystem *turns.TurnSystem turnSystem *systems.TurnSystem
inputSystem *input.InputSystem inputSystem *systems.InputSystem
player *player.Player player *model.Player_V2
someNPC npc.RPGNPC someNPC model.Entity_V2
eventLog *engine.GameEventLog eventLog *engine.GameEventLog
uiEventLog *ui.UIEventLog uiEventLog *ui.UIEventLog
healthBar *ui.UIHealthBar healthBar *ui.UIHealthBar
dungeon *world.Dungeon dungeon *model.Dungeon
viewport *engine.Viewport viewport *engine.Viewport
@ -36,7 +32,7 @@ type PlayingState struct {
nextGameState GameState 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() turnSystem.Clear()
s := new(PlayingState) s := new(PlayingState)
@ -46,11 +42,11 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy
mapSize := engine.SizeOf(128, 128) 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.player = model.CreatePlayer_V2(
s.dungeon.CurrentLevel().PlayerSpawnPoint().X(), s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position.X(),
s.dungeon.CurrentLevel().PlayerSpawnPoint().Y(), s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position.Y(),
playerStats, playerStats,
) )
@ -58,30 +54,34 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy
requeue = true requeue = true
complete = false complete = false
if s.player.HealthData().IsDead {
s.nextGameState = CreateGameOverState(inputSystem)
}
switch inputSystem.NextAction() { switch inputSystem.NextAction() {
case input.InputAction_PauseGame: case systems.InputAction_PauseGame:
s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem) s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem)
case input.InputAction_OpenInventory: case systems.InputAction_OpenInventory:
s.nextGameState = CreateInventoryScreenState(s.inputSystem, s.turnSystem, s.player, s) s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s)
case input.InputAction_PickUpItem: case systems.InputAction_PickUpItem:
s.PickUpItemUnderPlayer() s.PickUpItemUnderPlayer()
complete = true complete = true
case input.InputAction_Interact: case systems.InputAction_Interact:
s.InteractBelowPlayer() s.InteractBelowPlayer()
complete = true complete = true
case input.InputAction_OpenLogs: case systems.InputAction_OpenLogs:
s.viewShortLogs = !s.viewShortLogs s.viewShortLogs = !s.viewShortLogs
case input.InputAction_MovePlayer_East: case systems.InputAction_MovePlayer_East:
s.MovePlayer(npc.East) s.MovePlayer(model.East)
complete = true complete = true
case input.InputAction_MovePlayer_West: case systems.InputAction_MovePlayer_West:
s.MovePlayer(npc.West) s.MovePlayer(model.West)
complete = true complete = true
case input.InputAction_MovePlayer_North: case systems.InputAction_MovePlayer_North:
s.MovePlayer(npc.North) s.MovePlayer(model.North)
complete = true complete = true
case input.InputAction_MovePlayer_South: case systems.InputAction_MovePlayer_South:
s.MovePlayer(npc.South) s.MovePlayer(model.South)
complete = true complete = true
default: default:
} }
@ -89,13 +89,12 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy
return return
}) })
s.someNPC = npc.CreateRPGNPC( s.someNPC = model.CreateEntity(
s.dungeon.CurrentLevel().NextLevelStaircase().X(), model.WithPosition(s.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position),
s.dungeon.CurrentLevel().NextLevelStaircase().Y(), model.WithName("NPC"),
"NPC", model.WithPresentation('n', tcell.StyleDefault),
'n', 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})),
tcell.StyleDefault, model.WithHealthData(20, 20, false),
rpg.RandomStats(21, 1, 20, []rpg.Stat{rpg.Stat_Attributes_Strength, rpg.Stat_Attributes_Constitution, rpg.Stat_Attributes_Intelligence, rpg.Stat_Attributes_Dexterity}),
) )
s.turnSystem.Schedule(20, func() (complete bool, requeue bool) { 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.eventLog = engine.CreateGameEventLog(100)
s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault) s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault)
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.player)
s.dungeon.CurrentLevel().AddEntity(s.someNPC) s.dungeon.CurrentLevel().AddEntity(s.someNPC)
s.viewport = engine.CreateViewport( s.viewport = engine.CreateViewport(
engine.PositionAt(0, 0), engine.PositionAt(0, 0),
s.dungeon.CurrentLevel().PlayerSpawnPoint(), s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position,
engine.SizeOf(80, 24), engine.SizeOf(80, 24),
tcell.StyleDefault, tcell.StyleDefault,
) )
@ -124,53 +123,89 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy
return s return s
} }
func (s *PlayingState) InputContext() input.Context { func (s *PlayingState) InputContext() systems.InputContext {
return input.InputContext_Play return systems.InputContext_Play
} }
func (ps *PlayingState) MovePlayer(direction npc.Direction) { func (ps *PlayingState) MovePlayer(direction model.Direction) {
if direction == npc.DirectionNone { if direction == model.DirectionNone {
return return
} }
newPlayerPos := ps.player.Position().WithOffset(npc.MovementDirectionOffset(direction)) newPlayerPos := ps.player.Position().WithOffset(model.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))
}
ent := ps.dungeon.CurrentLevel().EntityAt(newPlayerPos.XY()) ent := ps.dungeon.CurrentLevel().EntityAt(newPlayerPos.XY())
// We are moving into an entity. Attack it. // We are moving into an entity with health data. Attack it.
if ent != nil { if ent != nil && ent.HealthData() != nil {
switch rpge := ent.(type) { if ent.HealthData().IsDead {
case npc.RPGNPC: // TODO: If the entity is dead, the player should be able to move through it.
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 return
} }
rpge.Damage(dmg) ExecuteAttack(ps.eventLog, ps.player, ent)
ps.eventLog.Log(fmt.Sprintf("You attacked %v, and hit for %v %v damage", rpge.Name(), dmg, rpg.DamageTypeName(dmgType)))
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() { func (ps *PlayingState) InteractBelowPlayer() {
playerPos := ps.player.Position() playerPos := ps.player.Position()
if playerPos == ps.dungeon.CurrentLevel().NextLevelStaircase() { if playerPos == ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position {
ps.SwitchToNextLevel() ps.SwitchToNextLevel()
return return
} }
if playerPos == ps.dungeon.CurrentLevel().PreviousLevelStaircase() { if playerPos == ps.dungeon.CurrentLevel().Ground().PreviousLevelStaircase().Position {
ps.SwitchToPreviousLevel() ps.SwitchToPreviousLevel()
return return
} }
@ -200,11 +235,11 @@ func (ps *PlayingState) SwitchToNextLevel() {
ps.dungeon.MoveToNextLevel() ps.dungeon.MoveToNextLevel()
ps.player.MoveTo(ps.dungeon.CurrentLevel().PlayerSpawnPoint()) ps.player.Positioned().Position = ps.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position
ps.viewport = engine.CreateViewport( ps.viewport = engine.CreateViewport(
engine.PositionAt(0, 0), engine.PositionAt(0, 0),
ps.dungeon.CurrentLevel().PlayerSpawnPoint(), ps.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position,
engine.SizeOf(80, 24), engine.SizeOf(80, 24),
tcell.StyleDefault, tcell.StyleDefault,
) )
@ -236,11 +271,11 @@ func (ps *PlayingState) SwitchToPreviousLevel() {
ps.dungeon.MoveToPreviousLevel() ps.dungeon.MoveToPreviousLevel()
ps.player.MoveTo(ps.dungeon.CurrentLevel().NextLevelStaircase()) ps.player.Positioned().Position = ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position
ps.viewport = engine.CreateViewport( ps.viewport = engine.CreateViewport(
engine.PositionAt(0, 0), engine.PositionAt(0, 0),
ps.dungeon.CurrentLevel().NextLevelStaircase(), ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position,
engine.SizeOf(80, 24), engine.SizeOf(80, 24),
tcell.StyleDefault, tcell.StyleDefault,
) )
@ -263,9 +298,12 @@ func (ps *PlayingState) PickUpItemUnderPlayer() {
return return
} }
itemName, _ := item.Name() if item.Named() != nil {
itemName := item.Named().Name
ps.eventLog.Log("You picked up " + itemName) 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 { 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() { func (ps *PlayingState) CalcPathToPlayerAndMove() {
if ps.someNPC.HealthData().IsDead {
ps.dungeon.CurrentLevel().DropEntity(ps.someNPC.UniqueId())
return
}
playerVisibleAndInRange := false 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 playerVisibleAndInRange = true
} }
if !playerVisibleAndInRange { 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 { switch randomMove {
case npc.North: case model.North:
nextPos = nextPos.WithOffset(0, -1) nextPos = nextPos.WithOffset(0, -1)
case npc.South: case model.South:
nextPos = nextPos.WithOffset(0, +1) nextPos = nextPos.WithOffset(0, +1)
case npc.West: case model.West:
nextPos = nextPos.WithOffset(-1, 0) nextPos = nextPos.WithOffset(-1, 0)
case npc.East: case model.East:
nextPos = nextPos.WithOffset(+1, 0) nextPos = nextPos.WithOffset(+1, 0)
default: default:
return return
@ -320,23 +363,12 @@ func (ps *PlayingState) CalcPathToPlayerAndMove() {
return return
} }
if ps.PlayerWithinHitRange(ps.someNPC.Position()) { if ps.PlayerWithinHitRange(ps.someNPC.Positioned().Position) {
hit, precision, evasion, dmg, dmgType := ps.player.CalculateAttack(ps.player) ExecuteAttack(ps.eventLog, ps.someNPC, 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
} }
pathToPlayer := engine.FindPath( pathToPlayer := engine.FindPath(
ps.someNPC.Position(), ps.someNPC.Positioned().Position,
ps.player.Position(), ps.player.Position(),
func(x, y int) bool { func(x, y int) bool {
if x == ps.player.Position().X() && y == ps.player.Position().Y() { 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 { func (ps *PlayingState) CollectDrawables() []engine.Drawable {
mainCameraDrawingInstructions := engine.CreateDrawingInstructions(func(v views.View) { mainCameraDrawingInstructions := engine.CreateDrawingInstructions(func(v views.View) {
visibilityMap := engine.ComputeFOV( visibilityMap := engine.ComputeFOV(
func(x, y int) world.Tile { func(x, y int) model.Tile_V2 {
ps.dungeon.CurrentLevel().Flatten().MarkExplored(x, y) model.Map_MarkExplored(ps.dungeon.CurrentLevel().Ground(), x, y)
return ps.dungeon.CurrentLevel().TileAt(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 model.Map_IsInBounds(ps.dungeon.CurrentLevel().Ground(), x, y) },
func(x, y int) bool { return ps.dungeon.CurrentLevel().Flatten().TileAt(x, y).Opaque() }, func(x, y int) bool { return ps.dungeon.CurrentLevel().TileAt(x, y).Opaque() },
ps.player.Position().X(), ps.player.Position().Y(), ps.player.Position().X(), ps.player.Position().Y(),
13, 13,
) )
@ -386,13 +418,21 @@ func (ps *PlayingState) CollectDrawables() []engine.Drawable {
tile := visibilityMap[engine.PositionAt(x, y)] tile := visibilityMap[engine.PositionAt(x, y)]
if tile != nil { if tile != nil {
return tile.Presentation() if tile.Entity() != nil {
return tile.Entity().Entity.Presentable().Rune, tile.Entity().Entity.Presentable().Style
} }
explored := ps.dungeon.CurrentLevel().Flatten().ExploredTileAt(x, y) if tile.Item() != nil {
return tile.Item().Item.TileIcon(), tile.Item().Item.Style()
}
return tile.DefaultPresentation()
}
explored := model.Map_ExploredTileAt(ps.dungeon.CurrentLevel().Ground(), x, y)
if explored != nil { if explored != nil {
return explored.Presentation() return explored.DefaultPresentation()
} }
return ' ', tcell.StyleDefault return ' ', tcell.StyleDefault

View file

@ -2,14 +2,14 @@ package state
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
) )
type QuitState struct { type QuitState struct {
} }
func (s *QuitState) InputContext() input.Context { func (s *QuitState) InputContext() systems.InputContext {
return input.InputContext_Menu return systems.InputContext_Menu
} }
func (q *QuitState) OnTick(dt int64) GameState { func (q *QuitState) OnTick(dt int64) GameState {

View file

@ -1,4 +1,4 @@
package input package systems
import ( import (
"fmt" "fmt"
@ -6,7 +6,7 @@ import (
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
type Context string type InputContext string
const ( const (
InputContext_Play = "play" InputContext_Play = "play"
@ -16,7 +16,7 @@ const (
type InputKey string 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)) 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 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())] kb.nextAction = kb.keyBindings[InputKeyOf(context, ev.Modifiers(), ev.Key(), ev.Rune())]
} }

View file

@ -1,4 +1,4 @@
package turns package systems
import "mvvasilev/last_light/engine" import "mvvasilev/last_light/engine"

View file

@ -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)
}
}

View file

@ -3,8 +3,8 @@ package menu
import ( import (
"fmt" "fmt"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/rpg" "mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/ui" "mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -13,7 +13,7 @@ import (
) )
type statSelection struct { type statSelection struct {
stat rpg.Stat stat model.Stat
label *ui.UILabel label *ui.UILabel
plusButton *ui.UILabel plusButton *ui.UILabel
statNumberLabel *ui.UILabel statNumberLabel *ui.UILabel
@ -21,7 +21,7 @@ type statSelection struct {
} }
type StatState struct { type StatState struct {
Stat rpg.Stat Stat model.Stat
Value int Value int
} }
@ -93,7 +93,7 @@ func (ccm *CharacterCreationMenu) UpdateState(state *CharacterCreationMenuState)
label: ui.CreateSingleLineUILabel( label: ui.CreateSingleLineUILabel(
statX, statX,
3+i, 3+i,
rpg.StatLongName(s.Stat), model.StatLongName(s.Stat),
labelStyle, labelStyle,
), ),
minusButton: ui.CreateSingleLineUILabel( minusButton: ui.CreateSingleLineUILabel(
@ -172,7 +172,7 @@ func (ccm *CharacterCreationMenu) Size() engine.Size {
return engine.SizeOf(engine.TERMINAL_SIZE_WIDTH, engine.TERMINAL_SIZE_HEIGHT) 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) {
} }

View file

@ -3,9 +3,8 @@ package menu
import ( import (
"fmt" "fmt"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/item" "mvvasilev/last_light/game/systems"
"mvvasilev/last_light/game/rpg"
"mvvasilev/last_light/game/ui" "mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -14,7 +13,7 @@ import (
) )
type PlayerInventoryMenu struct { type PlayerInventoryMenu struct {
inventory *item.EquippedInventory inventory *model.EquippedInventory
inventoryMenu *ui.UIWindow inventoryMenu *ui.UIWindow
armourLabel *ui.UILabel armourLabel *ui.UILabel
@ -26,12 +25,11 @@ type PlayerInventoryMenu struct {
inventoryGrid *engine.Grid inventoryGrid *engine.Grid
playerItems *engine.ArbitraryDrawable playerItems *engine.ArbitraryDrawable
selectedItem *engine.ArbitraryDrawable selectedItem *engine.ArbitraryDrawable
help *ui.UILabel
selectedInventorySlot engine.Position 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 := new(PlayerInventoryMenu)
menu.inventory = playerInventory menu.inventory = playerInventory
@ -94,25 +92,19 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory
continue continue
} }
style := item.Type().Style() style := item.Style()
if isHighlighted { if isHighlighted {
style = highlightStyle style = highlightStyle
} }
ui.CreateSingleLineUILabel( menu.drawItemSlot(
menu.inventoryGrid.Position().X()+1+x*4, menu.inventoryGrid.Position().X()+1+x*4,
menu.inventoryGrid.Position().Y()+y*2, menu.inventoryGrid.Position().Y()+y*2,
fmt.Sprintf("%03d", item.Quantity()), item,
style, style,
).Draw(v) v,
)
ui.CreateSingleLineUILabel(
menu.inventoryGrid.Position().X()+1+x*4,
menu.inventoryGrid.Position().Y()+1+y*2,
item.Type().Icon(),
style,
).Draw(v)
} }
} }
}) })
@ -124,19 +116,30 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory
return return
} }
switch it := item.(type) { ui.CreateUIItem(x+2, y+14, item, style).Draw(v)
case rpg.RPGItem:
ui.CreateUIRPGItem(x+2, y+14, it, style).Draw(v)
default:
ui.CreateUIBasicItem(x+2, y+14, it, style).Draw(v)
}
}) })
menu.help = ui.CreateSingleLineUILabel(x+2, y+22, "hjkl - move, x - drop, e - equip", style)
return menu 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) { func (pim *PlayerInventoryMenu) MoveTo(x int, y int) {
} }
@ -149,7 +152,7 @@ func (pim *PlayerInventoryMenu) Size() engine.Size {
return pim.inventoryMenu.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.inventoryGrid.Draw(v)
pim.playerItems.Draw(v) pim.playerItems.Draw(v)
pim.selectedItem.Draw(v) pim.selectedItem.Draw(v)
pim.help.Draw(v)
} }

View file

@ -2,14 +2,14 @@ package ui
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
) )
type UIElement interface { type UIElement interface {
MoveTo(x, y int) MoveTo(x, y int)
Position() engine.Position Position() engine.Position
Size() engine.Size Size() engine.Size
Input(inputAction input.InputAction) Input(inputAction systems.InputAction)
engine.Drawable engine.Drawable
} }

View file

@ -2,7 +2,7 @@ package ui
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views" "github.com/gdamore/tcell/v2/views"
@ -91,13 +91,13 @@ func (d *UIDialog) Size() engine.Size {
return d.window.Size() return d.window.Size()
} }
func (d *UIDialog) Input(inputAction input.InputAction) { func (d *UIDialog) Input(inputAction systems.InputAction) {
if inputAction == input.InputAction_Menu_HighlightLeft { if inputAction == systems.InputAction_Menu_HighlightLeft {
if !d.yesBtn.IsHighlighted() { if !d.yesBtn.IsHighlighted() {
d.noBtn.Unhighlight() d.noBtn.Unhighlight()
d.yesBtn.Highlight() d.yesBtn.Highlight()
} }
} else if inputAction == input.InputAction_Menu_HighlightRight { } else if inputAction == systems.InputAction_Menu_HighlightRight {
if d.noBtn == nil { if d.noBtn == nil {
return return
} }

View file

@ -2,7 +2,7 @@ package ui
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views" "github.com/gdamore/tcell/v2/views"
@ -40,7 +40,7 @@ func (uie *UIEventLog) Size() engine.Size {
return uie.window.Size() return uie.window.Size()
} }
func (uie *UIEventLog) Input(inputAction input.InputAction) { func (uie *UIEventLog) Input(inputAction systems.InputAction) {
} }

View file

@ -4,7 +4,8 @@ import (
"fmt" "fmt"
"math" "math"
"mvvasilev/last_light/engine" "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"
"github.com/gdamore/tcell/v2/views" "github.com/gdamore/tcell/v2/views"
@ -13,34 +14,21 @@ import (
type UIHealthBar struct { type UIHealthBar struct {
id uuid.UUID id uuid.UUID
health int player *model.Player_V2
maxHealth int
window *UIWindow window *UIWindow
style tcell.Style style tcell.Style
} }
// TODO: style for health bar fill func CreateHealthBar(x, y, w, h int, player *model.Player_V2, style tcell.Style) *UIHealthBar {
// TODO: 'HP' title
// TODO: test different percentages
func CreateHealthBar(x, y, w, h, health, maxHealth int, style tcell.Style) *UIHealthBar {
return &UIHealthBar{ return &UIHealthBar{
window: CreateWindow(x, y, w, h, "HP", style), window: CreateWindow(x, y, w, h, "HP", style),
health: health, player: player,
maxHealth: maxHealth,
style: style, 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) { func (uihp *UIHealthBar) MoveTo(x int, y int) {
uihp.window.MoveTo(x, y) uihp.window.MoveTo(x, y)
} }
@ -53,7 +41,7 @@ func (uihp *UIHealthBar) Size() engine.Size {
return uihp.window.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 { 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 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) whole := math.Trunc(percentage)
last := percentage - whole 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( engine.DrawText(
x+w/2-len(hpText)/2, x+w/2-len(hpText)/2,

133
game/ui/ui_item.go Normal file
View file

@ -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)
}
}

View file

@ -2,7 +2,7 @@ package ui
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
"unicode/utf8" "unicode/utf8"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -57,4 +57,4 @@ func (t *UILabel) Draw(v views.View) {
t.text.Draw(v) t.text.Draw(v)
} }
func (t *UILabel) Input(inputAction input.InputAction) {} func (t *UILabel) Input(inputAction systems.InputAction) {}

View file

@ -2,7 +2,7 @@ package ui
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@ -97,6 +97,6 @@ func (sb *UISimpleButton) Draw(v views.View) {
sb.text.Draw(v) sb.text.Draw(v)
} }
func (sb *UISimpleButton) Input(inputAction input.InputAction) { func (sb *UISimpleButton) Input(inputAction systems.InputAction) {
} }

View file

@ -2,7 +2,7 @@ package ui
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input" "mvvasilev/last_light/game/systems"
"unicode/utf8" "unicode/utf8"
"github.com/gdamore/tcell/v2" "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) {
} }

View file

@ -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
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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) {
}

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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{}
}