mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-11 17:25:01 +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
|
||||
|
||||
- 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
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
|
||||
|
||||
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
|
||||
}
|
|
@ -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
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"
|
||||
)
|
||||
|
||||
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) {
|
||||
}
|
||||
|
|
|
@ -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
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 (
|
||||
"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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
"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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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{}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue