mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-19 12:49:52 +03:00
Add fov, add rpg system, add rpg items
This commit is contained in:
parent
3c83d97a34
commit
b30dc8dec3
27 changed files with 1325 additions and 361 deletions
|
@ -1,7 +1,8 @@
|
||||||
# Last Light
|
# Last Light
|
||||||
|
|
||||||
- Roguelike RPG
|
- Roguelike RPG
|
||||||
- Inventory System
|
- ~~FOV with explored tiles greyed out~~
|
||||||
|
- ~~Inventory System~~
|
||||||
- Weapons & Armor
|
- Weapons & Armor
|
||||||
- Head
|
- Head
|
||||||
- Chest
|
- Chest
|
||||||
|
@ -11,6 +12,7 @@
|
||||||
- Right Hand
|
- Right Hand
|
||||||
- Damage Types
|
- Damage Types
|
||||||
- Physical
|
- Physical
|
||||||
|
- Unarmed
|
||||||
- Slashing
|
- Slashing
|
||||||
- Piercing
|
- Piercing
|
||||||
- Bludgeoning
|
- Bludgeoning
|
||||||
|
@ -24,7 +26,7 @@
|
||||||
- 9-level Dungeon
|
- 9-level Dungeon
|
||||||
- 4 types of dungeon levels:
|
- 4 types of dungeon levels:
|
||||||
- Caverns ( Cellular Automata )
|
- Caverns ( Cellular Automata )
|
||||||
- Dungeon ( Maze w/ Rooms )
|
- ~~Dungeon ( Maze w/ Rooms )~~
|
||||||
- Mine ( Broguelike )
|
- Mine ( Broguelike )
|
||||||
- Underground City ( Caverns + Dungeon combo )
|
- Underground City ( Caverns + Dungeon combo )
|
||||||
- Objective: Pick up the Last Light and bring it to its Altar ( Altar of the Last Light )
|
- Objective: Pick up the Last Light and bring it to its Altar ( Altar of the Last Light )
|
||||||
|
|
87
engine/fov.go
Normal file
87
engine/fov.go
Normal 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))
|
||||||
|
}
|
|
@ -1,37 +1,41 @@
|
||||||
package model
|
package item
|
||||||
|
|
||||||
import "mvvasilev/last_light/engine"
|
import (
|
||||||
|
"mvvasilev/last_light/engine"
|
||||||
|
)
|
||||||
|
|
||||||
type EquippedSlot int
|
type EquippedSlot int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EquippedSlotOffhand EquippedSlot = iota
|
EquippedSlotNone EquippedSlot = 0
|
||||||
EquippedSlotDominantHand
|
|
||||||
EquippedSlotHead
|
EquippedSlotOffhand EquippedSlot = 1
|
||||||
EquippedSlotChestplate
|
EquippedSlotDominantHand EquippedSlot = 2
|
||||||
EquippedSlotLeggings
|
EquippedSlotHead EquippedSlot = 3
|
||||||
EquippedSlotShoes
|
EquippedSlotChestplate EquippedSlot = 4
|
||||||
|
EquippedSlotLeggings EquippedSlot = 5
|
||||||
|
EquippedSlotShoes EquippedSlot = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
type EquippedInventory struct {
|
type EquippedInventory struct {
|
||||||
offHand *Item
|
offHand Item
|
||||||
dominantHand *Item
|
dominantHand Item
|
||||||
|
|
||||||
head *Item
|
head Item
|
||||||
chestplate *Item
|
chestplate Item
|
||||||
leggings *Item
|
leggings Item
|
||||||
shoes *Item
|
shoes Item
|
||||||
|
|
||||||
*BasicInventory
|
*BasicInventory
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePlayerInventory() *EquippedInventory {
|
func CreateEquippedInventory() *EquippedInventory {
|
||||||
return &EquippedInventory{
|
return &EquippedInventory{
|
||||||
BasicInventory: CreateInventory(engine.SizeOf(8, 4)),
|
BasicInventory: CreateInventory(engine.SizeOf(8, 4)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ei *EquippedInventory) AtSlot(slot EquippedSlot) *Item {
|
func (ei *EquippedInventory) AtSlot(slot EquippedSlot) Item {
|
||||||
switch slot {
|
switch slot {
|
||||||
case EquippedSlotOffhand:
|
case EquippedSlotOffhand:
|
||||||
return ei.offHand
|
return ei.offHand
|
||||||
|
@ -50,23 +54,21 @@ func (ei *EquippedInventory) AtSlot(slot EquippedSlot) *Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ei *EquippedInventory) Equip(item Item, slot EquippedSlot) *Item {
|
func (ei *EquippedInventory) Equip(item Item, slot EquippedSlot) Item {
|
||||||
ref := &item
|
|
||||||
|
|
||||||
switch slot {
|
switch slot {
|
||||||
case EquippedSlotOffhand:
|
case EquippedSlotOffhand:
|
||||||
ei.offHand = ref
|
ei.offHand = item
|
||||||
case EquippedSlotDominantHand:
|
case EquippedSlotDominantHand:
|
||||||
ei.dominantHand = ref
|
ei.dominantHand = item
|
||||||
case EquippedSlotHead:
|
case EquippedSlotHead:
|
||||||
ei.head = ref
|
ei.head = item
|
||||||
case EquippedSlotChestplate:
|
case EquippedSlotChestplate:
|
||||||
ei.chestplate = ref
|
ei.chestplate = item
|
||||||
case EquippedSlotLeggings:
|
case EquippedSlotLeggings:
|
||||||
ei.leggings = ref
|
ei.leggings = item
|
||||||
case EquippedSlotShoes:
|
case EquippedSlotShoes:
|
||||||
ei.shoes = ref
|
ei.shoes = item
|
||||||
}
|
}
|
||||||
|
|
||||||
return ref
|
return item
|
||||||
}
|
}
|
|
@ -1,30 +1,32 @@
|
||||||
package model
|
package item
|
||||||
|
|
||||||
import "mvvasilev/last_light/engine"
|
import (
|
||||||
|
"mvvasilev/last_light/engine"
|
||||||
|
)
|
||||||
|
|
||||||
type Inventory interface {
|
type Inventory interface {
|
||||||
Items() []*Item
|
Items() []Item
|
||||||
Shape() engine.Size
|
Shape() engine.Size
|
||||||
Push(item Item) bool
|
Push(item Item) bool
|
||||||
Drop(x, y int) *Item
|
Drop(x, y int) Item
|
||||||
ItemAt(x, y int) *Item
|
ItemAt(x, y int) Item
|
||||||
}
|
}
|
||||||
|
|
||||||
type BasicInventory struct {
|
type BasicInventory struct {
|
||||||
contents []*Item
|
contents []Item
|
||||||
shape engine.Size
|
shape engine.Size
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateInventory(shape engine.Size) *BasicInventory {
|
func CreateInventory(shape engine.Size) *BasicInventory {
|
||||||
inv := new(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
|
inv.shape = shape
|
||||||
|
|
||||||
return inv
|
return inv
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BasicInventory) Items() (items []*Item) {
|
func (i *BasicInventory) Items() (items []Item) {
|
||||||
return i.contents
|
return i.contents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,43 +34,43 @@ func (i *BasicInventory) Shape() engine.Size {
|
||||||
return i.shape
|
return i.shape
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BasicInventory) Push(item Item) (success bool) {
|
func (inv *BasicInventory) Push(i Item) (success bool) {
|
||||||
if len(i.contents) == i.shape.Area() {
|
if len(inv.contents) == inv.shape.Area() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
itemType := item.Type()
|
itemType := i.Type()
|
||||||
|
|
||||||
// Try to first find a matching item with capacity
|
// Try to first find a matching item with capacity
|
||||||
for index, existingItem := range i.contents {
|
for index, existingItem := range inv.contents {
|
||||||
if existingItem != nil && existingItem.itemType == itemType {
|
if existingItem != nil && existingItem.Type() == itemType {
|
||||||
if existingItem.Quantity()+1 > existingItem.Type().MaxStack() {
|
if existingItem.Quantity()+1 > existingItem.Type().MaxStack() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
it := CreateItem(itemType, existingItem.Quantity()+1)
|
it := CreateBasicItem(itemType, existingItem.Quantity()+1)
|
||||||
i.contents[index] = &it
|
inv.contents[index] = &it
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, try to find an intermediate empty slot to fit this item into
|
// 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 {
|
if existingItem == nil {
|
||||||
i.contents[index] = &item
|
inv.contents[index] = i
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, just append the new item at the end
|
// Finally, just append the new item at the end
|
||||||
i.contents = append(i.contents, &item)
|
inv.contents = append(inv.contents, i)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *BasicInventory) Drop(x, y int) *Item {
|
func (i *BasicInventory) Drop(x, y int) Item {
|
||||||
index := y*i.shape.Width() + x
|
index := y*i.shape.Width() + x
|
||||||
|
|
||||||
if index > len(i.contents)-1 {
|
if index > len(i.contents)-1 {
|
||||||
|
@ -82,7 +84,7 @@ func (i *BasicInventory) Drop(x, y int) *Item {
|
||||||
return 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
|
index := y*i.shape.Width() + x
|
||||||
|
|
||||||
if index > len(i.contents)-1 {
|
if index > len(i.contents)-1 {
|
87
game/item/item.go
Normal file
87
game/item/item.go
Normal 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
121
game/item/item_type.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,28 +7,28 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NPC struct {
|
type BasicNPC struct {
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
engine.Positioned
|
engine.Positioned
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateNPC(pos engine.Position) *NPC {
|
func CreateNPC(pos engine.Position) *BasicNPC {
|
||||||
return &NPC{
|
return &BasicNPC{
|
||||||
id: uuid.New(),
|
id: uuid.New(),
|
||||||
Positioned: engine.WithPosition(pos),
|
Positioned: engine.WithPosition(pos),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NPC) MoveTo(newPosition engine.Position) {
|
func (c *BasicNPC) MoveTo(newPosition engine.Position) {
|
||||||
c.Positioned.SetPosition(newPosition)
|
c.Positioned.SetPosition(newPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NPC) UniqueId() uuid.UUID {
|
func (c *BasicNPC) UniqueId() uuid.UUID {
|
||||||
return c.id
|
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) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package model
|
package player
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
|
"mvvasilev/last_light/game/item"
|
||||||
|
"mvvasilev/last_light/game/rpg"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -11,7 +13,9 @@ type Player struct {
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
position engine.Position
|
position engine.Position
|
||||||
|
|
||||||
inventory *EquippedInventory
|
inventory *item.EquippedInventory
|
||||||
|
|
||||||
|
*rpg.BasicRPGEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePlayer(x, y int) *Player {
|
func CreatePlayer(x, y int) *Player {
|
||||||
|
@ -19,7 +23,8 @@ func CreatePlayer(x, y int) *Player {
|
||||||
|
|
||||||
p.id = uuid.New()
|
p.id = uuid.New()
|
||||||
p.position = engine.PositionAt(x, y)
|
p.position = engine.PositionAt(x, y)
|
||||||
p.inventory = CreatePlayerInventory()
|
p.inventory = item.CreateEquippedInventory()
|
||||||
|
p.BasicRPGEntity = rpg.CreateBasicRPGEntity()
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
@ -48,7 +53,7 @@ func (p *Player) Transparent() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) Inventory() *EquippedInventory {
|
func (p *Player) Inventory() *item.EquippedInventory {
|
||||||
return p.inventory
|
return p.inventory
|
||||||
}
|
}
|
||||||
|
|
143
game/rpg/generate_items.go
Normal file
143
game/rpg/generate_items.go
Normal 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
90
game/rpg/rpg_entity.go
Normal 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
241
game/rpg/rpg_items.go
Normal 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
211
game/rpg/rpg_system.go
Normal 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)
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package state
|
||||||
import (
|
import (
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
"mvvasilev/last_light/game/model"
|
"mvvasilev/last_light/game/model"
|
||||||
|
"mvvasilev/last_light/game/player"
|
||||||
"mvvasilev/last_light/game/ui/menu"
|
"mvvasilev/last_light/game/ui/menu"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
@ -15,13 +16,13 @@ type InventoryScreenState struct {
|
||||||
inventoryMenu *menu.PlayerInventoryMenu
|
inventoryMenu *menu.PlayerInventoryMenu
|
||||||
selectedInventorySlot engine.Position
|
selectedInventorySlot engine.Position
|
||||||
|
|
||||||
player *model.Player
|
player *player.Player
|
||||||
|
|
||||||
moveInventorySlotDirection model.Direction
|
moveInventorySlotDirection model.Direction
|
||||||
dropSelectedInventorySlot bool
|
dropSelectedInventorySlot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateInventoryScreenState(player *model.Player, prevState PausableState) *InventoryScreenState {
|
func CreateInventoryScreenState(player *player.Player, prevState PausableState) *InventoryScreenState {
|
||||||
iss := new(InventoryScreenState)
|
iss := new(InventoryScreenState)
|
||||||
|
|
||||||
iss.prevState = prevState
|
iss.prevState = prevState
|
||||||
|
|
|
@ -3,6 +3,7 @@ package state
|
||||||
import (
|
import (
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
"mvvasilev/last_light/game/model"
|
"mvvasilev/last_light/game/model"
|
||||||
|
"mvvasilev/last_light/game/player"
|
||||||
"mvvasilev/last_light/game/ui"
|
"mvvasilev/last_light/game/ui"
|
||||||
"mvvasilev/last_light/game/world"
|
"mvvasilev/last_light/game/world"
|
||||||
|
|
||||||
|
@ -11,8 +12,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type PlayingState struct {
|
type PlayingState struct {
|
||||||
player *model.Player
|
player *player.Player
|
||||||
someNPC *model.NPC
|
someNPC *model.BasicNPC
|
||||||
|
|
||||||
dungeon *world.Dungeon
|
dungeon *world.Dungeon
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ func BeginPlayingState() *PlayingState {
|
||||||
|
|
||||||
s.dungeon = world.CreateDungeon(mapSize.Width(), mapSize.Height(), 1)
|
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())
|
s.someNPC = model.CreateNPC(s.dungeon.CurrentLevel().NextLevelStaircase())
|
||||||
|
|
||||||
|
@ -172,10 +173,10 @@ func (ps *PlayingState) PickUpItemUnderPlayer() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
success := ps.player.Inventory().Push(*item)
|
success := ps.player.Inventory().Push(item)
|
||||||
|
|
||||||
if !success {
|
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 {
|
func (ps *PlayingState) CollectDrawables() []engine.Drawable {
|
||||||
return engine.Multidraw(engine.CreateDrawingInstructions(func(v views.View) {
|
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) {
|
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 {
|
if tile != nil {
|
||||||
return tile.Presentation()
|
return tile.Presentation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
explored := ps.dungeon.CurrentLevel().Flatten().ExploredTileAt(x, y)
|
||||||
|
|
||||||
|
if explored != nil {
|
||||||
|
return explored.Presentation()
|
||||||
|
}
|
||||||
|
|
||||||
return ' ', tcell.StyleDefault
|
return ' ', tcell.StyleDefault
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -3,7 +3,7 @@ package menu
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
"mvvasilev/last_light/game/model"
|
"mvvasilev/last_light/game/item"
|
||||||
"mvvasilev/last_light/game/ui"
|
"mvvasilev/last_light/game/ui"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
@ -12,7 +12,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type PlayerInventoryMenu struct {
|
type PlayerInventoryMenu struct {
|
||||||
inventory *model.EquippedInventory
|
inventory *item.EquippedInventory
|
||||||
|
|
||||||
inventoryMenu *ui.UIWindow
|
inventoryMenu *ui.UIWindow
|
||||||
armourLabel *ui.UILabel
|
armourLabel *ui.UILabel
|
||||||
|
@ -29,7 +29,7 @@ type PlayerInventoryMenu struct {
|
||||||
selectedInventorySlot engine.Position
|
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 := new(PlayerInventoryMenu)
|
||||||
|
|
||||||
menu.inventory = playerInventory
|
menu.inventory = playerInventory
|
||||||
|
@ -124,8 +124,17 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *model.EquippedInventor
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.CreateSingleLineUILabel(x+3, y+15, fmt.Sprintf("Name: %v", item.Name()), style).Draw(v)
|
name, nameStyle := item.Name()
|
||||||
ui.CreateSingleLineUILabel(x+3, y+16, fmt.Sprintf("Desc: %v", item.Description()), style).Draw(v)
|
|
||||||
|
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)
|
menu.help = ui.CreateSingleLineUILabel(x+2, y+22, "hjkl - move, x - drop, e - equip", style)
|
||||||
|
|
|
@ -32,6 +32,18 @@ func (bsp *BSPDungeonMap) TileAt(x int, y int) Tile {
|
||||||
return bsp.level.TileAt(x, y)
|
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) Tick(dt int64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,10 @@ package world
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
|
"mvvasilev/last_light/game/item"
|
||||||
"mvvasilev/last_light/game/model"
|
"mvvasilev/last_light/game/model"
|
||||||
|
"mvvasilev/last_light/game/rpg"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -104,12 +107,39 @@ type DungeonLevel struct {
|
||||||
|
|
||||||
func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLevel {
|
func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLevel {
|
||||||
|
|
||||||
genTable := make(map[float32]*model.ItemType, 0)
|
genTable := rpg.CreateLootTable()
|
||||||
|
|
||||||
genTable[0.2] = model.ItemTypeFish()
|
genTable.Add(10, func() item.Item {
|
||||||
genTable[0.05] = model.ItemTypeBow()
|
return item.CreateBasicItem(item.ItemTypeFish(), 1)
|
||||||
genTable[0.051] = model.ItemTypeLongsword()
|
})
|
||||||
genTable[0.052] = model.ItemTypeKey()
|
|
||||||
|
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 {
|
var groundLevel interface {
|
||||||
Map
|
Map
|
||||||
|
@ -126,7 +156,7 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLeve
|
||||||
groundLevel = CreateBSPDungeonMap(width, height, 4)
|
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.NextLevelStaircasePosition(),
|
||||||
groundLevel.PlayerSpawnPoint(),
|
groundLevel.PlayerSpawnPoint(),
|
||||||
groundLevel.PreviousLevelStaircasePosition(),
|
groundLevel.PreviousLevelStaircasePosition(),
|
||||||
|
@ -157,6 +187,43 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLeve
|
||||||
return d
|
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 {
|
func (d *DungeonLevel) PlayerSpawnPoint() engine.Position {
|
||||||
return d.groundLevel.PlayerSpawnPoint()
|
return d.groundLevel.PlayerSpawnPoint()
|
||||||
}
|
}
|
||||||
|
@ -185,7 +252,7 @@ func (d *DungeonLevel) MoveEntityTo(uuid uuid.UUID, x, y int) {
|
||||||
d.entityLevel.MoveEntityTo(uuid, x, y)
|
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) {
|
if !d.groundLevel.Size().Contains(x, y) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -199,17 +266,15 @@ func (d *DungeonLevel) RemoveItemAt(x, y int) *model.Item {
|
||||||
|
|
||||||
d.itemLevel.SetTileAt(x, y, nil)
|
d.itemLevel.SetTileAt(x, y, nil)
|
||||||
|
|
||||||
item := model.CreateItem(itemTile.Type(), itemTile.Quantity())
|
return itemTile.Item()
|
||||||
|
|
||||||
return &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() {
|
if !d.TileAt(x, y).Passable() {
|
||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,10 @@ func (edl *EmptyDungeonMap) TileAt(x int, y int) Tile {
|
||||||
return edl.level.TileAt(x, y)
|
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) Tick(dt int64) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,18 @@ func (em *EntityMap) TileAt(x int, y int) Tile {
|
||||||
return em.entities[key]
|
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) {
|
func (em *EntityMap) Tick(dt int64) {
|
||||||
for _, e := range em.entities {
|
for _, e := range em.entities {
|
||||||
e.Entity().Tick(dt)
|
e.Entity().Tick(dt)
|
||||||
|
|
|
@ -3,6 +3,8 @@ package world
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type splitDirection bool
|
type splitDirection bool
|
||||||
|
@ -92,7 +94,7 @@ func CreateBSPDungeonMap(width, height int, numSplits int) *BSPDungeonMap {
|
||||||
staircaseRoom := findRoom(root.right)
|
staircaseRoom := findRoom(root.right)
|
||||||
|
|
||||||
bsp.rooms = rooms
|
bsp.rooms = rooms
|
||||||
bsp.level = CreateBasicMap(tiles)
|
bsp.level = CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey))
|
||||||
|
|
||||||
bsp.playerSpawnPoint = engine.PositionAt(
|
bsp.playerSpawnPoint = engine.PositionAt(
|
||||||
spawnRoom.Position().X()+spawnRoom.Size().Width()/2,
|
spawnRoom.Position().X()+spawnRoom.Size().Width()/2,
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
package world
|
package world
|
||||||
|
|
||||||
func CreateEmptyDungeonLevel(width, height int) *EmptyDungeonMap {
|
import "github.com/gdamore/tcell/v2"
|
||||||
m := new(EmptyDungeonMap)
|
|
||||||
|
|
||||||
|
func CreateEmptyDungeonLevel(width, height int) *BasicMap {
|
||||||
tiles := make([][]Tile, height)
|
tiles := make([][]Tile, height)
|
||||||
|
|
||||||
for h := range height {
|
for h := range height {
|
||||||
tiles[h] = make([]Tile, width)
|
tiles[h] = make([]Tile, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.level = CreateBasicMap(tiles)
|
return CreateBasicMap(tiles, tcell.StyleDefault.Foreground(tcell.ColorLightSlateGrey))
|
||||||
|
|
||||||
//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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -2,12 +2,17 @@ package world
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Map interface {
|
type Map interface {
|
||||||
Size() engine.Size
|
Size() engine.Size
|
||||||
SetTileAt(x, y int, t Tile) Tile
|
SetTileAt(x, y int, t Tile) Tile
|
||||||
TileAt(x, y int) Tile
|
TileAt(x, y int) Tile
|
||||||
|
IsInBounds(x, y int) bool
|
||||||
|
ExploredTileAt(x, y int) Tile
|
||||||
|
MarkExplored(x, y int)
|
||||||
Tick(dt int64)
|
Tick(dt int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,17 +34,22 @@ type WithPreviousLevelStaircasePosition interface {
|
||||||
|
|
||||||
type BasicMap struct {
|
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 := new(BasicMap)
|
||||||
|
|
||||||
bm.tiles = tiles
|
bm.tiles = tiles
|
||||||
|
bm.exploredTiles = make(map[engine.Position]Tile, 0)
|
||||||
|
bm.exploredStyle = exploredStyle
|
||||||
|
|
||||||
return bm
|
return bm
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bm *BasicMap) Tick() {
|
func (bm *BasicMap) Tick(dt int64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bm *BasicMap) Size() engine.Size {
|
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 {
|
func (bm *BasicMap) SetTileAt(x int, y int, t Tile) Tile {
|
||||||
if len(bm.tiles) <= y || len(bm.tiles[0]) <= x {
|
if !bm.IsInBounds(x, y) {
|
||||||
return CreateStaticTile(x, y, TileTypeVoid())
|
|
||||||
}
|
|
||||||
|
|
||||||
if x < 0 || y < 0 {
|
|
||||||
return CreateStaticTile(x, y, TileTypeVoid())
|
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 {
|
func (bm *BasicMap) TileAt(x int, y int) Tile {
|
||||||
if x < 0 || y < 0 {
|
if !bm.IsInBounds(x, y) {
|
||||||
return CreateStaticTile(x, y, TileTypeVoid())
|
|
||||||
}
|
|
||||||
|
|
||||||
if x >= bm.Size().Width() || y >= bm.Size().Height() {
|
|
||||||
return CreateStaticTile(x, y, TileTypeVoid())
|
return CreateStaticTile(x, y, TileTypeVoid())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,3 +79,29 @@ func (bm *BasicMap) TileAt(x int, y int) Tile {
|
||||||
|
|
||||||
return 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)
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
func (mm *MultilevelMap) CollectTilesAt(x, y int, filter func(t Tile) bool) []Tile {
|
||||||
tiles := make([]Tile, len(mm.layers))
|
tiles := make([]Tile, len(mm.layers))
|
||||||
|
|
||||||
if x < 0 || y < 0 {
|
if !mm.IsInBounds(x, y) {
|
||||||
return tiles
|
|
||||||
}
|
|
||||||
|
|
||||||
if x >= mm.Size().Width() || y >= mm.Size().Height() {
|
|
||||||
return tiles
|
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 {
|
func (mm *MultilevelMap) TileAt(x int, y int) Tile {
|
||||||
if x < 0 || y < 0 {
|
if !mm.IsInBounds(x, y) {
|
||||||
return CreateStaticTile(x, y, TileTypeVoid())
|
|
||||||
}
|
|
||||||
|
|
||||||
if x >= mm.Size().Width() || y >= mm.Size().Height() {
|
|
||||||
return CreateStaticTile(x, y, TileTypeVoid())
|
return CreateStaticTile(x, y, TileTypeVoid())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,12 +78,38 @@ func (mm *MultilevelMap) TileAt(x int, y int) Tile {
|
||||||
return CreateStaticTile(x, y, TileTypeVoid())
|
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 {
|
if x < 0 || y < 0 {
|
||||||
return CreateStaticTile(x, y, TileTypeVoid())
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if x >= mm.Size().Width() || y >= mm.Size().Height() {
|
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())
|
return CreateStaticTile(x, y, TileTypeVoid())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package world
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mvvasilev/last_light/engine"
|
"mvvasilev/last_light/engine"
|
||||||
|
"mvvasilev/last_light/game/item"
|
||||||
"mvvasilev/last_light/game/model"
|
"mvvasilev/last_light/game/model"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
@ -26,6 +27,7 @@ type TileType struct {
|
||||||
Passable bool
|
Passable bool
|
||||||
Presentation rune
|
Presentation rune
|
||||||
Transparent bool
|
Transparent bool
|
||||||
|
Opaque bool
|
||||||
Style tcell.Style
|
Style tcell.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +37,7 @@ func TileTypeGround() TileType {
|
||||||
Passable: true,
|
Passable: true,
|
||||||
Presentation: '.',
|
Presentation: '.',
|
||||||
Transparent: false,
|
Transparent: false,
|
||||||
|
Opaque: false,
|
||||||
Style: tcell.StyleDefault,
|
Style: tcell.StyleDefault,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +48,7 @@ func TileTypeRock() TileType {
|
||||||
Passable: false,
|
Passable: false,
|
||||||
Presentation: '█',
|
Presentation: '█',
|
||||||
Transparent: false,
|
Transparent: false,
|
||||||
|
Opaque: true,
|
||||||
Style: tcell.StyleDefault,
|
Style: tcell.StyleDefault,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +59,7 @@ func TileTypeGrass() TileType {
|
||||||
Passable: true,
|
Passable: true,
|
||||||
Presentation: ',',
|
Presentation: ',',
|
||||||
Transparent: false,
|
Transparent: false,
|
||||||
|
Opaque: false,
|
||||||
Style: tcell.StyleDefault,
|
Style: tcell.StyleDefault,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,6 +70,7 @@ func TileTypeVoid() TileType {
|
||||||
Passable: false,
|
Passable: false,
|
||||||
Presentation: ' ',
|
Presentation: ' ',
|
||||||
Transparent: true,
|
Transparent: true,
|
||||||
|
Opaque: true,
|
||||||
Style: tcell.StyleDefault,
|
Style: tcell.StyleDefault,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,6 +81,7 @@ func TileTypeWall() TileType {
|
||||||
Passable: false,
|
Passable: false,
|
||||||
Presentation: '#',
|
Presentation: '#',
|
||||||
Transparent: false,
|
Transparent: false,
|
||||||
|
Opaque: true,
|
||||||
Style: tcell.StyleDefault.Background(tcell.ColorGray),
|
Style: tcell.StyleDefault.Background(tcell.ColorGray),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,6 +92,7 @@ func TileTypeClosedDoor() TileType {
|
||||||
Passable: false,
|
Passable: false,
|
||||||
Transparent: false,
|
Transparent: false,
|
||||||
Presentation: '[',
|
Presentation: '[',
|
||||||
|
Opaque: true,
|
||||||
Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue).Background(tcell.ColorSaddleBrown),
|
Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue).Background(tcell.ColorSaddleBrown),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,6 +103,7 @@ func TileTypeOpenDoor() TileType {
|
||||||
Passable: false,
|
Passable: false,
|
||||||
Transparent: false,
|
Transparent: false,
|
||||||
Presentation: '_',
|
Presentation: '_',
|
||||||
|
Opaque: false,
|
||||||
Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue),
|
Style: tcell.StyleDefault.Foreground(tcell.ColorLightSteelBlue),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,6 +114,7 @@ func TileTypeStaircaseDown() TileType {
|
||||||
Passable: true,
|
Passable: true,
|
||||||
Transparent: false,
|
Transparent: false,
|
||||||
Presentation: '≡',
|
Presentation: '≡',
|
||||||
|
Opaque: false,
|
||||||
Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold),
|
Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,6 +125,7 @@ func TileTypeStaircaseUp() TileType {
|
||||||
Passable: true,
|
Passable: true,
|
||||||
Transparent: false,
|
Transparent: false,
|
||||||
Presentation: '^',
|
Presentation: '^',
|
||||||
|
Opaque: false,
|
||||||
Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold),
|
Style: tcell.StyleDefault.Foreground(tcell.ColorDarkSlateGray).Attributes(tcell.AttrBold),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,11 +135,15 @@ type Tile interface {
|
||||||
Presentation() (rune, tcell.Style)
|
Presentation() (rune, tcell.Style)
|
||||||
Passable() bool
|
Passable() bool
|
||||||
Transparent() bool
|
Transparent() bool
|
||||||
|
Opaque() bool
|
||||||
|
Type() TileType
|
||||||
}
|
}
|
||||||
|
|
||||||
type StaticTile struct {
|
type StaticTile struct {
|
||||||
position engine.Position
|
position engine.Position
|
||||||
t TileType
|
t TileType
|
||||||
|
|
||||||
|
style tcell.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateStaticTile(x, y int, t TileType) Tile {
|
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.position = engine.PositionAt(x, y)
|
||||||
st.t = t
|
st.t = t
|
||||||
|
st.style = t.Style
|
||||||
|
|
||||||
return st
|
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 {
|
func (st *StaticTile) Position() engine.Position {
|
||||||
return st.position
|
return st.position
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *StaticTile) Presentation() (rune, tcell.Style) {
|
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 {
|
func (st *StaticTile) Passable() bool {
|
||||||
|
@ -156,32 +180,30 @@ func (st *StaticTile) Transparent() bool {
|
||||||
return st.t.Transparent
|
return st.t.Transparent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (st *StaticTile) Opaque() bool {
|
||||||
|
return st.t.Opaque
|
||||||
|
}
|
||||||
|
|
||||||
func (st *StaticTile) Type() TileType {
|
func (st *StaticTile) Type() TileType {
|
||||||
return st.t
|
return st.t
|
||||||
}
|
}
|
||||||
|
|
||||||
type ItemTile struct {
|
type ItemTile struct {
|
||||||
position engine.Position
|
position engine.Position
|
||||||
itemType *model.ItemType
|
item item.Item
|
||||||
quantity int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateItemTile(position engine.Position, itemType *model.ItemType, quantity int) *ItemTile {
|
func CreateItemTile(position engine.Position, item item.Item) *ItemTile {
|
||||||
it := new(ItemTile)
|
it := new(ItemTile)
|
||||||
|
|
||||||
it.position = position
|
it.position = position
|
||||||
it.itemType = itemType
|
it.item = item
|
||||||
it.quantity = quantity
|
|
||||||
|
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *ItemTile) Type() *model.ItemType {
|
func (it *ItemTile) Item() item.Item {
|
||||||
return it.itemType
|
return it.item
|
||||||
}
|
|
||||||
|
|
||||||
func (it *ItemTile) Quantity() int {
|
|
||||||
return it.quantity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *ItemTile) Position() engine.Position {
|
func (it *ItemTile) Position() engine.Position {
|
||||||
|
@ -189,7 +211,7 @@ func (it *ItemTile) Position() engine.Position {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *ItemTile) Presentation() (rune, tcell.Style) {
|
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 {
|
func (it *ItemTile) Passable() bool {
|
||||||
|
@ -200,6 +222,14 @@ func (it *ItemTile) Transparent() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (it *ItemTile) Opaque() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *ItemTile) Type() TileType {
|
||||||
|
return TileType{}
|
||||||
|
}
|
||||||
|
|
||||||
type EntityTile interface {
|
type EntityTile interface {
|
||||||
Entity() model.MovableEntity
|
Entity() model.MovableEntity
|
||||||
Tile
|
Tile
|
||||||
|
@ -239,3 +269,11 @@ func (bet *BasicEntityTile) Passable() bool {
|
||||||
func (bet *BasicEntityTile) Transparent() bool {
|
func (bet *BasicEntityTile) Transparent() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bet *BasicEntityTile) Opaque() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bet *BasicEntityTile) Type() TileType {
|
||||||
|
return TileType{}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue