Add fov, add rpg system, add rpg items

This commit is contained in:
Miroslav Vasilev 2024-05-21 23:08:51 +03:00
parent 3c83d97a34
commit b30dc8dec3
27 changed files with 1325 additions and 361 deletions

View file

@ -1,7 +1,8 @@
# Last Light
- Roguelike RPG
- Inventory System
- ~~FOV with explored tiles greyed out~~
- ~~Inventory System~~
- Weapons & Armor
- Head
- Chest
@ -11,6 +12,7 @@
- Right Hand
- Damage Types
- Physical
- Unarmed
- Slashing
- Piercing
- Bludgeoning
@ -24,7 +26,7 @@
- 9-level Dungeon
- 4 types of dungeon levels:
- Caverns ( Cellular Automata )
- Dungeon ( Maze w/ Rooms )
- ~~Dungeon ( Maze w/ Rooms )~~
- Mine ( Broguelike )
- Underground City ( Caverns + Dungeon combo )
- Objective: Pick up the Last Light and bring it to its Altar ( Altar of the Last Light )

87
engine/fov.go Normal file
View file

@ -0,0 +1,87 @@
package engine
import "math"
/* Stolen and modified from the go-fov package */
// Compute takes a GridMap implementation along with the x and y coordinates representing a player's current
// position and will internally update the visibile set of tiles within the provided radius `r`
func ComputeFOV[T any](transform func(x, y int) T, isInBounds, isOpaque func(x, y int) bool, px, py, radius int) (visibilityMap map[Position]T) {
visibilityMap = make(map[Position]T)
visibilityMap[PositionAt(px, py)] = transform(px, py)
for i := 1; i <= 8; i++ {
fov(visibilityMap, transform, isInBounds, isOpaque, px, py, 1, 0, 1, i, radius)
}
return visibilityMap
}
// fov does the actual work of detecting the visible tiles based on the recursive shadowcasting algorithm
// annotations provided inline below for (hopefully) easier learning
func fov[T any](visibilityMap map[Position]T, transform func(x, y int) T, isInBounds, isOpaque func(x, y int) bool, px, py, dist int, lowSlope, highSlope float64, oct, rad int) {
// If the current distance is greater than the radius provided, then this is the end of the iteration
if dist > rad {
return
}
// Convert our slope into integers that will represent the "height" from the player position
// "height" will alternately apply to x OR y coordinates as we move around the octants
low := math.Floor(lowSlope*float64(dist) + 0.5)
high := math.Floor(highSlope*float64(dist) + 0.5)
// inGap refers to whether we are currently scanning non-blocked tiles consecutively
// inGap = true means that the previous tile examined was empty
inGap := false
for height := low; height <= high; height++ {
// Given the player coords and a distance, height and octant, determine which tile is being visited
mapx, mapy := distHeightXY(px, py, dist, int(height), oct)
if isInBounds(mapx, mapy) && distTo(px, py, mapx, mapy) < rad {
// As long as a tile is within the bounds of the map, if we visit it at all, it is considered visible
// That's the efficiency of shadowcasting, you just dont visit tiles that aren't visible
visibilityMap[PositionAt(mapx, mapy)] = transform(mapx, mapy)
}
if isInBounds(mapx, mapy) && isOpaque(mapx, mapy) {
if inGap {
// An opaque tile was discovered, so begin a recursive call
fov(visibilityMap, transform, isInBounds, isOpaque, px, py, dist+1, lowSlope, (height-0.5)/float64(dist), oct, rad)
}
// Any time a recursive call is made, adjust the minimum slope for all future calls within this octant
lowSlope = (height + 0.5) / float64(dist)
inGap = false
} else {
inGap = true
// We've reached the end of the scan and, since the last tile in the scan was empty, begin
// another on the next depth up
if height == high {
fov(visibilityMap, transform, isInBounds, isOpaque, px, py, dist+1, lowSlope, highSlope, oct, rad)
}
}
}
}
// distHeightXY performs some bitwise and operations to handle the transposition of the depth and height values
// since the concept of "depth" and "height" is relative to whichever octant is currently being scanned
func distHeightXY(px, py, d, h, oct int) (int, int) {
if oct&0x1 > 0 {
d = -d
}
if oct&0x2 > 0 {
h = -h
}
if oct&0x4 > 0 {
return px + h, py + d
}
return px + d, py + h
}
// distTo is simply a helper function to determine the distance between two points, for checking visibility of a tile
// within a provided radius
func distTo(x1, y1, x2, y2 int) int {
vx := math.Pow(float64(x1-x2), 2)
vy := math.Pow(float64(y1-y2), 2)
return int(math.Sqrt(vx + vy))
}

View file

@ -1,37 +1,41 @@
package model
package item
import "mvvasilev/last_light/engine"
import (
"mvvasilev/last_light/engine"
)
type EquippedSlot int
const (
EquippedSlotOffhand EquippedSlot = iota
EquippedSlotDominantHand
EquippedSlotHead
EquippedSlotChestplate
EquippedSlotLeggings
EquippedSlotShoes
EquippedSlotNone EquippedSlot = 0
EquippedSlotOffhand EquippedSlot = 1
EquippedSlotDominantHand EquippedSlot = 2
EquippedSlotHead EquippedSlot = 3
EquippedSlotChestplate EquippedSlot = 4
EquippedSlotLeggings EquippedSlot = 5
EquippedSlotShoes EquippedSlot = 6
)
type EquippedInventory struct {
offHand *Item
dominantHand *Item
offHand Item
dominantHand Item
head *Item
chestplate *Item
leggings *Item
shoes *Item
head Item
chestplate Item
leggings Item
shoes Item
*BasicInventory
}
func CreatePlayerInventory() *EquippedInventory {
func CreateEquippedInventory() *EquippedInventory {
return &EquippedInventory{
BasicInventory: CreateInventory(engine.SizeOf(8, 4)),
}
}
func (ei *EquippedInventory) AtSlot(slot EquippedSlot) *Item {
func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item {
switch slot {
case EquippedSlotOffhand:
return ei.offHand
@ -50,23 +54,21 @@ func (ei *EquippedInventory) AtSlot(slot EquippedSlot) *Item {
}
}
func (ei *EquippedInventory) Equip(item Item, slot EquippedSlot) *Item {
ref := &item
func (ei *EquippedInventory) Equip(item Item, slot EquippedSlot) Item {
switch slot {
case EquippedSlotOffhand:
ei.offHand = ref
ei.offHand = item
case EquippedSlotDominantHand:
ei.dominantHand = ref
ei.dominantHand = item
case EquippedSlotHead:
ei.head = ref
ei.head = item
case EquippedSlotChestplate:
ei.chestplate = ref
ei.chestplate = item
case EquippedSlotLeggings:
ei.leggings = ref
ei.leggings = item
case EquippedSlotShoes:
ei.shoes = ref
ei.shoes = item
}
return ref
return item
}

View file

@ -1,30 +1,32 @@
package model
package item
import "mvvasilev/last_light/engine"
import (
"mvvasilev/last_light/engine"
)
type Inventory interface {
Items() []*Item
Items() []Item
Shape() engine.Size
Push(item Item) bool
Drop(x, y int) *Item
ItemAt(x, y int) *Item
Drop(x, y int) Item
ItemAt(x, y int) Item
}
type BasicInventory struct {
contents []*Item
contents []Item
shape engine.Size
}
func CreateInventory(shape engine.Size) *BasicInventory {
inv := new(BasicInventory)
inv.contents = make([]*Item, 0, shape.Height()*shape.Width())
inv.contents = make([]Item, 0, shape.Height()*shape.Width())
inv.shape = shape
return inv
}
func (i *BasicInventory) Items() (items []*Item) {
func (i *BasicInventory) Items() (items []Item) {
return i.contents
}
@ -32,43 +34,43 @@ func (i *BasicInventory) Shape() engine.Size {
return i.shape
}
func (i *BasicInventory) Push(item Item) (success bool) {
if len(i.contents) == i.shape.Area() {
func (inv *BasicInventory) Push(i Item) (success bool) {
if len(inv.contents) == inv.shape.Area() {
return false
}
itemType := item.Type()
itemType := i.Type()
// Try to first find a matching item with capacity
for index, existingItem := range i.contents {
if existingItem != nil && existingItem.itemType == itemType {
for index, existingItem := range inv.contents {
if existingItem != nil && existingItem.Type() == itemType {
if existingItem.Quantity()+1 > existingItem.Type().MaxStack() {
continue
}
it := CreateItem(itemType, existingItem.Quantity()+1)
i.contents[index] = &it
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 i.contents {
for index, existingItem := range inv.contents {
if existingItem == nil {
i.contents[index] = &item
inv.contents[index] = i
return true
}
}
// Finally, just append the new item at the end
i.contents = append(i.contents, &item)
inv.contents = append(inv.contents, i)
return true
}
func (i *BasicInventory) Drop(x, y int) *Item {
func (i *BasicInventory) Drop(x, y int) Item {
index := y*i.shape.Width() + x
if index > len(i.contents)-1 {
@ -82,7 +84,7 @@ func (i *BasicInventory) Drop(x, y int) *Item {
return item
}
func (i *BasicInventory) ItemAt(x, y int) (item *Item) {
func (i *BasicInventory) ItemAt(x, y int) (item Item) {
index := y*i.shape.Width() + x
if index > len(i.contents)-1 {

87
game/item/item.go Normal file
View file

@ -0,0 +1,87 @@
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
}

121
game/item/item_type.go Normal file
View file

@ -0,0 +1,121 @@
package item
import (
"github.com/gdamore/tcell/v2"
)
type ItemType interface {
Name() string
Description() string
TileIcon() rune
Icon() string
Style() tcell.Style
MaxStack() int
EquippableSlot() EquippedSlot
}
type BasicItemType struct {
name string
description string
tileIcon rune
itemIcon string
maxStack int
equippableSlot EquippedSlot
style tcell.Style
}
func CreateBasicItemType(
name, description string,
tileIcon rune,
icon string,
maxStack int,
equippableSlot EquippedSlot,
style tcell.Style,
) *BasicItemType {
return &BasicItemType{
name: name,
description: description,
tileIcon: tileIcon,
itemIcon: icon,
style: style,
maxStack: maxStack,
equippableSlot: equippableSlot,
}
}
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{
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{
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{
name: "Arrow",
description: "Ammunition for a bow",
tileIcon: '-',
itemIcon: "»->",
equippableSlot: EquippedSlotNone,
style: tcell.StyleDefault.Foreground(tcell.ColorGoldenrod),
maxStack: 32,
}
}
func ItemTypeKey() ItemType {
return &BasicItemType{
name: "Key",
description: "Indispensable for unlocking things",
tileIcon: '¬',
itemIcon: " o╖",
equippableSlot: EquippedSlotNone,
style: tcell.StyleDefault.Foreground(tcell.ColorDarkGoldenrod),
maxStack: 1,
}
}

View file

@ -1,68 +0,0 @@
package model
import (
"github.com/gdamore/tcell/v2"
)
type Item struct {
name string
description string
itemType *ItemType
quantity int
}
func EmptyItem() Item {
return Item{
itemType: &ItemType{
name: "",
description: "",
tileIcon: ' ',
itemIcon: " ",
style: tcell.StyleDefault,
maxStack: 0,
},
}
}
func CreateItem(itemType *ItemType, quantity int) Item {
return Item{
itemType: itemType,
quantity: quantity,
}
}
func (i Item) WithName(name string) Item {
i.name = name
return i
}
func (i Item) Name() string {
if i.name == "" {
return i.itemType.name
}
return i.name
}
func (i Item) Description() string {
if i.description == "" {
return i.itemType.description
}
return i.description
}
func (i Item) WithDescription(description string) Item {
i.description = description
return i
}
func (i Item) Type() *ItemType {
return i.itemType
}
func (i Item) Quantity() int {
return i.quantity
}

View file

@ -1,105 +0,0 @@
package model
import (
"github.com/gdamore/tcell/v2"
)
type ItemType struct {
name string
description string
tileIcon rune
itemIcon string
maxStack int
style tcell.Style
}
func (it *ItemType) Name() string {
return it.name
}
func (it *ItemType) Description() string {
return it.description
}
func (it *ItemType) TileIcon() rune {
return it.tileIcon
}
func (it *ItemType) Icon() string {
return it.itemIcon
}
func (it *ItemType) Style() tcell.Style {
return it.style
}
func (it *ItemType) MaxStack() int {
return it.maxStack
}
func ItemTypeFish() *ItemType {
return &ItemType{
name: "Fish",
description: "What's a fish doing down here?",
tileIcon: '>',
itemIcon: "»o>",
style: tcell.StyleDefault.Foreground(tcell.ColorDarkCyan),
maxStack: 16,
}
}
func ItemTypeGold() *ItemType {
return &ItemType{
name: "Gold",
description: "Not all those who wander are lost",
tileIcon: '¤',
itemIcon: " ¤ ",
style: tcell.StyleDefault.Foreground(tcell.ColorGoldenrod),
maxStack: 255,
}
}
func ItemTypeArrow() *ItemType {
return &ItemType{
name: "Arrow",
description: "Ammunition for a bow",
tileIcon: '-',
itemIcon: "»->",
style: tcell.StyleDefault.Foreground(tcell.ColorGoldenrod),
maxStack: 32,
}
}
func ItemTypeBow() *ItemType {
return &ItemType{
name: "Bow",
description: "To shoot arrows with",
tileIcon: ')',
itemIcon: " |)",
style: tcell.StyleDefault.Foreground(tcell.ColorBrown),
maxStack: 1,
}
}
func ItemTypeLongsword() *ItemType {
return &ItemType{
name: "Longsword",
description: "You know nothing.",
tileIcon: '/',
itemIcon: "╪══",
style: tcell.StyleDefault.Foreground(tcell.ColorSilver),
maxStack: 1,
}
}
func ItemTypeKey() *ItemType {
return &ItemType{
name: "Key",
description: "Indispensable for unlocking things",
tileIcon: '¬',
itemIcon: " o╖",
style: tcell.StyleDefault.Foreground(tcell.ColorDarkGoldenrod),
maxStack: 1,
}
}

View file

@ -7,28 +7,28 @@ import (
"github.com/google/uuid"
)
type NPC struct {
type BasicNPC struct {
id uuid.UUID
engine.Positioned
}
func CreateNPC(pos engine.Position) *NPC {
return &NPC{
func CreateNPC(pos engine.Position) *BasicNPC {
return &BasicNPC{
id: uuid.New(),
Positioned: engine.WithPosition(pos),
}
}
func (c *NPC) MoveTo(newPosition engine.Position) {
func (c *BasicNPC) MoveTo(newPosition engine.Position) {
c.Positioned.SetPosition(newPosition)
}
func (c *NPC) UniqueId() uuid.UUID {
func (c *BasicNPC) UniqueId() uuid.UUID {
return c.id
}
func (c *NPC) Input(e *tcell.EventKey) {
func (c *BasicNPC) Input(e *tcell.EventKey) {
}
func (c *NPC) Tick(dt int64) {
func (c *BasicNPC) Tick(dt int64) {
}

View file

@ -1,7 +1,9 @@
package model
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"
@ -11,7 +13,9 @@ type Player struct {
id uuid.UUID
position engine.Position
inventory *EquippedInventory
inventory *item.EquippedInventory
*rpg.BasicRPGEntity
}
func CreatePlayer(x, y int) *Player {
@ -19,7 +23,8 @@ func CreatePlayer(x, y int) *Player {
p.id = uuid.New()
p.position = engine.PositionAt(x, y)
p.inventory = CreatePlayerInventory()
p.inventory = item.CreateEquippedInventory()
p.BasicRPGEntity = rpg.CreateBasicRPGEntity()
return p
}
@ -48,7 +53,7 @@ func (p *Player) Transparent() bool {
return false
}
func (p *Player) Inventory() *EquippedInventory {
func (p *Player) Inventory() *item.EquippedInventory {
return p.inventory
}

143
game/rpg/generate_items.go Normal file
View file

@ -0,0 +1,143 @@
package rpg
import (
"math/rand"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
)
type ItemSupplier func() item.Item
type LootTable struct {
table []ItemSupplier
}
func CreateLootTable() *LootTable {
return &LootTable{
table: make([]ItemSupplier, 0),
}
}
func (igt *LootTable) Add(weight int, createItemFunction ItemSupplier) {
for range weight {
igt.table = append(igt.table, createItemFunction)
}
}
func (igt *LootTable) Generate() item.Item {
return igt.table[rand.Intn(len(igt.table))]()
}
type ItemRarity int
const (
ItemRarity_Common ItemRarity = 0
ItemRarity_Uncommon ItemRarity = 1
ItemRarity_Rare ItemRarity = 2
ItemRarity_Epic ItemRarity = 3
ItemRarity_Legendary ItemRarity = 4
)
func pointPerRarity(rarity ItemRarity) int {
switch rarity {
case ItemRarity_Common:
return 0
case ItemRarity_Uncommon:
return 3
case ItemRarity_Rare:
return 5
case ItemRarity_Epic:
return 8
case ItemRarity_Legendary:
return 13
default:
return 0
}
}
func generateItemName(itemType RPGItemType, rarity ItemRarity) (string, tcell.Style) {
switch rarity {
case ItemRarity_Common:
return itemType.Name(), tcell.StyleDefault
case ItemRarity_Uncommon:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorLime)
case ItemRarity_Rare:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorBlue)
case ItemRarity_Epic:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorPurple)
case ItemRarity_Legendary:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorOrange).Attributes(tcell.AttrBold)
default:
return itemType.Name(), tcell.StyleDefault
}
}
func randomStat() Stat {
stats := []Stat{
Stat_Attributes_Strength,
Stat_Attributes_Dexterity,
Stat_Attributes_Intelligence,
Stat_Attributes_Constitution,
Stat_PhysicalPrecisionBonus,
Stat_EvasionBonus,
Stat_MagicPrecisionBonus,
Stat_TotalPrecisionBonus,
Stat_DamageBonus_Physical_Unarmed,
Stat_DamageBonus_Physical_Slashing,
Stat_DamageBonus_Physical_Piercing,
Stat_DamageBonus_Physical_Bludgeoning,
Stat_DamageBonus_Magic_Fire,
Stat_DamageBonus_Magic_Cold,
Stat_DamageBonus_Magic_Necrotic,
Stat_DamageBonus_Magic_Thunder,
Stat_DamageBonus_Magic_Acid,
Stat_DamageBonus_Magic_Poison,
Stat_MaxHealthBonus,
}
return stats[rand.Intn(len(stats))]
}
func generateItemStatModifiers(rarity ItemRarity) []StatModifier {
points := pointPerRarity(rarity)
modifiers := []StatModifier{}
for {
if points <= 0 {
break
}
modAmount := engine.RandInt(-points/2, points)
if modAmount == 0 {
continue
}
modifiers = append(modifiers, StatModifier{
Id: StatModifierId(uuid.New().String()),
Stat: randomStat(),
Bonus: modAmount,
})
points -= modAmount
}
return modifiers
}
// 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)
return CreateRPGItem(
name,
style,
itemType,
generateItemStatModifiers(rarity),
)
}

90
game/rpg/rpg_entity.go Normal file
View file

@ -0,0 +1,90 @@
package rpg
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)
}
type BasicRPGEntity struct {
stats map[Stat]int
statModifiers map[Stat][]StatModifier
currentHealth int
}
func CreateBasicRPGEntity() *BasicRPGEntity {
return &BasicRPGEntity{
stats: make(map[Stat]int, 0),
statModifiers: make(map[Stat][]StatModifier, 0),
currentHealth: 0,
}
}
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) {
}
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
}

241
game/rpg/rpg_items.go Normal file
View file

@ -0,0 +1,241 @@
package rpg
import (
"mvvasilev/last_light/game/item"
"github.com/gdamore/tcell/v2"
)
type RPGItemType interface {
RollDamage() func(victim, attacker RPGEntity) (damage int, dmgType DamageType)
item.ItemType
}
type RPGItem interface {
Modifiers() []StatModifier
item.Item
}
type BasicRPGItemType struct {
damageRollFunc func(victim, attacker RPGEntity) (damage int, dmgType DamageType)
*item.BasicItemType
}
func (it *BasicRPGItemType) RollDamage() func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return it.damageRollFunc
}
func ItemTypeBow() RPGItemType {
return &BasicRPGItemType{
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
// TODO: Ranged
return RollD8(1), DamageType_Physical_Piercing
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"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
},
BasicItemType: item.CreateBasicItemType(
"Spear",
"Pokey, pokey",
'Î',
"──>",
1,
item.EquippedSlotDominantHand,
tcell.StyleDefault.Foreground(tcell.ColorSilver),
),
}
}
type BasicRPGItem struct {
modifiers []StatModifier
item.BasicItem
}
func (i *BasicRPGItem) Modifiers() []StatModifier {
return i.modifiers
}
func CreateRPGItem(name string, style tcell.Style, itemType RPGItemType, modifiers []StatModifier) RPGItem {
return &BasicRPGItem{
modifiers: modifiers,
BasicItem: item.CreateBasicItemWithName(
name,
style,
itemType,
1,
),
}
}

211
game/rpg/rpg_system.go Normal file
View file

@ -0,0 +1,211 @@
package rpg
import (
"math/rand"
)
type Stat int
const (
// Used as a default value in cases where no other stat could be determined.
// Should never be used except in cases of error handling
Stat_NonExtant Stat = -1
Stat_Attributes_Strength Stat = 0
Stat_Attributes_Dexterity Stat = 10
Stat_Attributes_Intelligence Stat = 20
Stat_Attributes_Constitution Stat = 30
Stat_PhysicalPrecisionBonus Stat = 5
Stat_EvasionBonus Stat = 15
Stat_MagicPrecisionBonus Stat = 25
Stat_TotalPrecisionBonus Stat = 35
Stat_DamageBonus_Physical_Unarmed Stat = 40
Stat_DamageBonus_Physical_Slashing Stat = 50
Stat_DamageBonus_Physical_Piercing Stat = 60
Stat_DamageBonus_Physical_Bludgeoning Stat = 70
Stat_DamageBonus_Magic_Fire Stat = 80
Stat_DamageBonus_Magic_Cold Stat = 90
Stat_DamageBonus_Magic_Necrotic Stat = 100
Stat_DamageBonus_Magic_Thunder Stat = 110
Stat_DamageBonus_Magic_Acid Stat = 120
Stat_DamageBonus_Magic_Poison Stat = 130
Stat_MaxHealthBonus Stat = 140
)
type StatModifierId string
type StatModifier struct {
Id StatModifierId
Stat Stat
Bonus int
}
// RPG system is based off of dice rolls
func rollDice(times, sides int) int {
acc := 0
for range times {
acc += 1 + rand.Intn(sides+1)
}
return acc
}
func RollD100(times int) int {
return rollDice(times, 100)
}
func RollD20(times int) int {
return rollDice(times, 20)
}
func RollD12(times int) int {
return rollDice(times, 12)
}
func RollD10(times int) int {
return rollDice(times, 10)
}
func RollD8(times int) int {
return rollDice(times, 8)
}
func RollD6(times int) int {
return rollDice(times, 6)
}
func RollD4(times int) int {
return rollDice(times, 4)
}
// Contests are "meets it, beats it"
//
// Luck roll = 1d10
//
// 2 rolls per attack:
//
// BASIC ATTACKS ( spells and abilities can have special rules, or in leu of special rules, these are used ):
//
// 1. Attack roll ( determines if the attack lands ). Contest between Evasion and Precision.
// Evasion = Dexterity + Luck roll.
// Precision = ( Strength | Intelligence ) + Luck roll ( intelligence for magic, strength for melee ).
//
// 2. Damage roll ( only if the previous was successful ). Each spell, ability and weapon has its own damage calculation.
type DamageType int
const (
DamageType_Physical_Unarmed DamageType = 0
DamageType_Physical_Slashing DamageType = 1
DamageType_Physical_Piercing DamageType = 2
DamageType_Physical_Bludgeoning DamageType = 3
DamageType_Magic_Fire DamageType = 4
DamageType_Magic_Cold DamageType = 5
DamageType_Magic_Necrotic DamageType = 6
DamageType_Magic_Thunder DamageType = 7
DamageType_Magic_Acid DamageType = 8
DamageType_Magic_Poison DamageType = 9
)
func DamageTypeToBonusStat(dmgType DamageType) Stat {
switch dmgType {
case DamageType_Physical_Unarmed:
return Stat_DamageBonus_Physical_Unarmed
case DamageType_Physical_Slashing:
return Stat_DamageBonus_Physical_Slashing
case DamageType_Physical_Piercing:
return Stat_DamageBonus_Physical_Piercing
case DamageType_Physical_Bludgeoning:
return Stat_DamageBonus_Physical_Bludgeoning
case DamageType_Magic_Fire:
return Stat_DamageBonus_Magic_Fire
case DamageType_Magic_Cold:
return Stat_DamageBonus_Magic_Fire
case DamageType_Magic_Necrotic:
return Stat_DamageBonus_Magic_Necrotic
case DamageType_Magic_Thunder:
return Stat_DamageBonus_Magic_Thunder
case DamageType_Magic_Acid:
return Stat_DamageBonus_Magic_Acid
case DamageType_Magic_Poison:
return Stat_DamageBonus_Magic_Poison
default:
return Stat_NonExtant
}
}
func LuckRoll() int {
return RollD10(1)
}
func TotalModifierForStat(entity RPGEntity, stat Stat) int {
agg := 0
for _, m := range entity.CollectModifiersForStat(stat) {
agg += m.Bonus
}
return agg
}
func StatValue(entity RPGEntity, stat Stat) int {
return entity.BaseStat(stat) + TotalModifierForStat(entity, stat)
}
// Base Max Health is determined from constitution:
// Constitution + Max Health Bonus + 10
func BaseMaxHealth(entity RPGEntity) int {
return StatValue(entity, Stat_Attributes_Constitution) + StatValue(entity, Stat_MaxHealthBonus) + 10
}
// Dexterity + Evasion bonus + luck roll
func EvasionRoll(victim RPGEntity) int {
return StatValue(victim, Stat_Attributes_Dexterity) + StatValue(victim, 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()
}
// 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()
}
// true = hit lands, false = hit does not land
func MagicHitRoll(attacker RPGEntity, victim RPGEntity) bool {
return hitRoll(EvasionRoll(victim), MagicPrecisionRoll(attacker))
}
// true = hit lands, false = hit does not land
func PhysicalHitRoll(attacker RPGEntity, victim RPGEntity) bool {
return hitRoll(EvasionRoll(victim), PhysicalPrecisionRoll(attacker))
}
func hitRoll(evasionRoll, precisionRoll int) bool {
if evasionRoll == 20 && precisionRoll == 20 {
return true
}
if evasionRoll == 20 {
return false
}
if precisionRoll == 20 {
return true
}
return evasionRoll < precisionRoll
}
func UnarmedDamage(attacker RPGEntity) int {
return RollD4(1) + StatValue(attacker, Stat_DamageBonus_Physical_Unarmed)
}

View file

@ -3,6 +3,7 @@ package state
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/player"
"mvvasilev/last_light/game/ui/menu"
"github.com/gdamore/tcell/v2"
@ -15,13 +16,13 @@ type InventoryScreenState struct {
inventoryMenu *menu.PlayerInventoryMenu
selectedInventorySlot engine.Position
player *model.Player
player *player.Player
moveInventorySlotDirection model.Direction
dropSelectedInventorySlot bool
}
func CreateInventoryScreenState(player *model.Player, prevState PausableState) *InventoryScreenState {
func CreateInventoryScreenState(player *player.Player, prevState PausableState) *InventoryScreenState {
iss := new(InventoryScreenState)
iss.prevState = prevState

View file

@ -3,6 +3,7 @@ package state
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/player"
"mvvasilev/last_light/game/ui"
"mvvasilev/last_light/game/world"
@ -11,8 +12,8 @@ import (
)
type PlayingState struct {
player *model.Player
someNPC *model.NPC
player *player.Player
someNPC *model.BasicNPC
dungeon *world.Dungeon
@ -35,7 +36,7 @@ func BeginPlayingState() *PlayingState {
s.dungeon = world.CreateDungeon(mapSize.Width(), mapSize.Height(), 1)
s.player = model.CreatePlayer(s.dungeon.CurrentLevel().PlayerSpawnPoint().XY())
s.player = player.CreatePlayer(s.dungeon.CurrentLevel().PlayerSpawnPoint().XY())
s.someNPC = model.CreateNPC(s.dungeon.CurrentLevel().NextLevelStaircase())
@ -172,10 +173,10 @@ func (ps *PlayingState) PickUpItemUnderPlayer() {
return
}
success := ps.player.Inventory().Push(*item)
success := ps.player.Inventory().Push(item)
if !success {
ps.dungeon.CurrentLevel().SetItemAt(pos.X(), pos.Y(), *item)
ps.dungeon.CurrentLevel().SetItemAt(pos.X(), pos.Y(), item)
}
}
@ -286,13 +287,31 @@ func (ps *PlayingState) OnTick(dt int64) GameState {
func (ps *PlayingState) CollectDrawables() []engine.Drawable {
return engine.Multidraw(engine.CreateDrawingInstructions(func(v views.View) {
visibilityMap := engine.ComputeFOV(
func(x, y int) world.Tile {
ps.dungeon.CurrentLevel().Flatten().MarkExplored(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() },
ps.player.Position().X(), ps.player.Position().Y(),
13,
)
ps.viewport.DrawFromProvider(v, func(x, y int) (rune, tcell.Style) {
tile := ps.dungeon.CurrentLevel().TileAt(x, y)
tile := visibilityMap[engine.PositionAt(x, y)]
if tile != nil {
return tile.Presentation()
}
explored := ps.dungeon.CurrentLevel().Flatten().ExploredTileAt(x, y)
if explored != nil {
return explored.Presentation()
}
return ' ', tcell.StyleDefault
})
}))

View file

@ -3,7 +3,7 @@ package menu
import (
"fmt"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2"
@ -12,7 +12,7 @@ import (
)
type PlayerInventoryMenu struct {
inventory *model.EquippedInventory
inventory *item.EquippedInventory
inventoryMenu *ui.UIWindow
armourLabel *ui.UILabel
@ -29,7 +29,7 @@ type PlayerInventoryMenu struct {
selectedInventorySlot engine.Position
}
func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventory, style tcell.Style, highlightStyle tcell.Style) *PlayerInventoryMenu {
func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory, style tcell.Style, highlightStyle tcell.Style) *PlayerInventoryMenu {
menu := new(PlayerInventoryMenu)
menu.inventory = playerInventory
@ -124,8 +124,17 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
return
}
ui.CreateSingleLineUILabel(x+3, y+15, fmt.Sprintf("Name: %v", item.Name()), style).Draw(v)
ui.CreateSingleLineUILabel(x+3, y+16, fmt.Sprintf("Desc: %v", item.Description()), style).Draw(v)
name, nameStyle := item.Name()
ui.CreateSingleLineUILabel(x+3, y+15, name, nameStyle).Draw(v)
// |Stt:+00|Stt:+00|Stt:+00|Stt:+00|
// switch it := item.(type) {
// case rpg.RPGItem:
// //statModifiers := it.Modifiers()
// default:
// }
})
menu.help = ui.CreateSingleLineUILabel(x+2, y+22, "hjkl - move, x - drop, e - equip", style)

View file

@ -32,6 +32,18 @@ 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) {
}

View file

@ -3,7 +3,10 @@ package world
import (
"math/rand"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/rpg"
"slices"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
@ -104,12 +107,39 @@ type DungeonLevel struct {
func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLevel {
genTable := make(map[float32]*model.ItemType, 0)
genTable := rpg.CreateLootTable()
genTable[0.2] = model.ItemTypeFish()
genTable[0.05] = model.ItemTypeBow()
genTable[0.051] = model.ItemTypeLongsword()
genTable[0.052] = model.ItemTypeKey()
genTable.Add(10, func() item.Item {
return item.CreateBasicItem(item.ItemTypeFish(), 1)
})
genTable.Add(1, func() item.Item {
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(),
}
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
@ -126,7 +156,7 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLeve
groundLevel = CreateBSPDungeonMap(width, height, 4)
}
items := SpawnItems(groundLevel.Rooms(), 0.01, genTable, []engine.Position{
items := SpawnItems(groundLevel.Rooms(), 0.02, genTable, []engine.Position{
groundLevel.NextLevelStaircasePosition(),
groundLevel.PlayerSpawnPoint(),
groundLevel.PreviousLevelStaircasePosition(),
@ -157,6 +187,43 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLeve
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 {
itemType := genTable.Generate()
if itemType == 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, itemType))
}
}
return itemTiles
}
func (d *DungeonLevel) PlayerSpawnPoint() engine.Position {
return d.groundLevel.PlayerSpawnPoint()
}
@ -185,7 +252,7 @@ func (d *DungeonLevel) MoveEntityTo(uuid uuid.UUID, x, y int) {
d.entityLevel.MoveEntityTo(uuid, x, y)
}
func (d *DungeonLevel) RemoveItemAt(x, y int) *model.Item {
func (d *DungeonLevel) RemoveItemAt(x, y int) item.Item {
if !d.groundLevel.Size().Contains(x, y) {
return nil
}
@ -199,17 +266,15 @@ func (d *DungeonLevel) RemoveItemAt(x, y int) *model.Item {
d.itemLevel.SetTileAt(x, y, nil)
item := model.CreateItem(itemTile.Type(), itemTile.Quantity())
return &item
return itemTile.Item()
}
func (d *DungeonLevel) SetItemAt(x, y int, it model.Item) (success bool) {
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.Type(), it.Quantity()))
d.itemLevel.SetTileAt(x, y, CreateItemTile(engine.PositionAt(x, y), it))
return true
}

View file

@ -18,6 +18,10 @@ 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) {
}

View file

@ -111,6 +111,18 @@ func (em *EntityMap) TileAt(x int, y int) Tile {
return em.entities[key]
}
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) {
for _, e := range em.entities {
e.Entity().Tick(dt)

View file

@ -3,6 +3,8 @@ package world
import (
"math/rand"
"mvvasilev/last_light/engine"
"github.com/gdamore/tcell/v2"
)
type splitDirection bool
@ -92,7 +94,7 @@ func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap {
staircaseRoom := findRoom(root.right)
bsp.rooms = rooms
bsp.level = CreateBasicMap(tiles)
bsp.level = CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey))
bsp.playerSpawnPoint = engine.PositionAt(
spawnRoom.Position().X()+spawnRoom.Size().Width()/2,

View file

@ -1,18 +1,13 @@
package world
func CreateEmptyDungeonLevel(width, height int) *EmptyDungeonMap {
m := new(EmptyDungeonMap)
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)
}
m.level = CreateBasicMap(tiles)
//m.level.SetTileAt(width/2, height/2, CreateStaticTile(width/2, height/2, TileTypeStaircaseDown()))
//m.level.SetTileAt(width/3, height/3, CreateStaticTile(width/3, height/3, TileTypeStaircaseUp()))
return m
return CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey))
}

View file

@ -1,57 +0,0 @@
package world
import (
"math/rand"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"slices"
)
func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTable map[float32]*model.ItemType, 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 {
itemType := GenerateItemType(genTable)
if itemType == 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, itemType, 1))
}
}
return itemTiles
}
func GenerateItemType(genTable map[float32]*model.ItemType) *model.ItemType {
num := rand.Float32()
for k, v := range genTable {
if num > k {
return v
}
}
return nil
}

View file

@ -2,12 +2,17 @@ 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)
}
@ -28,18 +33,23 @@ type WithPreviousLevelStaircasePosition interface {
}
type BasicMap struct {
tiles [][]Tile
tiles [][]Tile
exploredTiles map[engine.Position]Tile
exploredStyle tcell.Style
}
func CreateBasicMap(tiles [][]Tile) *BasicMap {
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() {
func (bm *BasicMap) Tick(dt int64) {
}
func (bm *BasicMap) Size() engine.Size {
@ -47,11 +57,7 @@ func (bm *BasicMap) Size() engine.Size {
}
func (bm *BasicMap) SetTileAt(x int, y int, t Tile) Tile {
if len(bm.tiles) <= y || len(bm.tiles[0]) <= x {
return CreateStaticTile(x, y, TileTypeVoid())
}
if x < 0 || y < 0 {
if !bm.IsInBounds(x, y) {
return CreateStaticTile(x, y, TileTypeVoid())
}
@ -61,11 +67,7 @@ func (bm *BasicMap) SetTileAt(x int, y int, t Tile) Tile {
}
func (bm *BasicMap) TileAt(x int, y int) Tile {
if x < 0 || y < 0 {
return CreateStaticTile(x, y, TileTypeVoid())
}
if x >= bm.Size().Width() || y >= bm.Size().Height() {
if !bm.IsInBounds(x, y) {
return CreateStaticTile(x, y, TileTypeVoid())
}
@ -77,3 +79,29 @@ func (bm *BasicMap) TileAt(x int, y int) Tile {
return tile
}
func (bm *BasicMap) IsInBounds(x, y int) bool {
if x < 0 || y < 0 {
return false
}
if x >= bm.Size().Width() || y >= bm.Size().Height() {
return false
}
return true
}
func (bm *BasicMap) ExploredTileAt(x, y int) Tile {
return bm.exploredTiles[engine.PositionAt(x, y)]
}
func (bm *BasicMap) MarkExplored(x, y int) {
if !bm.IsInBounds(x, y) {
return
}
tile := bm.TileAt(x, y)
bm.exploredTiles[engine.PositionAt(x, y)] = CreateStaticTileWithStyleOverride(tile.Position().X(), tile.Position().Y(), tile.Type(), bm.exploredStyle)
}

View file

@ -45,11 +45,7 @@ func (mm *MultilevelMap) SetTileAtHeight(x, y, height int, t Tile) {
func (mm *MultilevelMap) CollectTilesAt(x, y int, filter func(t Tile) bool) []Tile {
tiles := make([]Tile, len(mm.layers))
if x < 0 || y < 0 {
return tiles
}
if x >= mm.Size().Width() || y >= mm.Size().Height() {
if !mm.IsInBounds(x, y) {
return tiles
}
@ -66,11 +62,7 @@ func (mm *MultilevelMap) CollectTilesAt(x, y int, filter func(t Tile) bool) []Ti
}
func (mm *MultilevelMap) TileAt(x int, y int) Tile {
if x < 0 || y < 0 {
return CreateStaticTile(x, y, TileTypeVoid())
}
if x >= mm.Size().Width() || y >= mm.Size().Height() {
if !mm.IsInBounds(x, y) {
return CreateStaticTile(x, y, TileTypeVoid())
}
@ -86,12 +78,38 @@ func (mm *MultilevelMap) TileAt(x int, y int) Tile {
return CreateStaticTile(x, y, TileTypeVoid())
}
func (mm *MultilevelMap) TileAtHeight(x, y, height int) Tile {
func (mm *MultilevelMap) IsInBounds(x, y int) bool {
if x < 0 || y < 0 {
return CreateStaticTile(x, y, TileTypeVoid())
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())
}

View file

@ -2,6 +2,7 @@ package world
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/model"
"github.com/gdamore/tcell/v2"
@ -26,6 +27,7 @@ type TileType struct {
Passable bool
Presentation rune
Transparent bool
Opaque bool
Style tcell.Style
}
@ -35,6 +37,7 @@ func TileTypeGround() TileType {
Passable: true,
Presentation: '.',
Transparent: false,
Opaque: false,
Style: tcell.StyleDefault,
}
}
@ -45,6 +48,7 @@ func TileTypeRock() TileType {
Passable: false,
Presentation: '█',
Transparent: false,
Opaque: true,
Style: tcell.StyleDefault,
}
}
@ -55,6 +59,7 @@ func TileTypeGrass() TileType {
Passable: true,
Presentation: ',',
Transparent: false,
Opaque: false,
Style: tcell.StyleDefault,
}
}
@ -65,6 +70,7 @@ func TileTypeVoid() TileType {
Passable: false,
Presentation: ' ',
Transparent: true,
Opaque: true,
Style: tcell.StyleDefault,
}
}
@ -75,6 +81,7 @@ func TileTypeWall() TileType {
Passable: false,
Presentation: '#',
Transparent: false,
Opaque: true,
Style: tcell.StyleDefault.Background(tcell.ColorGray),
}
}
@ -85,6 +92,7 @@ func TileTypeClosedDoor() TileType {
Passable: false,
Transparent: false,
Presentation: '[',
Opaque: true,
Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue).Background(tcell.ColorSaddleBrown),
}
}
@ -95,6 +103,7 @@ func TileTypeOpenDoor() TileType {
Passable: false,
Transparent: false,
Presentation: '_',
Opaque: false,
Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue),
}
}
@ -105,6 +114,7 @@ func TileTypeStaircaseDown() TileType {
Passable: true,
Transparent: false,
Presentation: '≡',
Opaque: false,
Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold),
}
}
@ -115,6 +125,7 @@ func TileTypeStaircaseUp() TileType {
Passable: true,
Transparent: false,
Presentation: '^',
Opaque: false,
Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold),
}
}
@ -124,11 +135,15 @@ type Tile interface {
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 {
@ -136,16 +151,25 @@ func CreateStaticTile(x, y int, t TileType) Tile {
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.t.Style
return st.t.Presentation, st.style
}
func (st *StaticTile) Passable() bool {
@ -156,32 +180,30 @@ 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
itemType *model.ItemType
quantity int
item item.Item
}
func CreateItemTile(position engine.Position, itemType *model.ItemType, quantity int) *ItemTile {
func CreateItemTile(position engine.Position, item item.Item) *ItemTile {
it := new(ItemTile)
it.position = position
it.itemType = itemType
it.quantity = quantity
it.item = item
return it
}
func (it *ItemTile) Type() *model.ItemType {
return it.itemType
}
func (it *ItemTile) Quantity() int {
return it.quantity
func (it *ItemTile) Item() item.Item {
return it.item
}
func (it *ItemTile) Position() engine.Position {
@ -189,7 +211,7 @@ func (it *ItemTile) Position() engine.Position {
}
func (it *ItemTile) Presentation() (rune, tcell.Style) {
return it.itemType.TileIcon(), it.itemType.Style()
return it.item.Type().TileIcon(), it.item.Type().Style()
}
func (it *ItemTile) Passable() bool {
@ -200,6 +222,14 @@ 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() model.MovableEntity
Tile
@ -239,3 +269,11 @@ func (bet *BasicEntityTile) Passable() bool {
func (bet *BasicEntityTile) Transparent() bool {
return false
}
func (bet *BasicEntityTile) Opaque() bool {
return false
}
func (bet *BasicEntityTile) Type() TileType {
return TileType{}
}