mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-18 20:29:52 +03:00
small rewrite
This commit is contained in:
parent
6bcb59867e
commit
ec51edb7c0
77 changed files with 3407 additions and 2522 deletions
|
@ -114,6 +114,14 @@ func (s Size) Contains(x, y int) bool {
|
|||
return 0 <= x && x < s.width && 0 <= y && y < s.height
|
||||
}
|
||||
|
||||
func LimitAdd(original, amount, limit int) int {
|
||||
if original+amount > limit {
|
||||
return limit
|
||||
}
|
||||
|
||||
return original + amount
|
||||
}
|
||||
|
||||
func LimitIncrement(i int, limit int) int {
|
||||
if (i + 1) > limit {
|
||||
return i
|
||||
|
@ -122,6 +130,14 @@ func LimitIncrement(i int, limit int) int {
|
|||
return i + 1
|
||||
}
|
||||
|
||||
func LimitSubtract(original, amount, limit int) int {
|
||||
if original-amount < limit {
|
||||
return limit
|
||||
}
|
||||
|
||||
return original - amount
|
||||
}
|
||||
|
||||
func LimitDecrement(i int, limit int) int {
|
||||
if (i - 1) < limit {
|
||||
return i
|
11
game/game.go
11
game/game.go
|
@ -2,16 +2,15 @@ package game
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/state"
|
||||
"mvvasilev/last_light/game/turns"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
turnSystem *turns.TurnSystem
|
||||
inputSystem *input.InputSystem
|
||||
turnSystem *systems.TurnSystem
|
||||
inputSystem *systems.InputSystem
|
||||
|
||||
state state.GameState
|
||||
|
||||
|
@ -21,9 +20,9 @@ type Game struct {
|
|||
func CreateGame() *Game {
|
||||
game := new(Game)
|
||||
|
||||
game.turnSystem = turns.CreateTurnSystem()
|
||||
game.turnSystem = systems.CreateTurnSystem()
|
||||
|
||||
game.inputSystem = input.CreateInputSystemWithDefaultBindings()
|
||||
game.inputSystem = systems.CreateInputSystemWithDefaultBindings()
|
||||
|
||||
game.state = state.CreateMainMenuState(game.turnSystem, game.inputSystem)
|
||||
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
226
game/model/entity.go
Normal 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
1
game/model/entity_npc.go
Normal file
|
@ -0,0 +1 @@
|
|||
package model
|
49
game/model/entity_player.go
Normal file
49
game/model/entity_player.go
Normal 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()
|
||||
}
|
57
game/model/entity_rpg_npc.go
Normal file
57
game/model/entity_rpg_npc.go
Normal 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
136
game/model/inventory.go
Normal 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]
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package item
|
||||
package model
|
||||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
|
@ -18,13 +18,13 @@ const (
|
|||
)
|
||||
|
||||
type EquippedInventory struct {
|
||||
offHand Item
|
||||
dominantHand Item
|
||||
offHand Item_V2
|
||||
dominantHand Item_V2
|
||||
|
||||
head Item
|
||||
chestplate Item
|
||||
leggings Item
|
||||
shoes Item
|
||||
head Item_V2
|
||||
chestplate Item_V2
|
||||
leggings Item_V2
|
||||
shoes Item_V2
|
||||
|
||||
*BasicInventory
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ func CreateEquippedInventory() *EquippedInventory {
|
|||
}
|
||||
}
|
||||
|
||||
func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item {
|
||||
func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item_V2 {
|
||||
switch slot {
|
||||
case EquippedSlotOffhand:
|
||||
return ei.offHand
|
||||
|
@ -54,7 +54,7 @@ func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item {
|
|||
}
|
||||
}
|
||||
|
||||
func (ei *EquippedInventory) Equip(item Item, slot EquippedSlot) Item {
|
||||
func (ei *EquippedInventory) Equip(item Item_V2, slot EquippedSlot) Item_V2 {
|
||||
switch slot {
|
||||
case EquippedSlotOffhand:
|
||||
ei.offHand = item
|
308
game/model/item.go
Normal file
308
game/model/item.go
Normal 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
486
game/model/items.go
Normal 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
116
game/model/rpg_entity.go
Normal 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)
|
||||
// }
|
|
@ -1,9 +1,8 @@
|
|||
package rpg
|
||||
package model
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/item"
|
||||
"slices"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
@ -12,11 +11,9 @@ import (
|
|||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TODO: figure out event logging. Need to inform the player of the things that are happening...
|
||||
|
||||
const MaxNumberOfModifiers = 6
|
||||
|
||||
type ItemSupplier func() item.Item
|
||||
type ItemSupplier func() Item_V2
|
||||
|
||||
type LootTable struct {
|
||||
table []ItemSupplier
|
||||
|
@ -34,7 +31,7 @@ func (igt *LootTable) Add(weight int, createItemFunction ItemSupplier) {
|
|||
}
|
||||
}
|
||||
|
||||
func (igt *LootTable) Generate() item.Item {
|
||||
func (igt *LootTable) Generate() Item_V2 {
|
||||
return igt.table[rand.Intn(len(igt.table))]()
|
||||
}
|
||||
|
||||
|
@ -118,25 +115,25 @@ func randomSuffix() string {
|
|||
return suffixes[rand.Intn(len(suffixes))]
|
||||
}
|
||||
|
||||
func generateItemName(itemType RPGItemType, rarity ItemRarity) (string, tcell.Style) {
|
||||
func generateItemName(existingItemName string, rarity ItemRarity) (string, tcell.Style) {
|
||||
switch rarity {
|
||||
case ItemRarity_Common:
|
||||
return itemType.Name(), tcell.StyleDefault
|
||||
return existingItemName, tcell.StyleDefault
|
||||
case ItemRarity_Uncommon:
|
||||
return randomAdjective() + " " + itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorLime)
|
||||
return randomAdjective() + " " + existingItemName, tcell.StyleDefault.Foreground(tcell.ColorLime)
|
||||
case ItemRarity_Rare:
|
||||
return itemType.Name() + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorBlue)
|
||||
return existingItemName + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorBlue)
|
||||
case ItemRarity_Epic:
|
||||
return randomAdjective() + " " + itemType.Name() + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorPurple)
|
||||
return randomAdjective() + " " + existingItemName + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorPurple)
|
||||
case ItemRarity_Legendary:
|
||||
return generateUniqueItemName() + ", Legendary " + itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorOrange).Attributes(tcell.AttrBold)
|
||||
return generateUniqueItemName() + ", Legendary " + existingItemName, tcell.StyleDefault.Foreground(tcell.ColorOrange).Attributes(tcell.AttrBold)
|
||||
default:
|
||||
return itemType.Name(), tcell.StyleDefault
|
||||
return existingItemName, tcell.StyleDefault
|
||||
}
|
||||
}
|
||||
|
||||
func randomStat(metaItemTypes []RPGItemMetaType) Stat {
|
||||
stats := make(map[RPGItemMetaType][]Stat, 0)
|
||||
func randomStat(metaItemTypes []ItemMetaType) Stat {
|
||||
stats := make(map[ItemMetaType][]Stat, 0)
|
||||
|
||||
stats[MetaItemType_Weapon] = []Stat{
|
||||
Stat_Attributes_Strength,
|
||||
|
@ -183,6 +180,12 @@ func randomStat(metaItemTypes []RPGItemMetaType) Stat {
|
|||
Stat_DamageBonus_Magic_Thunder,
|
||||
Stat_DamageBonus_Magic_Acid,
|
||||
Stat_DamageBonus_Magic_Poison,
|
||||
Stat_ResistanceBonus_Magic_Acid,
|
||||
Stat_ResistanceBonus_Magic_Cold,
|
||||
Stat_ResistanceBonus_Magic_Fire,
|
||||
Stat_ResistanceBonus_Magic_Necrotic,
|
||||
Stat_ResistanceBonus_Magic_Poison,
|
||||
Stat_ResistanceBonus_Magic_Thunder,
|
||||
}
|
||||
|
||||
stats[MetaItemType_Physical_Armour] = []Stat{
|
||||
|
@ -190,6 +193,10 @@ func randomStat(metaItemTypes []RPGItemMetaType) Stat {
|
|||
Stat_DamageBonus_Physical_Slashing,
|
||||
Stat_DamageBonus_Physical_Piercing,
|
||||
Stat_DamageBonus_Physical_Bludgeoning,
|
||||
Stat_ResistanceBonus_Physical_Bludgeoning,
|
||||
Stat_ResistanceBonus_Physical_Piercing,
|
||||
Stat_ResistanceBonus_Physical_Slashing,
|
||||
Stat_ResistanceBonus_Physical_Unarmed,
|
||||
}
|
||||
|
||||
possibleStats := make([]Stat, 0, 10)
|
||||
|
@ -201,7 +208,7 @@ func randomStat(metaItemTypes []RPGItemMetaType) Stat {
|
|||
return slices.Compact(possibleStats)[rand.Intn(len(stats))]
|
||||
}
|
||||
|
||||
func generateItemStatModifiers(itemType RPGItemType, rarity ItemRarity) []StatModifier {
|
||||
func generateItemStatModifiers(itemMetaTypes []ItemMetaType, rarity ItemRarity) []StatModifier {
|
||||
points := pointPerRarity(rarity)
|
||||
modifiers := make(map[Stat]*StatModifier, 0)
|
||||
|
||||
|
@ -218,7 +225,7 @@ func generateItemStatModifiers(itemType RPGItemType, rarity ItemRarity) []StatMo
|
|||
continue
|
||||
}
|
||||
|
||||
stat := randomStat(itemType.MetaTypes())
|
||||
stat := randomStat(itemMetaTypes)
|
||||
|
||||
existingForStat := modifiers[stat]
|
||||
|
||||
|
@ -257,14 +264,32 @@ func generateItemStatModifiers(itemType RPGItemType, rarity ItemRarity) []StatMo
|
|||
|
||||
// Each rarity gets an amount of generation points, the higher the rarity, the more points
|
||||
// Each stat modifier consumes points. The higher the stat bonus, the more points it consumes.
|
||||
func GenerateItemOfTypeAndRarity(itemType RPGItemType, rarity ItemRarity) RPGItem {
|
||||
// points := pointPerRarity(rarity)
|
||||
name, style := generateItemName(itemType, rarity)
|
||||
func GenerateItemOfTypeAndRarity(prototype Item_V2, rarity ItemRarity) Item_V2 {
|
||||
if prototype.Named() == nil {
|
||||
return prototype
|
||||
}
|
||||
|
||||
return CreateRPGItem(
|
||||
name,
|
||||
style,
|
||||
itemType,
|
||||
generateItemStatModifiers(itemType, rarity),
|
||||
if prototype.MetaTypes() == nil {
|
||||
return prototype
|
||||
}
|
||||
|
||||
existingName := prototype.Named().Name
|
||||
metaTypes := prototype.MetaTypes().Types
|
||||
|
||||
// points := pointPerRarity(rarity)
|
||||
name, style := generateItemName(existingName, rarity)
|
||||
statModifiers := generateItemStatModifiers(metaTypes, rarity)
|
||||
|
||||
return createBaseItem(
|
||||
prototype.Type(),
|
||||
prototype.TileIcon(),
|
||||
prototype.Icon(),
|
||||
prototype.Style(),
|
||||
item_WithName(name, style),
|
||||
item_WithDescription(prototype.Described().Description, prototype.Described().Style),
|
||||
item_WithDamaging(prototype.Damaging().DamageRoll),
|
||||
item_WithEquippable(prototype.Equippable().Slot),
|
||||
item_WithStatModifiers(statModifiers),
|
||||
item_WithMetaTypes(metaTypes),
|
||||
)
|
||||
}
|
73
game/model/rpg_items.go
Normal file
73
game/model/rpg_items.go
Normal 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,
|
||||
// ),
|
||||
// }
|
||||
// }
|
|
@ -1,4 +1,4 @@
|
|||
package rpg
|
||||
package model
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
@ -34,6 +34,18 @@ const (
|
|||
Stat_DamageBonus_Magic_Acid Stat = 120
|
||||
Stat_DamageBonus_Magic_Poison Stat = 130
|
||||
|
||||
Stat_ResistanceBonus_Physical_Unarmed Stat = 200
|
||||
Stat_ResistanceBonus_Physical_Slashing Stat = 210
|
||||
Stat_ResistanceBonus_Physical_Piercing Stat = 220
|
||||
Stat_ResistanceBonus_Physical_Bludgeoning Stat = 230
|
||||
|
||||
Stat_ResistanceBonus_Magic_Fire Stat = 240
|
||||
Stat_ResistanceBonus_Magic_Cold Stat = 250
|
||||
Stat_ResistanceBonus_Magic_Necrotic Stat = 260
|
||||
Stat_ResistanceBonus_Magic_Thunder Stat = 270
|
||||
Stat_ResistanceBonus_Magic_Acid Stat = 280
|
||||
Stat_ResistanceBonus_Magic_Poison Stat = 290
|
||||
|
||||
Stat_MaxHealthBonus Stat = 140
|
||||
)
|
||||
|
||||
|
@ -214,52 +226,99 @@ func DamageTypeToBonusStat(dmgType DamageType) Stat {
|
|||
}
|
||||
}
|
||||
|
||||
func DamageTypeToResistanceStat(dmgType DamageType) Stat {
|
||||
switch dmgType {
|
||||
case DamageType_Physical_Unarmed:
|
||||
return Stat_ResistanceBonus_Physical_Unarmed
|
||||
case DamageType_Physical_Slashing:
|
||||
return Stat_ResistanceBonus_Physical_Slashing
|
||||
case DamageType_Physical_Piercing:
|
||||
return Stat_ResistanceBonus_Physical_Piercing
|
||||
case DamageType_Physical_Bludgeoning:
|
||||
return Stat_ResistanceBonus_Physical_Bludgeoning
|
||||
case DamageType_Magic_Fire:
|
||||
return Stat_ResistanceBonus_Magic_Fire
|
||||
case DamageType_Magic_Cold:
|
||||
return Stat_ResistanceBonus_Magic_Cold
|
||||
case DamageType_Magic_Necrotic:
|
||||
return Stat_ResistanceBonus_Magic_Necrotic
|
||||
case DamageType_Magic_Thunder:
|
||||
return Stat_ResistanceBonus_Magic_Thunder
|
||||
case DamageType_Magic_Acid:
|
||||
return Stat_ResistanceBonus_Magic_Acid
|
||||
case DamageType_Magic_Poison:
|
||||
return Stat_ResistanceBonus_Magic_Poison
|
||||
default:
|
||||
return Stat_NonExtant
|
||||
}
|
||||
}
|
||||
|
||||
func LuckRoll() int {
|
||||
return RollD10(1)
|
||||
}
|
||||
|
||||
func TotalModifierForStat(entity RPGEntity, stat Stat) int {
|
||||
func TotalModifierForStat(stats *Item_StatModifierComponent, stat Stat) int {
|
||||
agg := 0
|
||||
|
||||
for _, m := range entity.CollectModifiersForStat(stat) {
|
||||
agg += m.Bonus
|
||||
for _, m := range stats.StatModifiers {
|
||||
if m.Stat == stat {
|
||||
agg += m.Bonus
|
||||
}
|
||||
}
|
||||
|
||||
return agg
|
||||
}
|
||||
|
||||
func StatValue(entity RPGEntity, stat Stat) int {
|
||||
return entity.BaseStat(stat) + TotalModifierForStat(entity, stat)
|
||||
func statValue(stats *Entity_StatsHolderComponent, stat Stat) int {
|
||||
return stats.BaseStats[stat]
|
||||
}
|
||||
|
||||
// Base Max Health is determined from constitution:
|
||||
// 5*Constitution + Max Health Bonus
|
||||
func BaseMaxHealth(entity RPGEntity) int {
|
||||
return 5*StatValue(entity, Stat_Attributes_Constitution) + StatValue(entity, Stat_MaxHealthBonus)
|
||||
func BaseMaxHealth(entity Entity_V2) int {
|
||||
stats := entity.Stats()
|
||||
|
||||
if stats == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 5*statValue(stats, Stat_Attributes_Constitution) + statValue(stats, Stat_MaxHealthBonus)
|
||||
}
|
||||
|
||||
// Dexterity + Evasion bonus + luck roll
|
||||
func EvasionRoll(victim RPGEntity) int {
|
||||
return StatValue(victim, Stat_Attributes_Dexterity) + StatValue(victim, Stat_EvasionBonus) + LuckRoll()
|
||||
func EvasionRoll(victim Entity_V2) int {
|
||||
if victim.Stats() == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return statValue(victim.Stats(), Stat_Attributes_Dexterity) + statValue(victim.Stats(), Stat_EvasionBonus) + LuckRoll()
|
||||
}
|
||||
|
||||
// Strength + Precision bonus ( melee + total ) + luck roll
|
||||
func PhysicalPrecisionRoll(attacker RPGEntity) int {
|
||||
return StatValue(attacker, Stat_Attributes_Strength) + StatValue(attacker, Stat_PhysicalPrecisionBonus) + StatValue(attacker, Stat_TotalPrecisionBonus) + LuckRoll()
|
||||
func PhysicalPrecisionRoll(attacker Entity_V2) int {
|
||||
if attacker.Stats() == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return statValue(attacker.Stats(), Stat_Attributes_Strength) + statValue(attacker.Stats(), Stat_PhysicalPrecisionBonus) + statValue(attacker.Stats(), Stat_TotalPrecisionBonus) + LuckRoll()
|
||||
}
|
||||
|
||||
// Intelligence + Precision bonus ( magic + total ) + luck roll
|
||||
func MagicPrecisionRoll(attacker RPGEntity) int {
|
||||
return StatValue(attacker, Stat_Attributes_Intelligence) + StatValue(attacker, Stat_MagicPrecisionBonus) + StatValue(attacker, Stat_TotalPrecisionBonus) + LuckRoll()
|
||||
func MagicPrecisionRoll(attacker Entity_V2) int {
|
||||
if attacker.Stats() == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return statValue(attacker.Stats(), Stat_Attributes_Intelligence) + statValue(attacker.Stats(), Stat_MagicPrecisionBonus) + statValue(attacker.Stats(), Stat_TotalPrecisionBonus) + LuckRoll()
|
||||
}
|
||||
|
||||
// true = hit lands, false = hit does not land
|
||||
func MagicHitRoll(attacker RPGEntity, victim RPGEntity) bool {
|
||||
func MagicHitRoll(attacker Entity_V2, victim Entity_V2) bool {
|
||||
return hitRoll(EvasionRoll(victim), MagicPrecisionRoll(attacker))
|
||||
}
|
||||
|
||||
// true = hit lands, false = hit does not land
|
||||
func PhysicalHitRoll(attacker RPGEntity, victim RPGEntity) (hit bool, evasion, precision int) {
|
||||
func PhysicalHitRoll(attacker Entity_V2, victim Entity_V2) (hit bool, evasion, precision int) {
|
||||
evasion = EvasionRoll(victim)
|
||||
precision = PhysicalPrecisionRoll(attacker)
|
||||
hit = hitRoll(evasion, precision)
|
||||
|
@ -271,21 +330,30 @@ func hitRoll(evasionRoll, precisionRoll int) bool {
|
|||
return evasionRoll < precisionRoll
|
||||
}
|
||||
|
||||
func UnarmedDamage(attacker RPGEntity) int {
|
||||
return RollD4(1) + StatValue(attacker, Stat_DamageBonus_Physical_Unarmed)
|
||||
func UnarmedDamage(attacker Entity_V2) int {
|
||||
if attacker.Stats() == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return RollD4(1) + statValue(attacker.Stats(), Stat_DamageBonus_Physical_Unarmed)
|
||||
}
|
||||
|
||||
func PhysicalWeaponDamange(attacker RPGEntity, weapon RPGItem, victim RPGEntity) (totalDamage int, dmgType DamageType) {
|
||||
totalDamage, dmgType = weapon.RPGType().RollDamage()(victim, attacker)
|
||||
func PhysicalWeaponDamage(attacker Entity_V2, weapon Item_V2, victim Entity_V2) (totalDamage int, dmgType DamageType) {
|
||||
if attacker.Stats() == nil || weapon.Damaging() == nil || victim.Stats() == nil {
|
||||
return 0, DamageType_Physical_Unarmed
|
||||
}
|
||||
|
||||
totalDamage, dmgType = weapon.Damaging().DamageRoll()
|
||||
|
||||
bonusDmgStat := DamageTypeToBonusStat(dmgType)
|
||||
dmgResistStat := DamageTypeToResistanceStat(dmgType)
|
||||
|
||||
totalDamage = totalDamage + StatValue(attacker, bonusDmgStat)
|
||||
totalDamage = totalDamage + statValue(attacker.Stats(), bonusDmgStat) - statValue(victim.Stats(), dmgResistStat)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func UnarmedAttack(attacker RPGEntity, victim RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) {
|
||||
func UnarmedAttack(attacker Entity_V2, victim Entity_V2) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) {
|
||||
hit, evasionRoll, precisionRoll = PhysicalHitRoll(attacker, victim)
|
||||
|
||||
if !hit {
|
||||
|
@ -298,14 +366,14 @@ func UnarmedAttack(attacker RPGEntity, victim RPGEntity) (hit bool, precisionRol
|
|||
return
|
||||
}
|
||||
|
||||
func PhysicalWeaponAttack(attacker RPGEntity, weapon RPGItem, victim RPGEntity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) {
|
||||
func PhysicalWeaponAttack(attacker Entity_V2, weapon Item_V2, victim Entity_V2) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) {
|
||||
hit, evasionRoll, precisionRoll = PhysicalHitRoll(attacker, victim)
|
||||
|
||||
if !hit {
|
||||
return
|
||||
}
|
||||
|
||||
damage, damageType = PhysicalWeaponDamange(attacker, weapon, victim)
|
||||
damage, damageType = PhysicalWeaponDamage(attacker, weapon, victim)
|
||||
|
||||
return
|
||||
}
|
328
game/model/world_dungeon.go
Normal file
328
game/model/world_dungeon.go
Normal 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()
|
||||
}
|
50
game/model/world_empty_map.go
Normal file
50
game/model/world_empty_map.go
Normal 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()
|
||||
// }
|
140
game/model/world_entity_map.go
Normal file
140
game/model/world_entity_map.go
Normal 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) {
|
||||
// }
|
|
@ -1,4 +1,4 @@
|
|||
package world
|
||||
package model
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
@ -27,7 +27,7 @@ type bspNode struct {
|
|||
splitDir splitDirection
|
||||
}
|
||||
|
||||
func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap {
|
||||
func CreateBSPDungeonMap(width, height int, numSplits int) Map_V2 {
|
||||
root := new(bspNode)
|
||||
|
||||
root.origin = engine.PositionAt(0, 0)
|
||||
|
@ -35,10 +35,10 @@ func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap {
|
|||
|
||||
split(root, numSplits)
|
||||
|
||||
tiles := make([][]Tile, height)
|
||||
tiles := make([][]Tile_V2, height)
|
||||
|
||||
for h := range height {
|
||||
tiles[h] = make([]Tile, width)
|
||||
tiles[h] = make([]Tile_V2, width)
|
||||
}
|
||||
|
||||
rooms := make([]engine.BoundingBox, 0, numSplits*numSplits)
|
||||
|
@ -88,28 +88,50 @@ func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap {
|
|||
)
|
||||
})
|
||||
|
||||
bsp := new(BSPDungeonMap)
|
||||
|
||||
spawnRoom := findRoom(root.left)
|
||||
staircaseRoom := findRoom(root.right)
|
||||
|
||||
bsp.rooms = rooms
|
||||
bsp.level = CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey))
|
||||
|
||||
bsp.playerSpawnPoint = engine.PositionAt(
|
||||
playerPos := engine.PositionAt(
|
||||
spawnRoom.Position().X()+spawnRoom.Size().Width()/2,
|
||||
spawnRoom.Position().Y()+spawnRoom.Size().Height()/2,
|
||||
)
|
||||
|
||||
bsp.nextLevelStaircase = engine.PositionAt(
|
||||
staircaseRoom.Position().X()+staircaseRoom.Size().Width()/2,
|
||||
staircaseRoom.Position().Y()+staircaseRoom.Size().Height()/2,
|
||||
newBsp := CreateMap(
|
||||
root.size,
|
||||
tiles,
|
||||
tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey),
|
||||
Tile_Void(),
|
||||
Map_WithRooms(rooms),
|
||||
Map_WithPlayerSpawnPoint(playerPos),
|
||||
Map_WithNextLevelStaircase(engine.PositionAt(
|
||||
staircaseRoom.Position().X()+staircaseRoom.Size().Width()/2,
|
||||
staircaseRoom.Position().Y()+staircaseRoom.Size().Height()/2,
|
||||
)),
|
||||
Map_WithPreviousLevelStaircase(playerPos),
|
||||
)
|
||||
|
||||
bsp.level.SetTileAt(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), CreateStaticTile(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), TileTypeStaircaseDown()))
|
||||
bsp.level.SetTileAt(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), CreateStaticTile(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), TileTypeStaircaseUp()))
|
||||
Map_SetTileAt(newBsp, newBsp.NextLevelStaircase().Position.X(), newBsp.NextLevelStaircase().Position.Y(), Tile_StaircaseDown())
|
||||
Map_SetTileAt(newBsp, newBsp.PreviousLevelStaircase().Position.X(), newBsp.PreviousLevelStaircase().Position.Y(), Tile_StaircaseUp())
|
||||
|
||||
return bsp
|
||||
// bsp := new(BSPDungeonMap)
|
||||
|
||||
// bsp.rooms = rooms
|
||||
// bsp.level = CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey))
|
||||
|
||||
// bsp.playerSpawnPoint = engine.PositionAt(
|
||||
// spawnRoom.Position().X()+spawnRoom.Size().Width()/2,
|
||||
// spawnRoom.Position().Y()+spawnRoom.Size().Height()/2,
|
||||
// )
|
||||
|
||||
// bsp.nextLevelStaircase = engine.PositionAt(
|
||||
// staircaseRoom.Position().X()+staircaseRoom.Size().Width()/2,
|
||||
// staircaseRoom.Position().Y()+staircaseRoom.Size().Height()/2,
|
||||
// )
|
||||
|
||||
// bsp.level.SetTileAt(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), CreateStaticTile(bsp.nextLevelStaircase.X(), bsp.nextLevelStaircase.Y(), TileTypeStaircaseDown()))
|
||||
// bsp.level.SetTileAt(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), CreateStaticTile(bsp.playerSpawnPoint.X(), bsp.playerSpawnPoint.Y(), TileTypeStaircaseUp()))
|
||||
|
||||
return newBsp
|
||||
}
|
||||
|
||||
func findRoom(parent *bspNode) engine.BoundingBox {
|
||||
|
@ -124,7 +146,7 @@ func findRoom(parent *bspNode) engine.BoundingBox {
|
|||
}
|
||||
}
|
||||
|
||||
func zCorridor(tiles [][]Tile, from engine.Position, to engine.Position, direction splitDirection) {
|
||||
func zCorridor(tiles [][]Tile_V2, from engine.Position, to engine.Position, direction splitDirection) {
|
||||
switch direction {
|
||||
case splitDirectionHorizontal:
|
||||
xMidPoint := (from.X() + to.X()) / 2
|
||||
|
@ -206,7 +228,7 @@ func split(parent *bspNode, numSplits int) {
|
|||
split(parent.right, numSplits-1)
|
||||
}
|
||||
|
||||
func horizontalTunnel(tiles [][]Tile, x1, x2, y int) {
|
||||
func horizontalTunnel(tiles [][]Tile_V2, x1, x2, y int) {
|
||||
if x1 > x2 {
|
||||
tx := x2
|
||||
x2 = x1
|
||||
|
@ -222,7 +244,7 @@ func horizontalTunnel(tiles [][]Tile, x1, x2, y int) {
|
|||
continue
|
||||
}
|
||||
|
||||
tiles[y][x] = CreateStaticTile(x, y, TileTypeGround())
|
||||
tiles[y][x] = Tile_Ground()
|
||||
|
||||
placeWallAtIfNotPassable(tiles, x, y-1)
|
||||
placeWallAtIfNotPassable(tiles, x, y+1)
|
||||
|
@ -233,7 +255,7 @@ func horizontalTunnel(tiles [][]Tile, x1, x2, y int) {
|
|||
placeWallAtIfNotPassable(tiles, x2, y+1)
|
||||
}
|
||||
|
||||
func verticalTunnel(tiles [][]Tile, y1, y2, x int) {
|
||||
func verticalTunnel(tiles [][]Tile_V2, y1, y2, x int) {
|
||||
if y1 > y2 {
|
||||
ty := y2
|
||||
y2 = y1
|
||||
|
@ -249,7 +271,7 @@ func verticalTunnel(tiles [][]Tile, y1, y2, x int) {
|
|||
continue
|
||||
}
|
||||
|
||||
tiles[y][x] = CreateStaticTile(x, y, TileTypeGround())
|
||||
tiles[y][x] = Tile_Ground()
|
||||
|
||||
placeWallAtIfNotPassable(tiles, x-1, y)
|
||||
placeWallAtIfNotPassable(tiles, x+1, y)
|
||||
|
@ -260,33 +282,33 @@ func verticalTunnel(tiles [][]Tile, y1, y2, x int) {
|
|||
placeWallAtIfNotPassable(tiles, x+1, y2)
|
||||
}
|
||||
|
||||
func placeWallAtIfNotPassable(tiles [][]Tile, x, y int) {
|
||||
func placeWallAtIfNotPassable(tiles [][]Tile_V2, x, y int) {
|
||||
if tiles[y][x] != nil && tiles[y][x].Passable() {
|
||||
return
|
||||
}
|
||||
|
||||
tiles[y][x] = CreateStaticTile(x, y, TileTypeWall())
|
||||
tiles[y][x] = Tile_Wall()
|
||||
}
|
||||
|
||||
func makeRoom(tiles [][]Tile, room engine.BoundingBox) {
|
||||
func makeRoom(tiles [][]Tile_V2, room engine.BoundingBox) {
|
||||
width := room.Size().Width()
|
||||
height := room.Size().Height()
|
||||
x := room.Position().X()
|
||||
y := room.Position().Y()
|
||||
|
||||
for w := x; w < x+width+1; w++ {
|
||||
tiles[y][w] = CreateStaticTile(w, y, TileTypeWall())
|
||||
tiles[y+height][w] = CreateStaticTile(w, y+height, TileTypeWall())
|
||||
tiles[y][w] = Tile_Wall()
|
||||
tiles[y+height][w] = Tile_Wall()
|
||||
}
|
||||
|
||||
for h := y; h < y+height+1; h++ {
|
||||
tiles[h][x] = CreateStaticTile(x, h, TileTypeWall())
|
||||
tiles[h][x+width] = CreateStaticTile(x+width, h, TileTypeWall())
|
||||
tiles[h][x] = Tile_Wall()
|
||||
tiles[h][x+width] = Tile_Wall()
|
||||
}
|
||||
|
||||
for h := y + 1; h < y+height; h++ {
|
||||
for w := x + 1; w < x+width; w++ {
|
||||
tiles[h][w] = CreateStaticTile(w, h, TileTypeGround())
|
||||
tiles[h][w] = Tile_Ground()
|
||||
}
|
||||
}
|
||||
}
|
13
game/model/world_generate_empty_map.go
Normal file
13
game/model/world_generate_empty_map.go
Normal 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
188
game/model/world_map.go
Normal 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(),
|
||||
}
|
||||
}
|
127
game/model/world_multilevel_map.go
Normal file
127
game/model/world_multilevel_map.go
Normal 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
368
game/model/world_tile.go
Normal 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{}
|
||||
// }
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
}
|
|
@ -2,9 +2,8 @@ package state
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/rpg"
|
||||
"mvvasilev/last_light/game/turns"
|
||||
"mvvasilev/last_light/game/model"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"mvvasilev/last_light/game/ui/menu"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
|
@ -16,8 +15,8 @@ const (
|
|||
)
|
||||
|
||||
type CharacterCreationState struct {
|
||||
turnSystem *turns.TurnSystem
|
||||
inputSystem *input.InputSystem
|
||||
turnSystem *systems.TurnSystem
|
||||
inputSystem *systems.InputSystem
|
||||
|
||||
startGame bool
|
||||
|
||||
|
@ -25,26 +24,26 @@ type CharacterCreationState struct {
|
|||
ccMenu *menu.CharacterCreationMenu
|
||||
}
|
||||
|
||||
func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *CharacterCreationState {
|
||||
func CreateCharacterCreationState(turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem) *CharacterCreationState {
|
||||
|
||||
menuState := &menu.CharacterCreationMenuState{
|
||||
AvailablePoints: 21,
|
||||
CurrentHighlight: 0,
|
||||
Stats: []*menu.StatState{
|
||||
{
|
||||
Stat: rpg.Stat_Attributes_Strength,
|
||||
Stat: model.Stat_Attributes_Strength,
|
||||
Value: 1,
|
||||
},
|
||||
{
|
||||
Stat: rpg.Stat_Attributes_Dexterity,
|
||||
Stat: model.Stat_Attributes_Dexterity,
|
||||
Value: 1,
|
||||
},
|
||||
{
|
||||
Stat: rpg.Stat_Attributes_Intelligence,
|
||||
Stat: model.Stat_Attributes_Intelligence,
|
||||
Value: 1,
|
||||
},
|
||||
{
|
||||
Stat: rpg.Stat_Attributes_Constitution,
|
||||
Stat: model.Stat_Attributes_Constitution,
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
|
@ -58,7 +57,12 @@ func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *inp
|
|||
}
|
||||
|
||||
ccs.menuState.RandomizeCharacter = func() {
|
||||
stats := rpg.RandomStats(21, 1, 20, []rpg.Stat{rpg.Stat_Attributes_Strength, rpg.Stat_Attributes_Constitution, rpg.Stat_Attributes_Intelligence, rpg.Stat_Attributes_Dexterity})
|
||||
stats := model.RandomStats(21, 1, 20, []model.Stat{
|
||||
model.Stat_Attributes_Strength,
|
||||
model.Stat_Attributes_Constitution,
|
||||
model.Stat_Attributes_Intelligence,
|
||||
model.Stat_Attributes_Dexterity,
|
||||
})
|
||||
|
||||
ccs.menuState.AvailablePoints = 0
|
||||
ccs.menuState.Stats = []*menu.StatState{}
|
||||
|
@ -84,8 +88,8 @@ func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *inp
|
|||
return ccs
|
||||
}
|
||||
|
||||
func (ccs *CharacterCreationState) InputContext() input.Context {
|
||||
return input.InputContext_Menu
|
||||
func (ccs *CharacterCreationState) InputContext() systems.InputContext {
|
||||
return systems.InputContext_Menu
|
||||
}
|
||||
|
||||
func (ccs *CharacterCreationState) IncreaseStatValue() {
|
||||
|
@ -125,7 +129,7 @@ func (ccs *CharacterCreationState) DecreaseStatValue() {
|
|||
|
||||
func (ccs *CharacterCreationState) OnTick(dt int64) GameState {
|
||||
if ccs.startGame {
|
||||
stats := map[rpg.Stat]int{}
|
||||
stats := map[model.Stat]int{}
|
||||
|
||||
for _, s := range ccs.menuState.Stats {
|
||||
stats[s.Stat] = s.Value
|
||||
|
@ -137,23 +141,23 @@ func (ccs *CharacterCreationState) OnTick(dt int64) GameState {
|
|||
action := ccs.inputSystem.NextAction()
|
||||
|
||||
switch action {
|
||||
case input.InputAction_Menu_HighlightRight:
|
||||
case systems.InputAction_Menu_HighlightRight:
|
||||
ccs.IncreaseStatValue()
|
||||
case input.InputAction_Menu_HighlightLeft:
|
||||
case systems.InputAction_Menu_HighlightLeft:
|
||||
ccs.DecreaseStatValue()
|
||||
case input.InputAction_Menu_HighlightDown:
|
||||
case systems.InputAction_Menu_HighlightDown:
|
||||
if ccs.menuState.CurrentHighlight > len(ccs.menuState.Stats) {
|
||||
break
|
||||
}
|
||||
|
||||
ccs.menuState.CurrentHighlight++
|
||||
case input.InputAction_Menu_HighlightUp:
|
||||
case systems.InputAction_Menu_HighlightUp:
|
||||
if ccs.menuState.CurrentHighlight == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
ccs.menuState.CurrentHighlight--
|
||||
case input.InputAction_Menu_Select:
|
||||
case systems.InputAction_Menu_Select:
|
||||
ccs.ccMenu.SelectHighlight()
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,13 @@ package state
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/turns"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"mvvasilev/last_light/game/ui"
|
||||
)
|
||||
|
||||
type DialogState struct {
|
||||
inputSystem *input.InputSystem
|
||||
turnSystem *turns.TurnSystem
|
||||
inputSystem *systems.InputSystem
|
||||
turnSystem *systems.TurnSystem
|
||||
|
||||
prevState GameState
|
||||
|
||||
|
@ -18,7 +17,7 @@ type DialogState struct {
|
|||
returnToPreviousState bool
|
||||
}
|
||||
|
||||
func CreateDialogState(inputSystem *input.InputSystem, turnSystem *turns.TurnSystem, dialog *ui.UIDialog, prevState GameState) *DialogState {
|
||||
func CreateDialogState(inputSystem *systems.InputSystem, turnSystem *systems.TurnSystem, dialog *ui.UIDialog, prevState GameState) *DialogState {
|
||||
return &DialogState{
|
||||
inputSystem: inputSystem,
|
||||
turnSystem: turnSystem,
|
||||
|
@ -28,12 +27,12 @@ func CreateDialogState(inputSystem *input.InputSystem, turnSystem *turns.TurnSys
|
|||
}
|
||||
}
|
||||
|
||||
func (s *DialogState) InputContext() input.Context {
|
||||
return input.InputContext_Menu
|
||||
func (s *DialogState) InputContext() systems.InputContext {
|
||||
return systems.InputContext_Menu
|
||||
}
|
||||
|
||||
func (ds *DialogState) OnTick(dt int64) GameState {
|
||||
if ds.inputSystem.NextAction() == input.InputAction_Menu_Select {
|
||||
if ds.inputSystem.NextAction() == systems.InputAction_Menu_Select {
|
||||
ds.returnToPreviousState = true
|
||||
ds.dialog.Select()
|
||||
}
|
||||
|
|
84
game/state/game_over_state.go
Normal file
84
game/state/game_over_state.go
Normal 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,
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@ package state
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
)
|
||||
|
||||
type GameState interface {
|
||||
InputContext() input.Context
|
||||
InputContext() systems.InputContext
|
||||
OnTick(dt int64) GameState
|
||||
CollectDrawables() []engine.Drawable
|
||||
}
|
||||
|
|
|
@ -2,17 +2,18 @@ package state
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/player"
|
||||
"mvvasilev/last_light/game/turns"
|
||||
"mvvasilev/last_light/game/model"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"mvvasilev/last_light/game/ui/menu"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type InventoryScreenState struct {
|
||||
inputSystem *input.InputSystem
|
||||
turnSystem *turns.TurnSystem
|
||||
eventLog *engine.GameEventLog
|
||||
inputSystem *systems.InputSystem
|
||||
turnSystem *systems.TurnSystem
|
||||
dungeon *model.Dungeon
|
||||
|
||||
prevState GameState
|
||||
exitMenu bool
|
||||
|
@ -20,12 +21,13 @@ type InventoryScreenState struct {
|
|||
inventoryMenu *menu.PlayerInventoryMenu
|
||||
selectedInventorySlot engine.Position
|
||||
|
||||
player *player.Player
|
||||
player *model.Player_V2
|
||||
}
|
||||
|
||||
func CreateInventoryScreenState(inputSystem *input.InputSystem, turnSystem *turns.TurnSystem, player *player.Player, prevState GameState) *InventoryScreenState {
|
||||
func CreateInventoryScreenState(eventLog *engine.GameEventLog, dungeon *model.Dungeon, inputSystem *systems.InputSystem, turnSystem *systems.TurnSystem, player *model.Player_V2, prevState GameState) *InventoryScreenState {
|
||||
iss := new(InventoryScreenState)
|
||||
|
||||
iss.eventLog = eventLog
|
||||
iss.inputSystem = inputSystem
|
||||
iss.turnSystem = turnSystem
|
||||
iss.prevState = prevState
|
||||
|
@ -33,12 +35,13 @@ func CreateInventoryScreenState(inputSystem *input.InputSystem, turnSystem *turn
|
|||
iss.selectedInventorySlot = engine.PositionAt(0, 0)
|
||||
iss.exitMenu = false
|
||||
iss.inventoryMenu = menu.CreatePlayerInventoryMenu(43, 0, player.Inventory(), tcell.StyleDefault, tcell.StyleDefault.Background(tcell.ColorDarkSlateGray))
|
||||
iss.dungeon = dungeon
|
||||
|
||||
return iss
|
||||
}
|
||||
|
||||
func (s *InventoryScreenState) InputContext() input.Context {
|
||||
return input.InputContext_Inventory
|
||||
func (s *InventoryScreenState) InputContext() systems.InputContext {
|
||||
return systems.InputContext_Inventory
|
||||
}
|
||||
|
||||
func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) {
|
||||
|
@ -46,32 +49,53 @@ func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) {
|
|||
nextState = iss
|
||||
|
||||
switch nextAction {
|
||||
case input.InputAction_Menu_Exit:
|
||||
case systems.InputAction_Menu_Exit:
|
||||
nextState = iss.prevState
|
||||
case input.InputAction_DropItem:
|
||||
case systems.InputAction_InteractItem:
|
||||
item := iss.player.Inventory().ItemAt(iss.selectedInventorySlot.XY())
|
||||
|
||||
if item == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if item.Usable() != nil {
|
||||
item.Usable().Use(iss.eventLog, iss.dungeon, iss.player)
|
||||
}
|
||||
|
||||
if item.Equippable() != nil {
|
||||
if iss.player.Inventory().AtSlot(item.Equippable().Slot) != nil {
|
||||
iss.player.Inventory().Push(iss.player.Inventory().AtSlot(item.Equippable().Slot))
|
||||
}
|
||||
|
||||
iss.player.Inventory().Equip(item, item.Equippable().Slot)
|
||||
}
|
||||
|
||||
iss.player.Inventory().ReduceQuantityAt(iss.selectedInventorySlot.X(), iss.selectedInventorySlot.Y(), 1)
|
||||
|
||||
case systems.InputAction_DropItem:
|
||||
iss.player.Inventory().Drop(iss.selectedInventorySlot.XY())
|
||||
case input.InputAction_Menu_HighlightUp:
|
||||
case systems.InputAction_Menu_HighlightUp:
|
||||
if iss.selectedInventorySlot.Y() == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, -1)
|
||||
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY())
|
||||
case input.InputAction_Menu_HighlightDown:
|
||||
case systems.InputAction_Menu_HighlightDown:
|
||||
if iss.selectedInventorySlot.Y() == iss.player.Inventory().Shape().Height()-1 {
|
||||
break
|
||||
}
|
||||
|
||||
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, +1)
|
||||
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY())
|
||||
case input.InputAction_Menu_HighlightLeft:
|
||||
case systems.InputAction_Menu_HighlightLeft:
|
||||
if iss.selectedInventorySlot.X() == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(-1, 0)
|
||||
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY())
|
||||
case input.InputAction_Menu_HighlightRight:
|
||||
case systems.InputAction_Menu_HighlightRight:
|
||||
if iss.selectedInventorySlot.X() == iss.player.Inventory().Shape().Width()-1 {
|
||||
break
|
||||
}
|
||||
|
|
|
@ -2,16 +2,15 @@ package state
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/turns"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"mvvasilev/last_light/game/ui"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type MainMenuState struct {
|
||||
turnSystem *turns.TurnSystem
|
||||
inputSystem *input.InputSystem
|
||||
turnSystem *systems.TurnSystem
|
||||
inputSystem *systems.InputSystem
|
||||
|
||||
menuTitle *engine.Raw
|
||||
buttons []*ui.UISimpleButton
|
||||
|
@ -21,7 +20,7 @@ type MainMenuState struct {
|
|||
startNewGame bool
|
||||
}
|
||||
|
||||
func CreateMainMenuState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *MainMenuState {
|
||||
func CreateMainMenuState(turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem) *MainMenuState {
|
||||
turnSystem.Clear()
|
||||
|
||||
state := new(MainMenuState)
|
||||
|
@ -43,7 +42,7 @@ func CreateMainMenuState(turnSystem *turns.TurnSystem, inputSystem *input.InputS
|
|||
state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 7, "New Game", tcell.StyleDefault, highlightStyle, func() {
|
||||
state.startNewGame = true
|
||||
}))
|
||||
state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 9, "Load Game", tcell.StyleDefault, highlightStyle, func() {
|
||||
state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 9, "Key Bindings // TODO", tcell.StyleDefault, highlightStyle, func() {
|
||||
|
||||
}))
|
||||
state.buttons = append(state.buttons, ui.CreateSimpleButton(11, 11, "Quit", tcell.StyleDefault, highlightStyle, func() {
|
||||
|
@ -56,26 +55,26 @@ func CreateMainMenuState(turnSystem *turns.TurnSystem, inputSystem *input.InputS
|
|||
return state
|
||||
}
|
||||
|
||||
func (s *MainMenuState) InputContext() input.Context {
|
||||
return input.InputContext_Menu
|
||||
func (s *MainMenuState) InputContext() systems.InputContext {
|
||||
return systems.InputContext_Menu
|
||||
}
|
||||
|
||||
func (mms *MainMenuState) OnTick(dt int64) GameState {
|
||||
nextAction := mms.inputSystem.NextAction()
|
||||
|
||||
if nextAction == input.InputAction_Menu_HighlightDown {
|
||||
if nextAction == systems.InputAction_Menu_HighlightDown {
|
||||
mms.buttons[mms.currButtonSelected].Unhighlight()
|
||||
mms.currButtonSelected = engine.LimitIncrement(mms.currButtonSelected, 2)
|
||||
mms.buttons[mms.currButtonSelected].Highlight()
|
||||
}
|
||||
|
||||
if nextAction == input.InputAction_Menu_HighlightUp {
|
||||
if nextAction == systems.InputAction_Menu_HighlightUp {
|
||||
mms.buttons[mms.currButtonSelected].Unhighlight()
|
||||
mms.currButtonSelected = engine.LimitDecrement(mms.currButtonSelected, 0)
|
||||
mms.buttons[mms.currButtonSelected].Highlight()
|
||||
}
|
||||
|
||||
if nextAction == input.InputAction_Menu_Select {
|
||||
if nextAction == systems.InputAction_Menu_Select {
|
||||
mms.buttons[mms.currButtonSelected].Select()
|
||||
}
|
||||
|
||||
|
|
|
@ -2,16 +2,15 @@ package state
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/turns"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"mvvasilev/last_light/game/ui"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type PauseGameState struct {
|
||||
turnSystem *turns.TurnSystem
|
||||
inputSystem *input.InputSystem
|
||||
turnSystem *systems.TurnSystem
|
||||
inputSystem *systems.InputSystem
|
||||
|
||||
prevState GameState
|
||||
|
||||
|
@ -23,7 +22,7 @@ type PauseGameState struct {
|
|||
currButtonSelected int
|
||||
}
|
||||
|
||||
func PauseGame(prevState GameState, turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *PauseGameState {
|
||||
func PauseGame(prevState GameState, turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem) *PauseGameState {
|
||||
s := new(PauseGameState)
|
||||
|
||||
s.turnSystem = turnSystem
|
||||
|
@ -67,23 +66,23 @@ func PauseGame(prevState GameState, turnSystem *turns.TurnSystem, inputSystem *i
|
|||
return s
|
||||
}
|
||||
|
||||
func (s *PauseGameState) InputContext() input.Context {
|
||||
return input.InputContext_Menu
|
||||
func (s *PauseGameState) InputContext() systems.InputContext {
|
||||
return systems.InputContext_Menu
|
||||
}
|
||||
|
||||
func (pg *PauseGameState) OnTick(dt int64) GameState {
|
||||
switch pg.inputSystem.NextAction() {
|
||||
case input.InputAction_Menu_Exit:
|
||||
case systems.InputAction_Menu_Exit:
|
||||
pg.unpauseGame = true
|
||||
case input.InputAction_Menu_HighlightDown:
|
||||
case systems.InputAction_Menu_HighlightDown:
|
||||
pg.buttons[pg.currButtonSelected].Unhighlight()
|
||||
pg.currButtonSelected = engine.LimitIncrement(pg.currButtonSelected, 1)
|
||||
pg.buttons[pg.currButtonSelected].Highlight()
|
||||
case input.InputAction_Menu_HighlightUp:
|
||||
case systems.InputAction_Menu_HighlightUp:
|
||||
pg.buttons[pg.currButtonSelected].Unhighlight()
|
||||
pg.currButtonSelected = engine.LimitDecrement(pg.currButtonSelected, 0)
|
||||
pg.buttons[pg.currButtonSelected].Highlight()
|
||||
case input.InputAction_Menu_Select:
|
||||
case systems.InputAction_Menu_Select:
|
||||
pg.buttons[pg.currButtonSelected].Select()
|
||||
}
|
||||
|
||||
|
|
|
@ -3,31 +3,27 @@ package state
|
|||
import (
|
||||
"fmt"
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/npc"
|
||||
"mvvasilev/last_light/game/player"
|
||||
"mvvasilev/last_light/game/rpg"
|
||||
"mvvasilev/last_light/game/turns"
|
||||
"mvvasilev/last_light/game/model"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"mvvasilev/last_light/game/ui"
|
||||
"mvvasilev/last_light/game/world"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/views"
|
||||
)
|
||||
|
||||
type PlayingState struct {
|
||||
turnSystem *turns.TurnSystem
|
||||
inputSystem *input.InputSystem
|
||||
turnSystem *systems.TurnSystem
|
||||
inputSystem *systems.InputSystem
|
||||
|
||||
player *player.Player
|
||||
someNPC npc.RPGNPC
|
||||
player *model.Player_V2
|
||||
someNPC model.Entity_V2
|
||||
|
||||
eventLog *engine.GameEventLog
|
||||
uiEventLog *ui.UIEventLog
|
||||
|
||||
healthBar *ui.UIHealthBar
|
||||
|
||||
dungeon *world.Dungeon
|
||||
dungeon *model.Dungeon
|
||||
|
||||
viewport *engine.Viewport
|
||||
|
||||
|
@ -36,7 +32,7 @@ type PlayingState struct {
|
|||
nextGameState GameState
|
||||
}
|
||||
|
||||
func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem, playerStats map[rpg.Stat]int) *PlayingState {
|
||||
func CreatePlayingState(turnSystem *systems.TurnSystem, inputSystem *systems.InputSystem, playerStats map[model.Stat]int) *PlayingState {
|
||||
turnSystem.Clear()
|
||||
|
||||
s := new(PlayingState)
|
||||
|
@ -46,11 +42,11 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy
|
|||
|
||||
mapSize := engine.SizeOf(128, 128)
|
||||
|
||||
s.dungeon = world.CreateDungeon(mapSize.Width(), mapSize.Height(), 1)
|
||||
s.dungeon = model.CreateDungeon(mapSize.Width(), mapSize.Height(), 1)
|
||||
|
||||
s.player = player.CreatePlayer(
|
||||
s.dungeon.CurrentLevel().PlayerSpawnPoint().X(),
|
||||
s.dungeon.CurrentLevel().PlayerSpawnPoint().Y(),
|
||||
s.player = model.CreatePlayer_V2(
|
||||
s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position.X(),
|
||||
s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position.Y(),
|
||||
playerStats,
|
||||
)
|
||||
|
||||
|
@ -58,30 +54,34 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy
|
|||
requeue = true
|
||||
complete = false
|
||||
|
||||
if s.player.HealthData().IsDead {
|
||||
s.nextGameState = CreateGameOverState(inputSystem)
|
||||
}
|
||||
|
||||
switch inputSystem.NextAction() {
|
||||
case input.InputAction_PauseGame:
|
||||
case systems.InputAction_PauseGame:
|
||||
s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem)
|
||||
case input.InputAction_OpenInventory:
|
||||
s.nextGameState = CreateInventoryScreenState(s.inputSystem, s.turnSystem, s.player, s)
|
||||
case input.InputAction_PickUpItem:
|
||||
case systems.InputAction_OpenInventory:
|
||||
s.nextGameState = CreateInventoryScreenState(s.eventLog, s.dungeon, s.inputSystem, s.turnSystem, s.player, s)
|
||||
case systems.InputAction_PickUpItem:
|
||||
s.PickUpItemUnderPlayer()
|
||||
complete = true
|
||||
case input.InputAction_Interact:
|
||||
case systems.InputAction_Interact:
|
||||
s.InteractBelowPlayer()
|
||||
complete = true
|
||||
case input.InputAction_OpenLogs:
|
||||
case systems.InputAction_OpenLogs:
|
||||
s.viewShortLogs = !s.viewShortLogs
|
||||
case input.InputAction_MovePlayer_East:
|
||||
s.MovePlayer(npc.East)
|
||||
case systems.InputAction_MovePlayer_East:
|
||||
s.MovePlayer(model.East)
|
||||
complete = true
|
||||
case input.InputAction_MovePlayer_West:
|
||||
s.MovePlayer(npc.West)
|
||||
case systems.InputAction_MovePlayer_West:
|
||||
s.MovePlayer(model.West)
|
||||
complete = true
|
||||
case input.InputAction_MovePlayer_North:
|
||||
s.MovePlayer(npc.North)
|
||||
case systems.InputAction_MovePlayer_North:
|
||||
s.MovePlayer(model.North)
|
||||
complete = true
|
||||
case input.InputAction_MovePlayer_South:
|
||||
s.MovePlayer(npc.South)
|
||||
case systems.InputAction_MovePlayer_South:
|
||||
s.MovePlayer(model.South)
|
||||
complete = true
|
||||
default:
|
||||
}
|
||||
|
@ -89,13 +89,12 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy
|
|||
return
|
||||
})
|
||||
|
||||
s.someNPC = npc.CreateRPGNPC(
|
||||
s.dungeon.CurrentLevel().NextLevelStaircase().X(),
|
||||
s.dungeon.CurrentLevel().NextLevelStaircase().Y(),
|
||||
"NPC",
|
||||
'n',
|
||||
tcell.StyleDefault,
|
||||
rpg.RandomStats(21, 1, 20, []rpg.Stat{rpg.Stat_Attributes_Strength, rpg.Stat_Attributes_Constitution, rpg.Stat_Attributes_Intelligence, rpg.Stat_Attributes_Dexterity}),
|
||||
s.someNPC = model.CreateEntity(
|
||||
model.WithPosition(s.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position),
|
||||
model.WithName("NPC"),
|
||||
model.WithPresentation('n', tcell.StyleDefault),
|
||||
model.WithStats(model.RandomStats(21, 1, 20, []model.Stat{model.Stat_Attributes_Strength, model.Stat_Attributes_Constitution, model.Stat_Attributes_Intelligence, model.Stat_Attributes_Dexterity})),
|
||||
model.WithHealthData(20, 20, false),
|
||||
)
|
||||
|
||||
s.turnSystem.Schedule(20, func() (complete bool, requeue bool) {
|
||||
|
@ -107,14 +106,14 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy
|
|||
s.eventLog = engine.CreateGameEventLog(100)
|
||||
|
||||
s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault)
|
||||
s.healthBar = ui.CreateHealthBar(68, 0, 12, 3, s.player.CurrentHealth(), rpg.BaseMaxHealth(s.player), tcell.StyleDefault)
|
||||
s.healthBar = ui.CreateHealthBar(68, 0, 12, 3, s.player, tcell.StyleDefault)
|
||||
|
||||
s.dungeon.CurrentLevel().AddEntity(s.player)
|
||||
s.dungeon.CurrentLevel().AddEntity(s.someNPC)
|
||||
|
||||
s.viewport = engine.CreateViewport(
|
||||
engine.PositionAt(0, 0),
|
||||
s.dungeon.CurrentLevel().PlayerSpawnPoint(),
|
||||
s.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position,
|
||||
engine.SizeOf(80, 24),
|
||||
tcell.StyleDefault,
|
||||
)
|
||||
|
@ -124,53 +123,89 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy
|
|||
return s
|
||||
}
|
||||
|
||||
func (s *PlayingState) InputContext() input.Context {
|
||||
return input.InputContext_Play
|
||||
func (s *PlayingState) InputContext() systems.InputContext {
|
||||
return systems.InputContext_Play
|
||||
}
|
||||
|
||||
func (ps *PlayingState) MovePlayer(direction npc.Direction) {
|
||||
if direction == npc.DirectionNone {
|
||||
func (ps *PlayingState) MovePlayer(direction model.Direction) {
|
||||
if direction == model.DirectionNone {
|
||||
return
|
||||
}
|
||||
|
||||
newPlayerPos := ps.player.Position().WithOffset(npc.MovementDirectionOffset(direction))
|
||||
|
||||
if ps.dungeon.CurrentLevel().IsTilePassable(newPlayerPos.XY()) {
|
||||
dx, dy := npc.MovementDirectionOffset(direction)
|
||||
ps.dungeon.CurrentLevel().MoveEntity(ps.player.UniqueId(), dx, dy)
|
||||
ps.viewport.SetCenter(ps.player.Position())
|
||||
|
||||
ps.eventLog.Log("You moved " + npc.DirectionName(direction))
|
||||
}
|
||||
newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(direction))
|
||||
|
||||
ent := ps.dungeon.CurrentLevel().EntityAt(newPlayerPos.XY())
|
||||
|
||||
// We are moving into an entity. Attack it.
|
||||
if ent != nil {
|
||||
switch rpge := ent.(type) {
|
||||
case npc.RPGNPC:
|
||||
hit, precision, evasion, dmg, dmgType := ps.player.CalculateAttack(rpge)
|
||||
|
||||
if !hit {
|
||||
ps.eventLog.Log(fmt.Sprintf("You attacked %v, but missed ( %v Evasion vs %v Precision)", rpge.Name(), evasion, precision))
|
||||
return
|
||||
}
|
||||
|
||||
rpge.Damage(dmg)
|
||||
ps.eventLog.Log(fmt.Sprintf("You attacked %v, and hit for %v %v damage", rpge.Name(), dmg, rpg.DamageTypeName(dmgType)))
|
||||
// We are moving into an entity with health data. Attack it.
|
||||
if ent != nil && ent.HealthData() != nil {
|
||||
if ent.HealthData().IsDead {
|
||||
// TODO: If the entity is dead, the player should be able to move through it.
|
||||
return
|
||||
}
|
||||
|
||||
ExecuteAttack(ps.eventLog, ps.player, ent)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ps.dungeon.CurrentLevel().IsTilePassable(newPlayerPos.XY()) {
|
||||
ps.dungeon.CurrentLevel().MoveEntityTo(ps.player.UniqueId(), newPlayerPos.X(), newPlayerPos.Y())
|
||||
ps.viewport.SetCenter(ps.player.Position())
|
||||
|
||||
ps.eventLog.Log("You moved " + model.DirectionName(direction))
|
||||
}
|
||||
}
|
||||
|
||||
func ExecuteAttack(eventLog *engine.GameEventLog, attacker, victim model.Entity_V2) {
|
||||
hit, precision, evasion, dmg, dmgType := CalculateAttack(attacker, victim)
|
||||
|
||||
attackerName := "Unknown"
|
||||
|
||||
if attacker.Named() != nil {
|
||||
attackerName = attacker.Named().Name
|
||||
}
|
||||
|
||||
victimName := "Unknown"
|
||||
|
||||
if victim.Named() != nil {
|
||||
victimName = victim.Named().Name
|
||||
}
|
||||
|
||||
if !hit {
|
||||
eventLog.Log(fmt.Sprintf("%s attacked %s, but missed ( %v Evasion vs %v Precision)", attackerName, victimName, evasion, precision))
|
||||
return
|
||||
}
|
||||
|
||||
victim.HealthData().Health -= dmg
|
||||
|
||||
if victim.HealthData().Health <= 0 {
|
||||
victim.HealthData().IsDead = true
|
||||
eventLog.Log(fmt.Sprintf("%s attacked %s, and was victorious ( %v Evasion vs %v Precision)", attackerName, victimName, evasion, precision))
|
||||
return
|
||||
}
|
||||
|
||||
eventLog.Log(fmt.Sprintf("%s attacked %s, and hit for %v %v damage", attackerName, victimName, dmg, model.DamageTypeName(dmgType)))
|
||||
}
|
||||
|
||||
func CalculateAttack(attacker, victim model.Entity_V2) (hit bool, precisionRoll, evasionRoll int, damage int, damageType model.DamageType) {
|
||||
if attacker.Equipped() != nil && attacker.Equipped().Inventory.AtSlot(model.EquippedSlotDominantHand) != nil {
|
||||
weapon := attacker.Equipped().Inventory.AtSlot(model.EquippedSlotDominantHand)
|
||||
|
||||
return model.PhysicalWeaponAttack(attacker, weapon, victim)
|
||||
} else {
|
||||
return model.UnarmedAttack(attacker, victim)
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *PlayingState) InteractBelowPlayer() {
|
||||
playerPos := ps.player.Position()
|
||||
|
||||
if playerPos == ps.dungeon.CurrentLevel().NextLevelStaircase() {
|
||||
if playerPos == ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position {
|
||||
ps.SwitchToNextLevel()
|
||||
return
|
||||
}
|
||||
|
||||
if playerPos == ps.dungeon.CurrentLevel().PreviousLevelStaircase() {
|
||||
if playerPos == ps.dungeon.CurrentLevel().Ground().PreviousLevelStaircase().Position {
|
||||
ps.SwitchToPreviousLevel()
|
||||
return
|
||||
}
|
||||
|
@ -200,11 +235,11 @@ func (ps *PlayingState) SwitchToNextLevel() {
|
|||
|
||||
ps.dungeon.MoveToNextLevel()
|
||||
|
||||
ps.player.MoveTo(ps.dungeon.CurrentLevel().PlayerSpawnPoint())
|
||||
ps.player.Positioned().Position = ps.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position
|
||||
|
||||
ps.viewport = engine.CreateViewport(
|
||||
engine.PositionAt(0, 0),
|
||||
ps.dungeon.CurrentLevel().PlayerSpawnPoint(),
|
||||
ps.dungeon.CurrentLevel().Ground().PlayerSpawnPoint().Position,
|
||||
engine.SizeOf(80, 24),
|
||||
tcell.StyleDefault,
|
||||
)
|
||||
|
@ -236,11 +271,11 @@ func (ps *PlayingState) SwitchToPreviousLevel() {
|
|||
|
||||
ps.dungeon.MoveToPreviousLevel()
|
||||
|
||||
ps.player.MoveTo(ps.dungeon.CurrentLevel().NextLevelStaircase())
|
||||
ps.player.Positioned().Position = ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position
|
||||
|
||||
ps.viewport = engine.CreateViewport(
|
||||
engine.PositionAt(0, 0),
|
||||
ps.dungeon.CurrentLevel().NextLevelStaircase(),
|
||||
ps.dungeon.CurrentLevel().Ground().NextLevelStaircase().Position,
|
||||
engine.SizeOf(80, 24),
|
||||
tcell.StyleDefault,
|
||||
)
|
||||
|
@ -263,9 +298,12 @@ func (ps *PlayingState) PickUpItemUnderPlayer() {
|
|||
return
|
||||
}
|
||||
|
||||
itemName, _ := item.Name()
|
||||
|
||||
ps.eventLog.Log("You picked up " + itemName)
|
||||
if item.Named() != nil {
|
||||
itemName := item.Named().Name
|
||||
ps.eventLog.Log("You picked up " + itemName)
|
||||
} else {
|
||||
ps.eventLog.Log("You picked up an item")
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *PlayingState) HasLineOfSight(start, end engine.Position) bool {
|
||||
|
@ -285,25 +323,30 @@ func (ps *PlayingState) PlayerWithinHitRange(pos engine.Position) bool {
|
|||
}
|
||||
|
||||
func (ps *PlayingState) CalcPathToPlayerAndMove() {
|
||||
if ps.someNPC.HealthData().IsDead {
|
||||
ps.dungeon.CurrentLevel().DropEntity(ps.someNPC.UniqueId())
|
||||
return
|
||||
}
|
||||
|
||||
playerVisibleAndInRange := false
|
||||
|
||||
if ps.someNPC.Position().Distance(ps.player.Position()) < 20 && ps.HasLineOfSight(ps.someNPC.Position(), ps.player.Position()) {
|
||||
if ps.someNPC.Positioned().Position.Distance(ps.player.Position()) < 20 && ps.HasLineOfSight(ps.someNPC.Positioned().Position, ps.player.Position()) {
|
||||
playerVisibleAndInRange = true
|
||||
}
|
||||
|
||||
if !playerVisibleAndInRange {
|
||||
randomMove := npc.Direction(engine.RandInt(int(npc.DirectionNone), int(npc.East)))
|
||||
randomMove := model.Direction(engine.RandInt(int(model.DirectionNone), int(model.East)))
|
||||
|
||||
nextPos := ps.someNPC.Position()
|
||||
nextPos := ps.someNPC.Positioned().Position
|
||||
|
||||
switch randomMove {
|
||||
case npc.North:
|
||||
case model.North:
|
||||
nextPos = nextPos.WithOffset(0, -1)
|
||||
case npc.South:
|
||||
case model.South:
|
||||
nextPos = nextPos.WithOffset(0, +1)
|
||||
case npc.West:
|
||||
case model.West:
|
||||
nextPos = nextPos.WithOffset(-1, 0)
|
||||
case npc.East:
|
||||
case model.East:
|
||||
nextPos = nextPos.WithOffset(+1, 0)
|
||||
default:
|
||||
return
|
||||
|
@ -320,23 +363,12 @@ func (ps *PlayingState) CalcPathToPlayerAndMove() {
|
|||
return
|
||||
}
|
||||
|
||||
if ps.PlayerWithinHitRange(ps.someNPC.Position()) {
|
||||
hit, precision, evasion, dmg, dmgType := ps.player.CalculateAttack(ps.player)
|
||||
|
||||
if !hit {
|
||||
ps.eventLog.Log(fmt.Sprintf("%v attacked you, but missed ( %v Evasion vs %v Precision)", ps.someNPC.Name(), evasion, precision))
|
||||
return
|
||||
}
|
||||
|
||||
ps.player.Damage(dmg)
|
||||
ps.healthBar.SetHealth(ps.player.CurrentHealth())
|
||||
ps.eventLog.Log(fmt.Sprintf("%v attacked you, and hit for %v %v damage", ps.someNPC.Name(), dmg, rpg.DamageTypeName(dmgType)))
|
||||
|
||||
return
|
||||
if ps.PlayerWithinHitRange(ps.someNPC.Positioned().Position) {
|
||||
ExecuteAttack(ps.eventLog, ps.someNPC, ps.player)
|
||||
}
|
||||
|
||||
pathToPlayer := engine.FindPath(
|
||||
ps.someNPC.Position(),
|
||||
ps.someNPC.Positioned().Position,
|
||||
ps.player.Position(),
|
||||
func(x, y int) bool {
|
||||
if x == ps.player.Position().X() && y == ps.player.Position().Y() {
|
||||
|
@ -371,13 +403,13 @@ func (ps *PlayingState) OnTick(dt int64) (nextState GameState) {
|
|||
func (ps *PlayingState) CollectDrawables() []engine.Drawable {
|
||||
mainCameraDrawingInstructions := engine.CreateDrawingInstructions(func(v views.View) {
|
||||
visibilityMap := engine.ComputeFOV(
|
||||
func(x, y int) world.Tile {
|
||||
ps.dungeon.CurrentLevel().Flatten().MarkExplored(x, y)
|
||||
func(x, y int) model.Tile_V2 {
|
||||
model.Map_MarkExplored(ps.dungeon.CurrentLevel().Ground(), x, y)
|
||||
|
||||
return ps.dungeon.CurrentLevel().TileAt(x, y)
|
||||
},
|
||||
func(x, y int) bool { return ps.dungeon.CurrentLevel().Flatten().IsInBounds(x, y) },
|
||||
func(x, y int) bool { return ps.dungeon.CurrentLevel().Flatten().TileAt(x, y).Opaque() },
|
||||
func(x, y int) bool { return model.Map_IsInBounds(ps.dungeon.CurrentLevel().Ground(), x, y) },
|
||||
func(x, y int) bool { return ps.dungeon.CurrentLevel().TileAt(x, y).Opaque() },
|
||||
ps.player.Position().X(), ps.player.Position().Y(),
|
||||
13,
|
||||
)
|
||||
|
@ -386,13 +418,21 @@ func (ps *PlayingState) CollectDrawables() []engine.Drawable {
|
|||
tile := visibilityMap[engine.PositionAt(x, y)]
|
||||
|
||||
if tile != nil {
|
||||
return tile.Presentation()
|
||||
if tile.Entity() != nil {
|
||||
return tile.Entity().Entity.Presentable().Rune, tile.Entity().Entity.Presentable().Style
|
||||
}
|
||||
|
||||
if tile.Item() != nil {
|
||||
return tile.Item().Item.TileIcon(), tile.Item().Item.Style()
|
||||
}
|
||||
|
||||
return tile.DefaultPresentation()
|
||||
}
|
||||
|
||||
explored := ps.dungeon.CurrentLevel().Flatten().ExploredTileAt(x, y)
|
||||
explored := model.Map_ExploredTileAt(ps.dungeon.CurrentLevel().Ground(), x, y)
|
||||
|
||||
if explored != nil {
|
||||
return explored.Presentation()
|
||||
return explored.DefaultPresentation()
|
||||
}
|
||||
|
||||
return ' ', tcell.StyleDefault
|
||||
|
|
|
@ -2,14 +2,14 @@ package state
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
)
|
||||
|
||||
type QuitState struct {
|
||||
}
|
||||
|
||||
func (s *QuitState) InputContext() input.Context {
|
||||
return input.InputContext_Menu
|
||||
func (s *QuitState) InputContext() systems.InputContext {
|
||||
return systems.InputContext_Menu
|
||||
}
|
||||
|
||||
func (q *QuitState) OnTick(dt int64) GameState {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package input
|
||||
package systems
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type Context string
|
||||
type InputContext string
|
||||
|
||||
const (
|
||||
InputContext_Play = "play"
|
||||
|
@ -16,7 +16,7 @@ const (
|
|||
|
||||
type InputKey string
|
||||
|
||||
func InputKeyOf(context Context, mod tcell.ModMask, key tcell.Key, r rune) InputKey {
|
||||
func InputKeyOf(context InputContext, mod tcell.ModMask, key tcell.Key, r rune) InputKey {
|
||||
return InputKey(fmt.Sprintf("%v-%v-%v-%v", context, mod, key, r))
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ func (kb *InputSystem) Bind(key InputKey, action InputAction) {
|
|||
kb.keyBindings[key] = action
|
||||
}
|
||||
|
||||
func (kb *InputSystem) Input(context Context, ev *tcell.EventKey) {
|
||||
func (kb *InputSystem) Input(context InputContext, ev *tcell.EventKey) {
|
||||
kb.nextAction = kb.keyBindings[InputKeyOf(context, ev.Modifiers(), ev.Key(), ev.Rune())]
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package turns
|
||||
package systems
|
||||
|
||||
import "mvvasilev/last_light/engine"
|
||||
|
166
game/ui/item.go
166
game/ui/item.go
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -3,8 +3,8 @@ package menu
|
|||
import (
|
||||
"fmt"
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/rpg"
|
||||
"mvvasilev/last_light/game/model"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"mvvasilev/last_light/game/ui"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
|
@ -13,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
type statSelection struct {
|
||||
stat rpg.Stat
|
||||
stat model.Stat
|
||||
label *ui.UILabel
|
||||
plusButton *ui.UILabel
|
||||
statNumberLabel *ui.UILabel
|
||||
|
@ -21,7 +21,7 @@ type statSelection struct {
|
|||
}
|
||||
|
||||
type StatState struct {
|
||||
Stat rpg.Stat
|
||||
Stat model.Stat
|
||||
Value int
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,7 @@ func (ccm *CharacterCreationMenu) UpdateState(state *CharacterCreationMenuState)
|
|||
label: ui.CreateSingleLineUILabel(
|
||||
statX,
|
||||
3+i,
|
||||
rpg.StatLongName(s.Stat),
|
||||
model.StatLongName(s.Stat),
|
||||
labelStyle,
|
||||
),
|
||||
minusButton: ui.CreateSingleLineUILabel(
|
||||
|
@ -172,7 +172,7 @@ func (ccm *CharacterCreationMenu) Size() engine.Size {
|
|||
return engine.SizeOf(engine.TERMINAL_SIZE_WIDTH, engine.TERMINAL_SIZE_HEIGHT)
|
||||
}
|
||||
|
||||
func (ccm *CharacterCreationMenu) Input(inputAction input.InputAction) {
|
||||
func (ccm *CharacterCreationMenu) Input(inputAction systems.InputAction) {
|
||||
|
||||
}
|
||||
|
|
@ -3,9 +3,8 @@ package menu
|
|||
import (
|
||||
"fmt"
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/item"
|
||||
"mvvasilev/last_light/game/rpg"
|
||||
"mvvasilev/last_light/game/model"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"mvvasilev/last_light/game/ui"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
|
@ -14,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
type PlayerInventoryMenu struct {
|
||||
inventory *item.EquippedInventory
|
||||
inventory *model.EquippedInventory
|
||||
|
||||
inventoryMenu *ui.UIWindow
|
||||
armourLabel *ui.UILabel
|
||||
|
@ -26,12 +25,11 @@ type PlayerInventoryMenu struct {
|
|||
inventoryGrid *engine.Grid
|
||||
playerItems *engine.ArbitraryDrawable
|
||||
selectedItem *engine.ArbitraryDrawable
|
||||
help *ui.UILabel
|
||||
|
||||
selectedInventorySlot engine.Position
|
||||
}
|
||||
|
||||
func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory, style tcell.Style, highlightStyle tcell.Style) *PlayerInventoryMenu {
|
||||
func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventory, style tcell.Style, highlightStyle tcell.Style) *PlayerInventoryMenu {
|
||||
menu := new(PlayerInventoryMenu)
|
||||
|
||||
menu.inventory = playerInventory
|
||||
|
@ -94,25 +92,19 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory
|
|||
continue
|
||||
}
|
||||
|
||||
style := item.Type().Style()
|
||||
style := item.Style()
|
||||
|
||||
if isHighlighted {
|
||||
style = highlightStyle
|
||||
}
|
||||
|
||||
ui.CreateSingleLineUILabel(
|
||||
menu.drawItemSlot(
|
||||
menu.inventoryGrid.Position().X()+1+x*4,
|
||||
menu.inventoryGrid.Position().Y()+y*2,
|
||||
fmt.Sprintf("%03d", item.Quantity()),
|
||||
item,
|
||||
style,
|
||||
).Draw(v)
|
||||
|
||||
ui.CreateSingleLineUILabel(
|
||||
menu.inventoryGrid.Position().X()+1+x*4,
|
||||
menu.inventoryGrid.Position().Y()+1+y*2,
|
||||
item.Type().Icon(),
|
||||
style,
|
||||
).Draw(v)
|
||||
v,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -124,19 +116,30 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory
|
|||
return
|
||||
}
|
||||
|
||||
switch it := item.(type) {
|
||||
case rpg.RPGItem:
|
||||
ui.CreateUIRPGItem(x+2, y+14, it, style).Draw(v)
|
||||
default:
|
||||
ui.CreateUIBasicItem(x+2, y+14, it, style).Draw(v)
|
||||
}
|
||||
ui.CreateUIItem(x+2, y+14, item, style).Draw(v)
|
||||
})
|
||||
|
||||
menu.help = ui.CreateSingleLineUILabel(x+2, y+22, "hjkl - move, x - drop, e - equip", style)
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
func (pim *PlayerInventoryMenu) drawItemSlot(screenX, screenY int, item model.Item_V2, style tcell.Style, v views.View) {
|
||||
if item.Quantifiable() != nil {
|
||||
ui.CreateSingleLineUILabel(
|
||||
screenX,
|
||||
screenY,
|
||||
fmt.Sprintf("%03d", item.Quantifiable().CurrentQuantity),
|
||||
style,
|
||||
).Draw(v)
|
||||
}
|
||||
|
||||
ui.CreateSingleLineUILabel(
|
||||
screenX,
|
||||
screenY+1,
|
||||
item.Icon(),
|
||||
style,
|
||||
).Draw(v)
|
||||
}
|
||||
|
||||
func (pim *PlayerInventoryMenu) MoveTo(x int, y int) {
|
||||
|
||||
}
|
||||
|
@ -149,7 +152,7 @@ func (pim *PlayerInventoryMenu) Size() engine.Size {
|
|||
return pim.inventoryMenu.Size()
|
||||
}
|
||||
|
||||
func (pim *PlayerInventoryMenu) Input(inputAction input.InputAction) {
|
||||
func (pim *PlayerInventoryMenu) Input(inputAction systems.InputAction) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -178,5 +181,4 @@ func (pim *PlayerInventoryMenu) Draw(v views.View) {
|
|||
pim.inventoryGrid.Draw(v)
|
||||
pim.playerItems.Draw(v)
|
||||
pim.selectedItem.Draw(v)
|
||||
pim.help.Draw(v)
|
||||
}
|
|
@ -2,14 +2,14 @@ package ui
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
)
|
||||
|
||||
type UIElement interface {
|
||||
MoveTo(x, y int)
|
||||
Position() engine.Position
|
||||
Size() engine.Size
|
||||
Input(inputAction input.InputAction)
|
||||
Input(inputAction systems.InputAction)
|
||||
|
||||
engine.Drawable
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package ui
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/views"
|
||||
|
@ -91,13 +91,13 @@ func (d *UIDialog) Size() engine.Size {
|
|||
return d.window.Size()
|
||||
}
|
||||
|
||||
func (d *UIDialog) Input(inputAction input.InputAction) {
|
||||
if inputAction == input.InputAction_Menu_HighlightLeft {
|
||||
func (d *UIDialog) Input(inputAction systems.InputAction) {
|
||||
if inputAction == systems.InputAction_Menu_HighlightLeft {
|
||||
if !d.yesBtn.IsHighlighted() {
|
||||
d.noBtn.Unhighlight()
|
||||
d.yesBtn.Highlight()
|
||||
}
|
||||
} else if inputAction == input.InputAction_Menu_HighlightRight {
|
||||
} else if inputAction == systems.InputAction_Menu_HighlightRight {
|
||||
if d.noBtn == nil {
|
||||
return
|
||||
}
|
|
@ -2,7 +2,7 @@ package ui
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/views"
|
||||
|
@ -40,7 +40,7 @@ func (uie *UIEventLog) Size() engine.Size {
|
|||
return uie.window.Size()
|
||||
}
|
||||
|
||||
func (uie *UIEventLog) Input(inputAction input.InputAction) {
|
||||
func (uie *UIEventLog) Input(inputAction systems.InputAction) {
|
||||
|
||||
}
|
||||
|
|
@ -4,7 +4,8 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/model"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/views"
|
||||
|
@ -12,35 +13,22 @@ import (
|
|||
)
|
||||
|
||||
type UIHealthBar struct {
|
||||
id uuid.UUID
|
||||
health int
|
||||
maxHealth int
|
||||
id uuid.UUID
|
||||
player *model.Player_V2
|
||||
|
||||
window *UIWindow
|
||||
|
||||
style tcell.Style
|
||||
}
|
||||
|
||||
// TODO: style for health bar fill
|
||||
// TODO: 'HP' title
|
||||
// TODO: test different percentages
|
||||
func CreateHealthBar(x, y, w, h, health, maxHealth int, style tcell.Style) *UIHealthBar {
|
||||
func CreateHealthBar(x, y, w, h int, player *model.Player_V2, style tcell.Style) *UIHealthBar {
|
||||
return &UIHealthBar{
|
||||
window: CreateWindow(x, y, w, h, "HP", style),
|
||||
health: health,
|
||||
maxHealth: maxHealth,
|
||||
style: style,
|
||||
window: CreateWindow(x, y, w, h, "HP", style),
|
||||
player: player,
|
||||
style: style,
|
||||
}
|
||||
}
|
||||
|
||||
func (uihp *UIHealthBar) SetHealth(health int) {
|
||||
uihp.health = health
|
||||
}
|
||||
|
||||
func (uihp *UIHealthBar) SetMaxHealth(maxHealth int) {
|
||||
uihp.maxHealth = maxHealth
|
||||
}
|
||||
|
||||
func (uihp *UIHealthBar) MoveTo(x int, y int) {
|
||||
uihp.window.MoveTo(x, y)
|
||||
}
|
||||
|
@ -53,7 +41,7 @@ func (uihp *UIHealthBar) Size() engine.Size {
|
|||
return uihp.window.Size()
|
||||
}
|
||||
|
||||
func (uihp *UIHealthBar) Input(inputAction input.InputAction) {
|
||||
func (uihp *UIHealthBar) Input(inputAction systems.InputAction) {
|
||||
}
|
||||
|
||||
func (uihp *UIHealthBar) UniqueId() uuid.UUID {
|
||||
|
@ -67,7 +55,7 @@ func (uihp *UIHealthBar) Draw(v views.View) {
|
|||
|
||||
stages := []rune{'█', '▓', '▒', '░'} // 0 = 1.0, 1 = 0.75, 2 = 0.5, 3 = 0.25
|
||||
|
||||
percentage := (float64(w) - 2.0) * (float64(uihp.health) / float64(uihp.maxHealth))
|
||||
percentage := (float64(w) - 2.0) * (float64(uihp.player.HealthData().Health) / float64(uihp.player.HealthData().MaxHealth))
|
||||
|
||||
whole := math.Trunc(percentage)
|
||||
last := percentage - whole
|
||||
|
@ -92,7 +80,7 @@ func (uihp *UIHealthBar) Draw(v views.View) {
|
|||
}
|
||||
}
|
||||
|
||||
hpText := fmt.Sprintf("%v/%v", uihp.health, uihp.maxHealth)
|
||||
hpText := fmt.Sprintf("%v/%v", uihp.player.HealthData().Health, uihp.player.HealthData().MaxHealth)
|
||||
|
||||
engine.DrawText(
|
||||
x+w/2-len(hpText)/2,
|
133
game/ui/ui_item.go
Normal file
133
game/ui/ui_item.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ package ui
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
|
@ -57,4 +57,4 @@ func (t *UILabel) Draw(v views.View) {
|
|||
t.text.Draw(v)
|
||||
}
|
||||
|
||||
func (t *UILabel) Input(inputAction input.InputAction) {}
|
||||
func (t *UILabel) Input(inputAction systems.InputAction) {}
|
|
@ -2,7 +2,7 @@ package ui
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
|
@ -97,6 +97,6 @@ func (sb *UISimpleButton) Draw(v views.View) {
|
|||
sb.text.Draw(v)
|
||||
}
|
||||
|
||||
func (sb *UISimpleButton) Input(inputAction input.InputAction) {
|
||||
func (sb *UISimpleButton) Input(inputAction systems.InputAction) {
|
||||
|
||||
}
|
|
@ -2,7 +2,7 @@ package ui
|
|||
|
||||
import (
|
||||
"mvvasilev/last_light/engine"
|
||||
"mvvasilev/last_light/game/input"
|
||||
"mvvasilev/last_light/game/systems"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
|
@ -63,5 +63,5 @@ func (w *UIWindow) Draw(v views.View) {
|
|||
}
|
||||
}
|
||||
|
||||
func (w *UIWindow) Input(inputAction input.InputAction) {
|
||||
func (w *UIWindow) Input(inputAction systems.InputAction) {
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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) {
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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{}
|
||||
}
|
Loading…
Add table
Reference in a new issue