Turn system, bunch of other things

This commit is contained in:
Miroslav Vasilev 2024-05-30 23:39:54 +03:00
parent b30dc8dec3
commit 0f093dd7f9
39 changed files with 1339 additions and 420 deletions

50
engine/event_logger.go Normal file
View file

@ -0,0 +1,50 @@
package engine
import (
"time"
)
type GameEvent struct {
time time.Time
contents string
}
func (ge *GameEvent) Time() time.Time {
return ge.time
}
func (ge *GameEvent) Contents() string {
return ge.contents
}
type GameEventLog struct {
logs []*GameEvent
maxSize int
}
func CreateGameEventLog(maxSize int) *GameEventLog {
return &GameEventLog{
logs: make([]*GameEvent, 0, 10),
maxSize: maxSize,
}
}
func (log *GameEventLog) Log(contents string) {
log.logs = append(log.logs, &GameEvent{
time: time.Now(),
contents: contents,
})
if len(log.logs) > log.maxSize {
log.logs = log.logs[1:len(log.logs)]
}
}
func (log *GameEventLog) Tail(n int) []*GameEvent {
if n > len(log.logs) {
return log.logs
}
return log.logs[len(log.logs)-n : len(log.logs)]
}

73
engine/priority_queue.go Normal file
View file

@ -0,0 +1,73 @@
package engine
import "slices"
// [ a = 1, b = 2, c = 3, d = 4 ]
// a = 1 <- [ b = 2, c = 3, d = 4 ]
// do a action
// sub a priority from queue: [ b = 1, c = 2, d = 3 ]
// [ b = 1, a = 1, c = 2, d = 3 ] <- a = 1
// b = 1 <- [ a = 1, c = 2, d = 3 ]
type priorityQueueItem[T interface{}] struct {
priority int
value T
}
type PriorityQueue[T interface{}] struct {
queue []*priorityQueueItem[T]
}
func CreatePriorityQueue[T interface{}]() *PriorityQueue[T] {
return &PriorityQueue[T]{
queue: make([]*priorityQueueItem[T], 0, 10),
}
}
func (pq *PriorityQueue[T]) Enqueue(prio int, value T) {
pq.queue = append(pq.queue, &priorityQueueItem[T]{priority: prio, value: value})
slices.SortFunc(pq.queue, func(e1, e2 *priorityQueueItem[T]) int { return e1.priority - e2.priority })
}
func (pq *PriorityQueue[T]) AdjustPriorities(amount int) {
for _, e := range pq.queue {
e.priority += amount
}
}
// Pop element w/ lowest priority
func (pq *PriorityQueue[T]) DequeueValue() (value T) {
if len(pq.queue) < 1 {
return
}
value, pq.queue = pq.queue[0].value, pq.queue[1:len(pq.queue)]
return
}
// Peek element w/ lowest priority
func (pq *PriorityQueue[T]) Peek() (priority int, value T) {
if len(pq.queue) < 1 {
return
}
priority, value = pq.queue[0].priority, pq.queue[0].value
return
}
// Pop element w/ lowest priority, returning the priority as well as the value
func (pq *PriorityQueue[T]) Dequeue() (priority int, value T) {
if len(pq.queue) < 1 {
return
}
priority, value, pq.queue = pq.queue[0].priority, pq.queue[0].value, pq.queue[1:len(pq.queue)]
return
}
func (pq *PriorityQueue[T]) Clear() {
pq.queue = make([]*priorityQueueItem[T], 0, 10)
}

53
engine/raycasting.go Normal file
View file

@ -0,0 +1,53 @@
package engine
func CastRay(pos1, pos2 Position) (points []Position) {
x1, y1 := pos1.XY()
x2, y2 := pos2.XY()
isSteep := AbsInt(y2-y1) > AbsInt(x2-x1)
if isSteep {
x1, y1 = y1, x1
x2, y2 = y2, x2
}
reversed := false
if x1 > x2 {
x1, x2 = x2, x1
y1, y2 = y2, y1
reversed = true
}
deltaX := x2 - x1
deltaY := AbsInt(y2 - y1)
err := deltaX / 2
y := y1
var ystep int
if y1 < y2 {
ystep = 1
} else {
ystep = -1
}
for x := x1; x < x2+1; x++ {
if isSteep {
points = append(points, Position{y, x})
} else {
points = append(points, Position{x, y})
}
err -= deltaY
if err < 0 {
y += ystep
err += deltaX
}
}
if reversed {
for i, j := 0, len(points)-1; i < j; i, j = i+1, j-1 {
points[i], points[j] = points[j], points[i]
}
}
return
}

View file

@ -126,6 +126,10 @@ func (rect Rectangle) Position() Position {
return rect.position
}
func (rect Rectangle) Size() Size {
return rect.size
}
func (rect Rectangle) drawBorders(v views.View) {
width := rect.size.Width()
height := rect.size.Height()
@ -170,3 +174,43 @@ func (rect Rectangle) Draw(v views.View) {
rect.drawFill(v)
}
}
func DrawRectangle(
x int,
y int,
width int,
height int,
nwCorner, northBorder, neCorner,
westBorder, fillRune, eastBorder,
swCorner, southBorder, seCorner rune,
isBorderless, isFilled bool,
style tcell.Style, v views.View,
) {
v.SetContent(x, y, nwCorner, nil, style)
v.SetContent(x+width-1, y, neCorner, nil, style)
v.SetContent(x, y+height-1, swCorner, nil, style)
v.SetContent(x+width-1, y+height-1, seCorner, nil, style)
if !isBorderless {
for w := 1; w < width-1; w++ {
v.SetContent(x+w, y, northBorder, nil, style)
v.SetContent(x+w, y+height-1, southBorder, nil, style)
}
for h := 1; h < height-1; h++ {
v.SetContent(x, y+h, westBorder, nil, style)
v.SetContent(x+width-1, y+h, eastBorder, nil, style)
}
}
if !isFilled {
return
}
for w := 1; w < width-1; w++ {
for h := 1; h < height-1; h++ {
v.SetContent(x+w, y+h, fillRune, nil, style)
}
}
}

View file

@ -109,3 +109,15 @@ func (t *Text) Draw(s views.View) {
currentHPos += runeCount + 1 // add +1 to account for space after word
}
}
func DrawText(x, y int, content string, style tcell.Style, s views.View) {
currentHPos := 0
currentVPos := 0
lastPos := 0
for _, r := range content {
s.SetContent(x+currentHPos+lastPos, y+currentVPos, r, nil, style)
lastPos++
}
}

View file

@ -143,3 +143,13 @@ func MapSlice[S ~[]E, E any, R any](slice S, mappingFunc func(e E) R) []R {
return newSlice
}
func AbsInt(val int) int {
switch {
case val < 0:
return -val
case val == 0:
return 0
}
return val
}

View file

@ -2,12 +2,17 @@ package game
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/state"
"mvvasilev/last_light/game/turns"
"github.com/gdamore/tcell/v2"
)
type Game struct {
turnSystem *turns.TurnSystem
inputSystem *input.InputSystem
state state.GameState
quitGame bool
@ -16,7 +21,11 @@ type Game struct {
func CreateGame() *Game {
game := new(Game)
game.state = state.NewMainMenuState()
game.turnSystem = turns.CreateTurnSystem()
game.inputSystem = input.CreateInputSystemWithDefaultBindings()
game.state = state.CreateMainMenuState(game.turnSystem, game.inputSystem)
return game
}
@ -26,24 +35,22 @@ func (g *Game) Input(ev *tcell.EventKey) {
g.quitGame = true
}
g.state.OnInput(ev)
g.inputSystem.Input(g.state.InputContext(), ev)
}
func (g *Game) Tick(dt int64) bool {
if g.quitGame {
return false
}
func (g *Game) Tick(dt int64) (continueGame bool) {
continueGame = !g.quitGame
s := g.state.OnTick(dt)
switch s.(type) {
case *state.QuitState:
return false
g.quitGame = true
}
g.state = s
return true
return
}
func (g *Game) CollectDrawables() []engine.Drawable {

111
game/input/input_system.go Normal file
View file

@ -0,0 +1,111 @@
package input
import (
"fmt"
"github.com/gdamore/tcell/v2"
)
type Context string
const (
InputContext_Play = "play"
InputContext_Menu = "menu"
InputContext_Inventory = "inventory"
)
type InputKey string
func InputKeyOf(context Context, mod tcell.ModMask, key tcell.Key, r rune) InputKey {
return InputKey(fmt.Sprintf("%v-%v-%v-%v", context, mod, key, r))
}
type InputAction int
const (
InputAction_None InputAction = iota
InputAction_MovePlayer_North
InputAction_MovePlayer_South
InputAction_MovePlayer_East
InputAction_MovePlayer_West
InputAction_Interact
InputAction_OpenInventory
InputAction_PickUpItem
InputAction_OpenLogs
InputAction_DropItem
InputAction_EquipItem
InputAction_UnequipItem
InputAction_PauseGame
InputAction_Menu_HighlightDown
InputAction_Menu_HighlightUp
InputAction_Menu_HighlightLeft
InputAction_Menu_HighlightRight
InputAction_Menu_Select
InputAction_Menu_Exit
)
type InputSystem struct {
keyBindings map[InputKey]InputAction
nextAction InputAction
}
func CreateInputSystemWithDefaultBindings() *InputSystem {
return &InputSystem{
keyBindings: map[InputKey]InputAction{
InputKeyOf(InputContext_Play, 0, tcell.KeyUp, 0): InputAction_MovePlayer_North,
InputKeyOf(InputContext_Play, 0, tcell.KeyDown, 0): InputAction_MovePlayer_South,
InputKeyOf(InputContext_Play, 0, tcell.KeyLeft, 0): InputAction_MovePlayer_West,
InputKeyOf(InputContext_Play, 0, tcell.KeyRight, 0): InputAction_MovePlayer_East,
InputKeyOf(InputContext_Play, 0, tcell.KeyEsc, 0): InputAction_PauseGame,
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'i'): InputAction_OpenInventory,
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'l'): InputAction_OpenLogs,
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'e'): InputAction_Interact,
InputKeyOf(InputContext_Play, 0, tcell.KeyRune, 'p'): InputAction_PickUpItem,
InputKeyOf(InputContext_Menu, 0, tcell.KeyESC, 0): InputAction_Menu_Exit,
InputKeyOf(InputContext_Menu, 0, tcell.KeyLeft, 0): InputAction_Menu_HighlightLeft,
InputKeyOf(InputContext_Menu, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight,
InputKeyOf(InputContext_Menu, 0, tcell.KeyUp, 0): InputAction_Menu_HighlightUp,
InputKeyOf(InputContext_Menu, 0, tcell.KeyDown, 0): InputAction_Menu_HighlightDown,
InputKeyOf(InputContext_Menu, 0, tcell.KeyCR, 13): InputAction_Menu_Select,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyESC, 0): InputAction_Menu_Exit,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyRune, 'i'): InputAction_Menu_Exit,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyRune, 'e'): InputAction_EquipItem,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyRune, 'd'): InputAction_DropItem,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyLeft, 0): InputAction_Menu_HighlightLeft,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyRight, 0): InputAction_Menu_HighlightRight,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyUp, 0): InputAction_Menu_HighlightUp,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyDown, 0): InputAction_Menu_HighlightDown,
},
}
}
func (kb *InputSystem) ImportBindings(imports map[InputKey]InputAction) {
kb.keyBindings = imports
}
func (kb *InputSystem) ExportBindings() map[InputKey]InputAction {
return kb.keyBindings
}
func (kb *InputSystem) Bind(key InputKey, action InputAction) {
kb.keyBindings[key] = action
}
func (kb *InputSystem) Input(context Context, ev *tcell.EventKey) {
kb.nextAction = kb.keyBindings[InputKeyOf(context, ev.Modifiers(), ev.Key(), ev.Rune())]
}
func (kb *InputSystem) NextAction() (nextAction InputAction) {
nextAction = kb.nextAction
kb.nextAction = InputAction_None
return
}

View file

@ -43,7 +43,7 @@ func (inv *BasicInventory) Push(i Item) (success bool) {
// Try to first find a matching item with capacity
for index, existingItem := range inv.contents {
if existingItem != nil && existingItem.Type() == itemType {
if existingItem != nil && existingItem.Type().Id() == itemType.Id() {
if existingItem.Quantity()+1 > existingItem.Type().MaxStack() {
continue
}

View file

@ -5,6 +5,7 @@ import (
)
type ItemType interface {
Id() int
Name() string
Description() string
TileIcon() rune
@ -15,6 +16,7 @@ type ItemType interface {
}
type BasicItemType struct {
id int
name string
description string
tileIcon rune
@ -26,6 +28,7 @@ type BasicItemType struct {
}
func CreateBasicItemType(
id int,
name, description string,
tileIcon rune,
icon string,
@ -34,6 +37,7 @@ func CreateBasicItemType(
style tcell.Style,
) *BasicItemType {
return &BasicItemType{
id: id,
name: name,
description: description,
tileIcon: tileIcon,
@ -44,6 +48,10 @@ func CreateBasicItemType(
}
}
func (it *BasicItemType) Id() int {
return it.id
}
func (it *BasicItemType) Name() string {
return it.name
}
@ -74,6 +82,7 @@ func (it *BasicItemType) EquippableSlot() EquippedSlot {
func ItemTypeFish() ItemType {
return &BasicItemType{
id: 0,
name: "Fish",
description: "What's a fish doing down here?",
tileIcon: '>',
@ -86,6 +95,7 @@ func ItemTypeFish() ItemType {
func ItemTypeGold() ItemType {
return &BasicItemType{
id: 1,
name: "Gold",
description: "Not all those who wander are lost",
tileIcon: '¤',
@ -98,6 +108,7 @@ func ItemTypeGold() ItemType {
func ItemTypeArrow() ItemType {
return &BasicItemType{
id: 2,
name: "Arrow",
description: "Ammunition for a bow",
tileIcon: '-',
@ -110,6 +121,7 @@ func ItemTypeArrow() ItemType {
func ItemTypeKey() ItemType {
return &BasicItemType{
id: 3,
name: "Key",
description: "Indispensable for unlocking things",
tileIcon: '¬',

View file

@ -1,4 +1,4 @@
package model
package npc
import (
"mvvasilev/last_light/engine"
@ -11,21 +11,36 @@ type Direction int
const (
DirectionNone Direction = iota
DirectionUp
DirectionDown
DirectionLeft
DirectionRight
North
South
West
East
)
func DirectionName(dir Direction) string {
switch dir {
case North:
return "North"
case South:
return "South"
case West:
return "West"
case East:
return "East"
default:
return "Unknown"
}
}
func MovementDirectionOffset(dir Direction) (int, int) {
switch dir {
case DirectionUp:
case North:
return 0, -1
case DirectionDown:
case South:
return 0, 1
case DirectionLeft:
case West:
return -1, 0
case DirectionRight:
case East:
return 1, 0
}

View file

@ -1,4 +1,4 @@
package model
package npc
import (
"mvvasilev/last_light/engine"

View file

@ -24,7 +24,15 @@ func CreatePlayer(x, y int) *Player {
p.id = uuid.New()
p.position = engine.PositionAt(x, y)
p.inventory = item.CreateEquippedInventory()
p.BasicRPGEntity = rpg.CreateBasicRPGEntity()
p.BasicRPGEntity = rpg.CreateBasicRPGEntity(
map[rpg.Stat]int{
rpg.Stat_Attributes_Constitution: 10,
rpg.Stat_Attributes_Dexterity: 10,
rpg.Stat_Attributes_Strength: 10,
rpg.Stat_Attributes_Intelligence: 10,
},
map[rpg.Stat][]rpg.StatModifier{},
)
return p
}

View file

@ -0,0 +1,16 @@
package player
import "mvvasilev/last_light/game/input"
func PlayerTurn(inputSystem *input.InputSystem) (complete, requeue bool) {
requeue = true
complete = false
nextAction := inputSystem.NextAction()
if nextAction == input.InputAction_None {
return
}
return
}

View file

@ -4,11 +4,18 @@ import (
"math/rand"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item"
"slices"
"unicode"
"unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
)
// TODO: figure out event logging. Need to inform the player of the things that are happening...
const MaxNumberOfModifiers = 6
type ItemSupplier func() item.Item
type LootTable struct {
@ -58,34 +65,89 @@ func pointPerRarity(rarity ItemRarity) int {
}
}
func generateUniqueItemName() string {
starts := []string{
"du", "nol", "ma", "re",
"ka", "gro", "hru", "lo",
"ara", "ke", "ko", "uro",
"ne", "pe", "pa", "pho",
}
middles := []string{
"kora", "duru", "kolku", "dila",
"luio", "ghro", "kelma", "riga",
"fela", "fiya", "numa", "ruta",
}
end := []string{
"dum", "dor", "dar", "thar",
"thor", "thum", "hor", "hum",
"her", "kom", "kur", "kyr",
"mor", "mar", "man", "kum",
"tum",
}
name := starts[rand.Intn(len(starts))] + middles[rand.Intn(len(middles))] + end[rand.Intn(len(end))]
r, size := utf8.DecodeRuneInString(name)
return string(unicode.ToUpper(r)) + name[size:]
}
func randomAdjective() string {
adjectives := []string{
"shiny", "gruesome", "sharp", "tattered",
"mediocre", "unusual", "bright", "rusty",
"dreadful", "exceptional", "old", "bent",
"ancient", "crude", "dented", "cool",
}
adj := adjectives[rand.Intn(len(adjectives))]
r, size := utf8.DecodeRuneInString(adj)
return string(unicode.ToUpper(r)) + adj[size:]
}
func randomSuffix() string {
suffixes := []string{
"of the Monkey", "of the Tiger", "of the Elephant", "of the Slug",
"of Elven Make",
}
return suffixes[rand.Intn(len(suffixes))]
}
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)
return randomAdjective() + " " + itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorLime)
case ItemRarity_Rare:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorBlue)
return itemType.Name() + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorBlue)
case ItemRarity_Epic:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorPurple)
return randomAdjective() + " " + itemType.Name() + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorPurple)
case ItemRarity_Legendary:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorOrange).Attributes(tcell.AttrBold)
return generateUniqueItemName() + ", Legendary " + itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorOrange).Attributes(tcell.AttrBold)
default:
return itemType.Name(), tcell.StyleDefault
}
}
func randomStat() Stat {
stats := []Stat{
func randomStat(metaItemTypes []RPGItemMetaType) Stat {
stats := make(map[RPGItemMetaType][]Stat, 0)
stats[MetaItemType_Weapon] = []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,
}
stats[MetaItemType_Physical_Weapon] = []Stat{
Stat_PhysicalPrecisionBonus,
Stat_DamageBonus_Physical_Slashing,
Stat_DamageBonus_Physical_Piercing,
Stat_DamageBonus_Physical_Bludgeoning,
@ -95,37 +157,102 @@ func randomStat() Stat {
Stat_DamageBonus_Magic_Thunder,
Stat_DamageBonus_Magic_Acid,
Stat_DamageBonus_Magic_Poison,
}
stats[MetaItemType_Magic_Weapon] = []Stat{
Stat_MagicPrecisionBonus,
Stat_DamageBonus_Magic_Fire,
Stat_DamageBonus_Magic_Cold,
Stat_DamageBonus_Magic_Necrotic,
Stat_DamageBonus_Magic_Thunder,
Stat_DamageBonus_Magic_Acid,
Stat_DamageBonus_Magic_Poison,
}
stats[MetaItemType_Armour] = []Stat{
Stat_EvasionBonus,
Stat_DamageBonus_Physical_Unarmed,
Stat_MaxHealthBonus,
}
return stats[rand.Intn(len(stats))]
stats[MetaItemType_Magic_Armour] = []Stat{
Stat_MagicPrecisionBonus,
Stat_DamageBonus_Magic_Fire,
Stat_DamageBonus_Magic_Cold,
Stat_DamageBonus_Magic_Necrotic,
Stat_DamageBonus_Magic_Thunder,
Stat_DamageBonus_Magic_Acid,
Stat_DamageBonus_Magic_Poison,
}
stats[MetaItemType_Physical_Armour] = []Stat{
Stat_PhysicalPrecisionBonus,
Stat_DamageBonus_Physical_Slashing,
Stat_DamageBonus_Physical_Piercing,
Stat_DamageBonus_Physical_Bludgeoning,
}
possibleStats := make([]Stat, 0, 10)
for _, mt := range metaItemTypes {
possibleStats = append(possibleStats, stats[mt]...)
}
return slices.Compact(possibleStats)[rand.Intn(len(stats))]
}
func generateItemStatModifiers(rarity ItemRarity) []StatModifier {
func generateItemStatModifiers(itemType RPGItemType, rarity ItemRarity) []StatModifier {
points := pointPerRarity(rarity)
modifiers := []StatModifier{}
modifiers := make(map[Stat]*StatModifier, 0)
for {
if points <= 0 {
// If no points remain, or if the number of modifiers on the item reaches the maximum
if points <= 0 || len(modifiers) == MaxNumberOfModifiers {
break
}
modAmount := engine.RandInt(-points/2, points)
// Random chance to increase or decrease a stat
modAmount := engine.RandInt(-points, points)
if modAmount == 0 {
continue
}
modifiers = append(modifiers, StatModifier{
Id: StatModifierId(uuid.New().String()),
Stat: randomStat(),
Bonus: modAmount,
})
stat := randomStat(itemType.MetaTypes())
points -= modAmount
existingForStat := modifiers[stat]
// If this stat modifier already exists on the item, add the new modification amount to the old
if existingForStat != nil {
existingForStat.Bonus += modAmount
// If the added amount is 0, remove the modifier
if existingForStat.Bonus == 0 {
delete(modifiers, stat)
} else {
modifiers[stat] = existingForStat
}
} else {
// Otherwise, append a new stat modifier
modifiers[stat] = &StatModifier{
Id: StatModifierId(uuid.New().String()),
Stat: stat,
Bonus: modAmount,
}
}
// Decrease amount of points left by absolute value
points -= engine.AbsInt(modAmount)
}
return modifiers
vals := make([]StatModifier, 0, len(modifiers))
for _, v := range modifiers {
vals = append(vals, *v)
}
return vals
}
// Each rarity gets an amount of generation points, the higher the rarity, the more points
@ -138,6 +265,6 @@ func GenerateItemOfTypeAndRarity(itemType RPGItemType, rarity ItemRarity) RPGIte
name,
style,
itemType,
generateItemStatModifiers(rarity),
generateItemStatModifiers(itemType, rarity),
)
}

View file

@ -21,10 +21,10 @@ type BasicRPGEntity struct {
currentHealth int
}
func CreateBasicRPGEntity() *BasicRPGEntity {
func CreateBasicRPGEntity(baseStats map[Stat]int, statModifiers map[Stat][]StatModifier) *BasicRPGEntity {
return &BasicRPGEntity{
stats: make(map[Stat]int, 0),
statModifiers: make(map[Stat][]StatModifier, 0),
stats: baseStats,
statModifiers: statModifiers,
currentHealth: 0,
}
}
@ -60,7 +60,7 @@ func (brpg *BasicRPGEntity) AddStatModifier(modifier StatModifier) {
}
func (brpg *BasicRPGEntity) RemoveStatModifier(id StatModifierId) {
// TODO
}
func (brpg *BasicRPGEntity) CurrentHealth() int {

View file

@ -6,8 +6,20 @@ import (
"github.com/gdamore/tcell/v2"
)
type RPGItemMetaType int
const (
MetaItemType_Physical_Weapon RPGItemMetaType = iota
MetaItemType_Magic_Weapon
MetaItemType_Weapon
MetaItemType_Physical_Armour
MetaItemType_Magic_Armour
MetaItemType_Armour
)
type RPGItemType interface {
RollDamage() func(victim, attacker RPGEntity) (damage int, dmgType DamageType)
MetaTypes() []RPGItemMetaType
item.ItemType
}
@ -21,6 +33,8 @@ type RPGItem interface {
type BasicRPGItemType struct {
damageRollFunc func(victim, attacker RPGEntity) (damage int, dmgType DamageType)
metaTypes []RPGItemMetaType
*item.BasicItemType
}
@ -28,13 +42,19 @@ func (it *BasicRPGItemType) RollDamage() func(victim, attacker RPGEntity) (damag
return it.damageRollFunc
}
func (it *BasicRPGItemType) MetaTypes() []RPGItemMetaType {
return it.metaTypes
}
func ItemTypeBow() RPGItemType {
return &BasicRPGItemType{
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
// TODO: Ranged
return RollD8(1), DamageType_Physical_Piercing
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1000,
"Bow",
"To shoot arrows with",
')',
@ -51,7 +71,9 @@ func ItemTypeLongsword() RPGItemType {
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return RollD8(1), DamageType_Physical_Slashing
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1001,
"Longsword",
"You know nothing.",
'/',
@ -68,7 +90,9 @@ func ItemTypeClub() RPGItemType {
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return RollD8(1), DamageType_Physical_Bludgeoning
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1002,
"Club",
"Bonk",
'!',
@ -85,7 +109,9 @@ func ItemTypeDagger() RPGItemType {
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Piercing
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1003,
"Dagger",
"Stabby, stabby",
'-',
@ -102,7 +128,9 @@ func ItemTypeHandaxe() RPGItemType {
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Slashing
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1004,
"Handaxe",
"Choppy, choppy",
'¶',
@ -120,7 +148,9 @@ func ItemTypeJavelin() RPGItemType {
// TODO: Ranged
return RollD6(1), DamageType_Physical_Piercing
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1005,
"Javelin",
"Ranged pokey, pokey",
'Î',
@ -137,7 +167,9 @@ func ItemTypeLightHammer() RPGItemType {
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Bludgeoning
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1006,
"Handaxe",
"Choppy, choppy",
'¶',
@ -154,7 +186,9 @@ func ItemTypeMace() RPGItemType {
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Bludgeoning
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1007,
"Mace",
"Smashey, smashey",
'i',
@ -172,7 +206,9 @@ func ItemTypeQuarterstaff() RPGItemType {
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Bludgeoning
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1008,
"Quarterstaff",
"Whacky, whacky",
'|',
@ -189,7 +225,9 @@ func ItemTypeSickle() RPGItemType {
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return RollD6(1), DamageType_Physical_Slashing
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1009,
"Sickle",
"Slicey, slicey?",
'?',
@ -206,7 +244,9 @@ func ItemTypeSpear() RPGItemType {
damageRollFunc: func(victim, attacker RPGEntity) (damage int, dmgType DamageType) {
return RollD8(1), DamageType_Physical_Piercing
},
metaTypes: []RPGItemMetaType{MetaItemType_Physical_Weapon, MetaItemType_Weapon},
BasicItemType: item.CreateBasicItemType(
1010,
"Spear",
"Pokey, pokey",
'Î',

View file

@ -160,9 +160,9 @@ func StatValue(entity RPGEntity, stat Stat) int {
}
// Base Max Health is determined from constitution:
// Constitution + Max Health Bonus + 10
// 5*Constitution + Max Health Bonus
func BaseMaxHealth(entity RPGEntity) int {
return StatValue(entity, Stat_Attributes_Constitution) + StatValue(entity, Stat_MaxHealthBonus) + 10
return 5*StatValue(entity, Stat_Attributes_Constitution) + StatValue(entity, Stat_MaxHealthBonus)
}
// Dexterity + Evasion bonus + luck roll
@ -191,18 +191,6 @@ func PhysicalHitRoll(attacker RPGEntity, victim RPGEntity) bool {
}
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
}

View file

@ -2,40 +2,38 @@ package state
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2"
)
type DialogState struct {
inputSystem *input.InputSystem
turnSystem *turns.TurnSystem
prevState GameState
dialog *ui.UIDialog
selectDialog bool
returnToPreviousState bool
}
func CreateDialogState(dialog *ui.UIDialog, prevState GameState) *DialogState {
func CreateDialogState(inputSystem *input.InputSystem, turnSystem *turns.TurnSystem, dialog *ui.UIDialog, prevState GameState) *DialogState {
return &DialogState{
inputSystem: inputSystem,
turnSystem: turnSystem,
prevState: prevState,
dialog: dialog,
returnToPreviousState: false,
}
}
func (ds *DialogState) OnInput(e *tcell.EventKey) {
if e.Key() == tcell.KeyEnter {
ds.selectDialog = true
return
}
ds.dialog.Input(e)
func (s *DialogState) InputContext() input.Context {
return input.InputContext_Menu
}
func (ds *DialogState) OnTick(dt int64) GameState {
if ds.selectDialog {
ds.selectDialog = false
if ds.inputSystem.NextAction() == input.InputAction_Menu_Select {
ds.returnToPreviousState = true
ds.dialog.Select()
}

View file

@ -2,20 +2,11 @@ package state
import (
"mvvasilev/last_light/engine"
"github.com/gdamore/tcell/v2"
"mvvasilev/last_light/game/input"
)
type GameState interface {
OnInput(e *tcell.EventKey)
InputContext() input.Context
OnTick(dt int64) GameState
CollectDrawables() []engine.Drawable
}
type PausableState interface {
Pause()
Unpause()
SetPaused(paused bool)
GameState
}

View file

@ -2,29 +2,32 @@ package state
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/player"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui/menu"
"github.com/gdamore/tcell/v2"
)
type InventoryScreenState struct {
prevState PausableState
inputSystem *input.InputSystem
turnSystem *turns.TurnSystem
prevState GameState
exitMenu bool
inventoryMenu *menu.PlayerInventoryMenu
selectedInventorySlot engine.Position
player *player.Player
moveInventorySlotDirection model.Direction
dropSelectedInventorySlot bool
}
func CreateInventoryScreenState(player *player.Player, prevState PausableState) *InventoryScreenState {
func CreateInventoryScreenState(inputSystem *input.InputSystem, turnSystem *turns.TurnSystem, player *player.Player, prevState GameState) *InventoryScreenState {
iss := new(InventoryScreenState)
iss.inputSystem = inputSystem
iss.turnSystem = turnSystem
iss.prevState = prevState
iss.player = player
iss.selectedInventorySlot = engine.PositionAt(0, 0)
@ -34,76 +37,50 @@ func CreateInventoryScreenState(player *player.Player, prevState PausableState)
return iss
}
func (iss *InventoryScreenState) OnInput(e *tcell.EventKey) {
if e.Key() == tcell.KeyEsc || (e.Key() == tcell.KeyRune && e.Rune() == 'i') {
iss.exitMenu = true
}
if e.Key() == tcell.KeyRune && e.Rune() == 'x' {
iss.dropSelectedInventorySlot = true
}
if e.Key() != tcell.KeyRune {
return
}
switch e.Rune() {
case 'k':
iss.moveInventorySlotDirection = model.DirectionUp
case 'j':
iss.moveInventorySlotDirection = model.DirectionDown
case 'h':
iss.moveInventorySlotDirection = model.DirectionLeft
case 'l':
iss.moveInventorySlotDirection = model.DirectionRight
}
func (s *InventoryScreenState) InputContext() input.Context {
return input.InputContext_Inventory
}
func (iss *InventoryScreenState) OnTick(dt int64) GameState {
if iss.exitMenu {
iss.prevState.Unpause()
return iss.prevState
}
func (iss *InventoryScreenState) OnTick(dt int64) (nextState GameState) {
nextAction := iss.inputSystem.NextAction()
nextState = iss
if iss.dropSelectedInventorySlot {
switch nextAction {
case input.InputAction_Menu_Exit:
nextState = iss.prevState
case input.InputAction_DropItem:
iss.player.Inventory().Drop(iss.selectedInventorySlot.XY())
iss.dropSelectedInventorySlot = false
}
if iss.moveInventorySlotDirection != model.DirectionNone {
switch iss.moveInventorySlotDirection {
case model.DirectionUp:
if iss.selectedInventorySlot.Y() == 0 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, -1)
case model.DirectionDown:
if iss.selectedInventorySlot.Y() == iss.player.Inventory().Shape().Height()-1 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, +1)
case model.DirectionLeft:
if iss.selectedInventorySlot.X() == 0 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(-1, 0)
case model.DirectionRight:
if iss.selectedInventorySlot.X() == iss.player.Inventory().Shape().Width()-1 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(+1, 0)
case input.InputAction_Menu_HighlightUp:
if iss.selectedInventorySlot.Y() == 0 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, -1)
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY())
case input.InputAction_Menu_HighlightDown:
if iss.selectedInventorySlot.Y() == iss.player.Inventory().Shape().Height()-1 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, +1)
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY())
case input.InputAction_Menu_HighlightLeft:
if iss.selectedInventorySlot.X() == 0 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(-1, 0)
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY())
case input.InputAction_Menu_HighlightRight:
if iss.selectedInventorySlot.X() == iss.player.Inventory().Shape().Width()-1 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(+1, 0)
iss.inventoryMenu.SelectSlot(iss.selectedInventorySlot.XY())
iss.moveInventorySlotDirection = model.DirectionNone
}
return iss
return
}
func (iss *InventoryScreenState) CollectDrawables() []engine.Drawable {

View file

@ -2,12 +2,17 @@ package state
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2"
)
type MainMenuState struct {
turnSystem *turns.TurnSystem
inputSystem *input.InputSystem
menuTitle *engine.Raw
buttons []*ui.UISimpleButton
currButtonSelected int
@ -16,11 +21,16 @@ type MainMenuState struct {
startNewGame bool
}
func NewMainMenuState() *MainMenuState {
func CreateMainMenuState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *MainMenuState {
turnSystem.Clear()
state := new(MainMenuState)
highlightStyle := tcell.StyleDefault.Attributes(tcell.AttrBold)
state.turnSystem = turnSystem
state.inputSystem = inputSystem
state.menuTitle = engine.CreateRawDrawable(
11, 1, tcell.StyleDefault.Attributes(tcell.AttrBold).Foreground(tcell.ColorYellow),
" | | | _) | | ",
@ -46,31 +56,35 @@ func NewMainMenuState() *MainMenuState {
return state
}
func (mms *MainMenuState) OnInput(e *tcell.EventKey) {
if e.Key() == tcell.KeyDown {
func (s *MainMenuState) InputContext() input.Context {
return input.InputContext_Menu
}
func (mms *MainMenuState) OnTick(dt int64) GameState {
nextAction := mms.inputSystem.NextAction()
if nextAction == input.InputAction_Menu_HighlightDown {
mms.buttons[mms.currButtonSelected].Unhighlight()
mms.currButtonSelected = engine.LimitIncrement(mms.currButtonSelected, 2)
mms.buttons[mms.currButtonSelected].Highlight()
}
if e.Key() == tcell.KeyUp {
if nextAction == input.InputAction_Menu_HighlightUp {
mms.buttons[mms.currButtonSelected].Unhighlight()
mms.currButtonSelected = engine.LimitDecrement(mms.currButtonSelected, 0)
mms.buttons[mms.currButtonSelected].Highlight()
}
if e.Key() == tcell.KeyEnter {
if nextAction == input.InputAction_Menu_Select {
mms.buttons[mms.currButtonSelected].Select()
}
}
func (mms *MainMenuState) OnTick(dt int64) GameState {
if mms.quitGame {
return &QuitState{}
}
if mms.startNewGame {
return BeginPlayingState()
return CreatePlayingState(mms.turnSystem, mms.inputSystem)
}
return mms

View file

@ -2,13 +2,18 @@ package state
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2"
)
type PauseGameState struct {
prevState PausableState
turnSystem *turns.TurnSystem
inputSystem *input.InputSystem
prevState GameState
unpauseGame bool
returnToMainMenu bool
@ -18,9 +23,11 @@ type PauseGameState struct {
currButtonSelected int
}
func PauseGame(prevState PausableState) *PauseGameState {
func PauseGame(prevState GameState, turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *PauseGameState {
s := new(PauseGameState)
s.turnSystem = turnSystem
s.inputSystem = inputSystem
s.prevState = prevState
highlightStyle := tcell.StyleDefault.Attributes(tcell.AttrBold)
@ -60,36 +67,32 @@ func PauseGame(prevState PausableState) *PauseGameState {
return s
}
func (pg *PauseGameState) OnInput(e *tcell.EventKey) {
if e.Key() == tcell.KeyEsc {
pg.unpauseGame = true
}
if e.Key() == tcell.KeyDown {
pg.buttons[pg.currButtonSelected].Unhighlight()
pg.currButtonSelected = engine.LimitIncrement(pg.currButtonSelected, 1)
pg.buttons[pg.currButtonSelected].Highlight()
}
if e.Key() == tcell.KeyUp {
pg.buttons[pg.currButtonSelected].Unhighlight()
pg.currButtonSelected = engine.LimitDecrement(pg.currButtonSelected, 0)
pg.buttons[pg.currButtonSelected].Highlight()
}
if e.Key() == tcell.KeyEnter {
pg.buttons[pg.currButtonSelected].Select()
}
func (s *PauseGameState) InputContext() input.Context {
return input.InputContext_Menu
}
func (pg *PauseGameState) OnTick(dt int64) GameState {
switch pg.inputSystem.NextAction() {
case input.InputAction_Menu_Exit:
pg.unpauseGame = true
case input.InputAction_Menu_HighlightDown:
pg.buttons[pg.currButtonSelected].Unhighlight()
pg.currButtonSelected = engine.LimitIncrement(pg.currButtonSelected, 1)
pg.buttons[pg.currButtonSelected].Highlight()
case input.InputAction_Menu_HighlightUp:
pg.buttons[pg.currButtonSelected].Unhighlight()
pg.currButtonSelected = engine.LimitDecrement(pg.currButtonSelected, 0)
pg.buttons[pg.currButtonSelected].Highlight()
case input.InputAction_Menu_Select:
pg.buttons[pg.currButtonSelected].Select()
}
if pg.unpauseGame {
pg.prevState.Unpause()
return pg.prevState
}
if pg.returnToMainMenu {
return NewMainMenuState()
return CreateMainMenuState(pg.turnSystem, pg.inputSystem)
}
return pg

View file

@ -2,8 +2,11 @@ package state
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/npc"
"mvvasilev/last_light/game/player"
"mvvasilev/last_light/game/rpg"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui"
"mvvasilev/last_light/game/world"
@ -12,33 +15,88 @@ import (
)
type PlayingState struct {
turnSystem *turns.TurnSystem
inputSystem *input.InputSystem
player *player.Player
someNPC *model.BasicNPC
someNPC *npc.BasicNPC
eventLog *engine.GameEventLog
uiEventLog *ui.UIEventLog
healthBar *ui.UIHealthBar
dungeon *world.Dungeon
viewport *engine.Viewport
movePlayerDirection model.Direction
pauseGame bool
openInventory bool
pickUpUnderPlayer bool
interact bool
moveEntities bool
viewShortLogs bool
nextGameState GameState
}
func BeginPlayingState() *PlayingState {
func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *PlayingState {
turnSystem.Clear()
s := new(PlayingState)
s.turnSystem = turnSystem
s.inputSystem = inputSystem
mapSize := engine.SizeOf(128, 128)
s.dungeon = world.CreateDungeon(mapSize.Width(), mapSize.Height(), 1)
s.player = player.CreatePlayer(s.dungeon.CurrentLevel().PlayerSpawnPoint().XY())
s.player.Heal(rpg.BaseMaxHealth(s.player))
s.someNPC = model.CreateNPC(s.dungeon.CurrentLevel().NextLevelStaircase())
s.turnSystem.Schedule(10, func() (complete bool, requeue bool) {
requeue = true
complete = false
switch inputSystem.NextAction() {
case input.InputAction_PauseGame:
s.nextGameState = PauseGame(s, s.turnSystem, s.inputSystem)
case input.InputAction_OpenInventory:
s.nextGameState = CreateInventoryScreenState(s.inputSystem, s.turnSystem, s.player, s)
case input.InputAction_PickUpItem:
s.PickUpItemUnderPlayer()
complete = true
case input.InputAction_Interact:
s.InteractBelowPlayer()
complete = true
case input.InputAction_OpenLogs:
s.viewShortLogs = !s.viewShortLogs
case input.InputAction_MovePlayer_East:
s.MovePlayer(npc.East)
complete = true
case input.InputAction_MovePlayer_West:
s.MovePlayer(npc.West)
complete = true
case input.InputAction_MovePlayer_North:
s.MovePlayer(npc.North)
complete = true
case input.InputAction_MovePlayer_South:
s.MovePlayer(npc.South)
complete = true
default:
}
return
})
s.someNPC = npc.CreateNPC(s.dungeon.CurrentLevel().NextLevelStaircase())
s.turnSystem.Schedule(20, func() (complete bool, requeue bool) {
s.CalcPathToPlayerAndMove()
return true, true
})
s.eventLog = engine.CreateGameEventLog(100)
s.uiEventLog = ui.CreateUIEventLog(0, 17, 80, 7, s.eventLog, tcell.StyleDefault)
s.healthBar = ui.CreateHealthBar(68, 0, 12, 3, s.player.CurrentHealth(), rpg.BaseMaxHealth(s.player), tcell.StyleDefault)
s.dungeon.CurrentLevel().AddEntity(s.player, '@', tcell.StyleDefault)
s.dungeon.CurrentLevel().AddEntity(s.someNPC, 'N', tcell.StyleDefault)
@ -55,32 +113,24 @@ func BeginPlayingState() *PlayingState {
return s
}
func (ps *PlayingState) Pause() {
ps.pauseGame = true
func (s *PlayingState) InputContext() input.Context {
return input.InputContext_Play
}
func (ps *PlayingState) Unpause() {
ps.pauseGame = false
}
func (ps *PlayingState) SetPaused(paused bool) {
ps.pauseGame = paused
}
func (ps *PlayingState) MovePlayer() {
if ps.movePlayerDirection == model.DirectionNone {
func (ps *PlayingState) MovePlayer(direction npc.Direction) {
if direction == npc.DirectionNone {
return
}
newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(ps.movePlayerDirection))
newPlayerPos := ps.player.Position().WithOffset(npc.MovementDirectionOffset(direction))
if ps.dungeon.CurrentLevel().IsTilePassable(newPlayerPos.XY()) {
dx, dy := model.MovementDirectionOffset(ps.movePlayerDirection)
dx, dy := npc.MovementDirectionOffset(direction)
ps.dungeon.CurrentLevel().MoveEntity(ps.player.UniqueId(), dx, dy)
ps.viewport.SetCenter(ps.player.Position())
}
ps.movePlayerDirection = model.DirectionNone
ps.eventLog.Log("You moved " + npc.DirectionName(direction))
}
func (ps *PlayingState) InteractBelowPlayer() {
@ -100,6 +150,8 @@ func (ps *PlayingState) InteractBelowPlayer() {
func (ps *PlayingState) SwitchToNextLevel() {
if !ps.dungeon.HasNextLevel() {
ps.nextGameState = CreateDialogState(
ps.inputSystem,
ps.turnSystem,
ui.CreateOkDialog(
"The Unknown Depths",
"The staircases descent down to the lower levels is seemingly blocked by multiple large boulders. They appear immovable.",
@ -134,6 +186,8 @@ func (ps *PlayingState) SwitchToNextLevel() {
func (ps *PlayingState) SwitchToPreviousLevel() {
if !ps.dungeon.HasPreviousLevel() {
ps.nextGameState = CreateDialogState(
ps.inputSystem,
ps.turnSystem,
ui.CreateOkDialog(
"The Surface",
"You feel the gentle, yet chilling breeze of the surface make its way through the weaving cavern tunnels, the very same you had to make your way through to get where you are. There is nothing above that you need. Find the last light, or die trying.",
@ -177,13 +231,59 @@ func (ps *PlayingState) PickUpItemUnderPlayer() {
if !success {
ps.dungeon.CurrentLevel().SetItemAt(pos.X(), pos.Y(), item)
return
}
itemName, _ := item.Name()
ps.eventLog.Log("You picked up " + itemName)
}
func (ps *PlayingState) HasLineOfSight(start, end engine.Position) bool {
positions := engine.CastRay(start, end)
for _, p := range positions {
if ps.dungeon.CurrentLevel().IsGroundTileOpaque(p.XY()) {
return false
}
}
return true
}
func (ps *PlayingState) CalcPathToPlayerAndMove() {
distanceToPlayer := ps.someNPC.Position().Distance(ps.player.Position())
playerVisibleAndInRange := false
if ps.someNPC.Position().Distance(ps.player.Position()) < 20 && ps.HasLineOfSight(ps.someNPC.Position(), ps.player.Position()) {
playerVisibleAndInRange = true
}
if !playerVisibleAndInRange {
randomMove := npc.Direction(engine.RandInt(int(npc.DirectionNone), int(npc.East)))
nextPos := ps.someNPC.Position()
switch randomMove {
case npc.North:
nextPos = nextPos.WithOffset(0, -1)
case npc.South:
nextPos = nextPos.WithOffset(0, +1)
case npc.West:
nextPos = nextPos.WithOffset(-1, 0)
case npc.East:
nextPos = nextPos.WithOffset(+1, 0)
default:
return
}
if ps.dungeon.CurrentLevel().IsTilePassable(nextPos.XY()) {
ps.dungeon.CurrentLevel().MoveEntityTo(
ps.someNPC.UniqueId(),
nextPos.X(),
nextPos.Y(),
)
}
if distanceToPlayer > 20 {
return
}
@ -212,81 +312,16 @@ func (ps *PlayingState) CalcPathToPlayerAndMove() {
ps.dungeon.CurrentLevel().MoveEntityTo(ps.someNPC.UniqueId(), nextPos.X(), nextPos.Y())
}
func (ps *PlayingState) OnInput(e *tcell.EventKey) {
ps.player.Input(e)
func (ps *PlayingState) OnTick(dt int64) (nextState GameState) {
ps.nextGameState = ps
if e.Key() == tcell.KeyEsc {
ps.pauseGame = true
return
}
if e.Key() == tcell.KeyRune && e.Rune() == 'i' {
ps.openInventory = true
return
}
if e.Key() == tcell.KeyRune && e.Rune() == 'p' {
ps.pickUpUnderPlayer = true
return
}
if e.Key() == tcell.KeyRune && e.Rune() == 'e' {
ps.interact = true
return
}
switch e.Key() {
case tcell.KeyUp:
ps.movePlayerDirection = model.DirectionUp
ps.moveEntities = true
case tcell.KeyDown:
ps.movePlayerDirection = model.DirectionDown
ps.moveEntities = true
case tcell.KeyLeft:
ps.movePlayerDirection = model.DirectionLeft
ps.moveEntities = true
case tcell.KeyRight:
ps.movePlayerDirection = model.DirectionRight
ps.moveEntities = true
}
}
func (ps *PlayingState) OnTick(dt int64) GameState {
ps.player.Tick(dt)
if ps.pauseGame {
return PauseGame(ps)
}
if ps.openInventory {
ps.openInventory = false
return CreateInventoryScreenState(ps.player, ps)
}
if ps.movePlayerDirection != model.DirectionNone {
ps.MovePlayer()
}
if ps.pickUpUnderPlayer {
ps.pickUpUnderPlayer = false
ps.PickUpItemUnderPlayer()
}
if ps.interact {
ps.interact = false
ps.InteractBelowPlayer()
}
if ps.moveEntities {
ps.moveEntities = false
ps.CalcPathToPlayerAndMove()
}
ps.turnSystem.NextTurn()
return ps.nextGameState
}
func (ps *PlayingState) CollectDrawables() []engine.Drawable {
return engine.Multidraw(engine.CreateDrawingInstructions(func(v views.View) {
mainCameraDrawingInstructions := engine.CreateDrawingInstructions(func(v views.View) {
visibilityMap := engine.ComputeFOV(
func(x, y int) world.Tile {
ps.dungeon.CurrentLevel().Flatten().MarkExplored(x, y)
@ -314,5 +349,17 @@ func (ps *PlayingState) CollectDrawables() []engine.Drawable {
return ' ', tcell.StyleDefault
})
}))
})
drawables := []engine.Drawable{}
drawables = append(drawables, mainCameraDrawingInstructions)
if ps.viewShortLogs {
drawables = append(drawables, ps.uiEventLog)
}
drawables = append(drawables, ps.healthBar)
return drawables
}

View file

@ -2,15 +2,14 @@ package state
import (
"mvvasilev/last_light/engine"
"github.com/gdamore/tcell/v2"
"mvvasilev/last_light/game/input"
)
type QuitState struct {
}
func (q *QuitState) OnInput(e *tcell.EventKey) {
func (s *QuitState) InputContext() input.Context {
return input.InputContext_Menu
}
func (q *QuitState) OnTick(dt int64) GameState {

59
game/turns/turn_system.go Normal file
View file

@ -0,0 +1,59 @@
package turns
import "mvvasilev/last_light/engine"
type turn struct {
cost int
action func() (complete, requeue bool)
}
type TurnSystem struct {
turnQueue *engine.PriorityQueue[*turn]
paused bool
}
func CreateTurnSystem() *TurnSystem {
return &TurnSystem{
turnQueue: engine.CreatePriorityQueue[*turn](),
paused: false,
}
}
func (ts *TurnSystem) NextTurn() {
if ts.paused {
return
}
turnCost, turn := ts.turnQueue.Peek()
if turn == nil {
return
}
complete, requeue := turn.action()
// If the action isn't complete, we'll re-do it again next time
if !complete {
return
}
ts.turnQueue.Dequeue()
ts.turnQueue.AdjustPriorities(-turnCost)
if requeue {
ts.turnQueue.Enqueue(turn.cost, turn)
}
}
func (ts *TurnSystem) Schedule(cost int, action func() (complete, requeue bool)) {
ts.turnQueue.Enqueue(cost, &turn{
cost: cost,
action: action,
})
}
func (ts *TurnSystem) Clear() {
ts.turnQueue.Clear()
}

View file

@ -1,82 +0,0 @@
package ui
import (
"mvvasilev/last_light/engine"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
type UIContainerLayout int
const (
// These change the provided ui positions
UpperLeft UIContainerLayout = iota
MiddleLeft
LowerLeft
UpperRight
MiddleRight
LowerRight
UpperCenter
MiddleCenter
LowerCenter
// This uses the positions as provided in the ui elements
Manual
)
type UIContainer struct {
id uuid.UUID
layout UIContainerLayout
position engine.Position
size engine.Size
elements []UIElement
}
func CreateUIContainer(x, y int, width, height int, layout UIContainerLayout) *UIContainer {
container := new(UIContainer)
container.id = uuid.New()
container.layout = layout
container.position = engine.PositionAt(x, y)
container.size = engine.SizeOf(width, height)
container.elements = make([]UIElement, 0)
return container
}
func (uic *UIContainer) Push(element UIElement) {
uic.elements = append(uic.elements, element)
}
func (uic *UIContainer) Clear() {
uic.elements = make([]UIElement, 0)
}
func (uic *UIContainer) UniqueId() uuid.UUID {
return uic.id
}
func (uic *UIContainer) MoveTo(x, y int) {
uic.position = engine.PositionAt(x, y)
}
func (uic *UIContainer) Position() engine.Position {
return uic.position
}
func (uic *UIContainer) Size() engine.Size {
return uic.size
}
func (uic *UIContainer) Draw(v views.View) {
for _, e := range uic.elements {
e.Draw(v)
}
}
func (uic *UIContainer) Input(ev *tcell.EventKey) {
for _, e := range uic.elements {
e.Input(ev)
}
}

View file

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

62
game/ui/event_logger.go Normal file
View file

@ -0,0 +1,62 @@
package ui
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
type UIEventLog struct {
id uuid.UUID
eventLogger *engine.GameEventLog
window *UIWindow
style tcell.Style
}
func CreateUIEventLog(x, y, width, height int, eventLogger *engine.GameEventLog, style tcell.Style) *UIEventLog {
return &UIEventLog{
id: uuid.New(),
eventLogger: eventLogger,
window: CreateWindow(x, y, width, height, "", style),
style: style,
}
}
func (uie *UIEventLog) MoveTo(x int, y int) {
uie.window.MoveTo(x, y)
}
func (uie *UIEventLog) Position() engine.Position {
return uie.window.Position()
}
func (uie *UIEventLog) Size() engine.Size {
return uie.window.Size()
}
func (uie *UIEventLog) Input(inputAction input.InputAction) {
}
func (uie *UIEventLog) UniqueId() uuid.UUID {
return uie.id
}
func (uie *UIEventLog) Draw(v views.View) {
uie.window.Draw(v)
x, y := uie.Position().XY()
height := uie.Size().Height()
textHeight := height - 2
for i, ge := range uie.eventLogger.Tail(textHeight) {
engine.DrawText(x+1, y+i+1, ge.Contents(), uie.style, v)
}
}

104
game/ui/health_bar.go Normal file
View file

@ -0,0 +1,104 @@
package ui
import (
"fmt"
"math"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
type UIHealthBar struct {
id uuid.UUID
health int
maxHealth int
window *UIWindow
style tcell.Style
}
// TODO: style for health bar fill
// TODO: 'HP' title
// TODO: test different percentages
func CreateHealthBar(x, y, w, h, health, maxHealth int, style tcell.Style) *UIHealthBar {
return &UIHealthBar{
window: CreateWindow(x, y, w, h, "HP", style),
health: health,
maxHealth: maxHealth,
style: style,
}
}
func (uihp *UIHealthBar) SetHealth(health int) {
uihp.health = health
}
func (uihp *UIHealthBar) SetMaxHealth(maxHealth int) {
uihp.maxHealth = maxHealth
}
func (uihp *UIHealthBar) MoveTo(x int, y int) {
uihp.window.MoveTo(x, y)
}
func (uihp *UIHealthBar) Position() engine.Position {
return uihp.window.Position()
}
func (uihp *UIHealthBar) Size() engine.Size {
return uihp.window.Size()
}
func (uihp *UIHealthBar) Input(inputAction input.InputAction) {
}
func (uihp *UIHealthBar) UniqueId() uuid.UUID {
return uihp.id
}
func (uihp *UIHealthBar) Draw(v views.View) {
x, y, w, h := uihp.Position().X(), uihp.Position().Y(), uihp.Size().Width(), uihp.Size().Height()
uihp.window.Draw(v)
stages := []rune{'█', '▓', '▒', '░'} // 0 = 1.0, 1 = 0.75, 2 = 0.5, 3 = 0.25
percentage := (float64(w) - 2.0) * (float64(uihp.health) / float64(uihp.maxHealth))
whole := math.Trunc(percentage)
last := percentage - whole
hpStyle := tcell.StyleDefault.Foreground(tcell.ColorIndianRed)
for i := range int(whole) {
v.SetContent(x+1+i, y+1, stages[0], nil, hpStyle)
}
if last > 0.0 {
if last <= 0.25 {
v.SetContent(x+1+int(whole), y+1, stages[3], nil, hpStyle)
}
if last <= 0.50 {
v.SetContent(x+1+int(whole), y+1, stages[2], nil, hpStyle)
}
if last <= 0.75 {
v.SetContent(x+1+int(whole), y+1, stages[1], nil, hpStyle)
}
}
hpText := fmt.Sprintf("%v/%v", uihp.health, uihp.maxHealth)
engine.DrawText(
x+w/2-len(hpText)/2,
y+h-1,
hpText,
hpStyle,
v,
)
}

166
game/ui/item.go Normal file
View file

@ -0,0 +1,166 @@
package ui
import (
"fmt"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/rpg"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
)
type UIBasicItem struct {
id uuid.UUID
item item.Item
window UIWindow
itemName UILabel
engine.Positioned
engine.Sized
}
func CreateUIBasicItem(x, y int, item item.Item, style tcell.Style) *UIBasicItem {
name, nameStyle := item.Name()
return &UIBasicItem{
id: uuid.New(),
item: item,
window: *CreateWindow(x, y, 33, 8, "Item", style),
itemName: *CreateSingleLineUILabel(x+1, y+1, name, nameStyle),
Positioned: engine.WithPosition(engine.PositionAt(x, y)),
Sized: engine.WithSize(engine.SizeOf(33, 8)),
}
}
func (uibi *UIBasicItem) Input(e *tcell.EventKey) {
}
func (uibi *UIBasicItem) UniqueId() uuid.UUID {
return uibi.id
}
func (uibi *UIBasicItem) Draw(v views.View) {
uibi.window.Draw(v)
uibi.itemName.Draw(v)
}
type UIRPGItem struct {
id uuid.UUID
item rpg.RPGItem
window UIWindow
itemName UILabel
engine.Positioned
engine.Sized
}
func CreateUIRPGItem(x, y int, item rpg.RPGItem, style tcell.Style) *UIRPGItem {
name, nameStyle := item.Name()
return &UIRPGItem{
id: uuid.New(),
item: item,
window: *CreateWindow(x, y, 33, 8, "Item", style),
itemName: *CreateSingleLineUILabel(x+1, y+1, name, nameStyle),
Positioned: engine.WithPosition(engine.PositionAt(x, y)),
Sized: engine.WithSize(engine.SizeOf(33, 8)),
}
}
func (uiri *UIRPGItem) Input(inputAction input.InputAction) {
}
func (uiri *UIRPGItem) UniqueId() uuid.UUID {
return uiri.id
}
func (uiri *UIRPGItem) Draw(v views.View) {
uiri.window.Draw(v)
uiri.itemName.Draw(v)
statModifiers := uiri.item.Modifiers()
x, y := uiri.itemName.Position().XY()
y++
for i, sm := range statModifiers {
drawRPGItemStatModifier(x, y, tcell.StyleDefault, v, &sm)
x += 9 + 2 // each stat is 9 characters long, with 2 characters separating the stats
// Only 3 stats per line
if i > 0 && (i+1)%3 == 0 {
x = uiri.itemName.Position().X()
y++
}
}
}
func drawRPGItemStatModifier(x, y int, style tcell.Style, view views.View, sm *rpg.StatModifier) {
// 5 characters per stat name
// 1 separating character
// 3 characters for bonus ( including sign, modifiers are limited to -99 and +99)
const SEPARATING_CHARACTER rune = ':'
switch sm.Stat {
case rpg.Stat_Attributes_Strength:
engine.DrawText(x, y, "STR", style, view)
case rpg.Stat_Attributes_Dexterity:
engine.DrawText(x, y, "DEX", style, view)
case rpg.Stat_Attributes_Intelligence:
engine.DrawText(x, y, "INT", style, view)
case rpg.Stat_Attributes_Constitution:
engine.DrawText(x, y, "CON", style, view)
case rpg.Stat_PhysicalPrecisionBonus:
engine.DrawText(x, y, "pPrcs", style, view)
case rpg.Stat_EvasionBonus:
engine.DrawText(x, y, "Evasn", style, view)
case rpg.Stat_MagicPrecisionBonus:
engine.DrawText(x, y, "mPrcs", style, view)
case rpg.Stat_TotalPrecisionBonus:
engine.DrawText(x, y, "tPrcs", style, view)
case rpg.Stat_DamageBonus_Physical_Unarmed:
engine.DrawText(x, y, "Unrmd", style, view)
case rpg.Stat_DamageBonus_Physical_Slashing:
engine.DrawText(x, y, "Slshn", style, view)
case rpg.Stat_DamageBonus_Physical_Piercing:
engine.DrawText(x, y, "Prcng", style, view)
case rpg.Stat_DamageBonus_Physical_Bludgeoning:
engine.DrawText(x, y, "Bldgn", style, view)
case rpg.Stat_DamageBonus_Magic_Fire:
engine.DrawText(x, y, "Fire", style, view)
case rpg.Stat_DamageBonus_Magic_Cold:
engine.DrawText(x, y, "Cold", style, view)
case rpg.Stat_DamageBonus_Magic_Necrotic:
engine.DrawText(x, y, "Ncrtc", style, view)
case rpg.Stat_DamageBonus_Magic_Thunder:
engine.DrawText(x, y, "Thndr", style, view)
case rpg.Stat_DamageBonus_Magic_Acid:
engine.DrawText(x, y, "Acid", style, view)
case rpg.Stat_DamageBonus_Magic_Poison:
engine.DrawText(x, y, "Poisn", style, view)
case rpg.Stat_MaxHealthBonus:
engine.DrawText(x, y, "maxHP", style, view)
default:
}
view.SetContent(x+5, y, SEPARATING_CHARACTER, nil, style)
if sm.Bonus < 0 {
engine.DrawText(x+6, y, fmt.Sprintf("-%02d", -sm.Bonus), tcell.StyleDefault.Foreground(tcell.ColorIndianRed), view)
} else {
engine.DrawText(x+6, y, fmt.Sprintf("+%02d", sm.Bonus), tcell.StyleDefault.Foreground(tcell.ColorLime), view)
}
}

View file

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

View file

@ -3,7 +3,9 @@ package menu
import (
"fmt"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/rpg"
"mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2"
@ -116,25 +118,18 @@ func CreatePlayerInventoryMenu(x, y int, playerInventory *item.EquippedInventory
})
menu.selectedItem = engine.CreateDrawingInstructions(func(v views.View) {
ui.CreateWindow(x+2, y+14, 33, 8, "ITEM", style).Draw(v)
item := playerInventory.ItemAt(menu.selectedInventorySlot.XY())
if item == nil {
return
}
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:
// }
switch it := item.(type) {
case rpg.RPGItem:
ui.CreateUIRPGItem(x+2, y+14, it, style).Draw(v)
default:
ui.CreateUIBasicItem(x+2, y+14, it, style).Draw(v)
}
})
menu.help = ui.CreateSingleLineUILabel(x+2, y+22, "hjkl - move, x - drop, e - equip", style)
@ -154,7 +149,7 @@ func (pim *PlayerInventoryMenu) Size() engine.Size {
return pim.inventoryMenu.Size()
}
func (pim *PlayerInventoryMenu) Input(e *tcell.EventKey) {
func (pim *PlayerInventoryMenu) Input(inputAction input.InputAction) {
}
@ -163,6 +158,10 @@ func (pim *PlayerInventoryMenu) UniqueId() uuid.UUID {
}
func (pim *PlayerInventoryMenu) SelectSlot(x, y int) {
if pim.selectedInventorySlot.X() == x && pim.selectedInventorySlot.Y() == y {
return
}
pim.inventoryGrid.Unhighlight()
pim.selectedInventorySlot = engine.PositionAt(x, y)
pim.inventoryGrid.Highlight(pim.selectedInventorySlot)

View file

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

View file

@ -2,15 +2,14 @@ package ui
import (
"mvvasilev/last_light/engine"
"github.com/gdamore/tcell/v2"
"mvvasilev/last_light/game/input"
)
type UIElement interface {
MoveTo(x, y int)
Position() engine.Position
Size() engine.Size
Input(e *tcell.EventKey)
Input(inputAction input.InputAction)
engine.Drawable
}

View file

@ -2,6 +2,7 @@ package ui
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"unicode/utf8"
"github.com/gdamore/tcell/v2"
@ -23,7 +24,9 @@ func CreateWindow(x, y, width, height int, title string, style tcell.Style) *UIW
titlePos := (width / 2) - int(titleLen/2)
w.title = engine.CreateText(x+titlePos, y, int(titleLen), 1, title, style)
if title != "" {
w.title = engine.CreateText(x+titlePos, y, int(titleLen), 1, title, style)
}
w.box = engine.CreateRectangle(
x, y, width, height,
@ -49,13 +52,16 @@ func (w *UIWindow) Position() engine.Position {
}
func (w *UIWindow) Size() engine.Size {
return engine.SizeOf(0, 0)
return w.box.Size()
}
func (w *UIWindow) Draw(v views.View) {
w.box.Draw(v)
w.title.Draw(v)
if w.title != nil {
w.title.Draw(v)
}
}
func (w *UIWindow) Input(e *tcell.EventKey) {
func (w *UIWindow) Input(inputAction input.InputAction) {
}

View file

@ -4,7 +4,7 @@ import (
"math/rand"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/npc"
"mvvasilev/last_light/game/rpg"
"slices"
@ -113,21 +113,21 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLeve
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(),
}
itemTypes := []rpg.RPGItemType{
rpg.ItemTypeBow(),
rpg.ItemTypeLongsword(),
rpg.ItemTypeClub(),
rpg.ItemTypeDagger(),
rpg.ItemTypeHandaxe(),
rpg.ItemTypeJavelin(),
rpg.ItemTypeLightHammer(),
rpg.ItemTypeMace(),
rpg.ItemTypeQuarterstaff(),
rpg.ItemTypeSickle(),
rpg.ItemTypeSpear(),
}
genTable.Add(1, func() item.Item {
itemType := itemTypes[rand.Intn(len(itemTypes))]
rarities := []rpg.ItemRarity{
@ -156,7 +156,7 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLeve
groundLevel = CreateBSPDungeonMap(width, height, 4)
}
items := SpawnItems(groundLevel.Rooms(), 0.02, genTable, []engine.Position{
items := SpawnItems(groundLevel.Rooms(), 0.1, genTable, []engine.Position{
groundLevel.NextLevelStaircasePosition(),
groundLevel.PlayerSpawnPoint(),
groundLevel.PreviousLevelStaircasePosition(),
@ -202,9 +202,9 @@ func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTa
numItems := rand.Intn(maxItems)
for range numItems {
itemType := genTable.Generate()
item := genTable.Generate()
if itemType == nil {
if item == nil {
continue
}
@ -217,7 +217,7 @@ func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTa
continue
}
itemTiles = append(itemTiles, CreateItemTile(pos, itemType))
itemTiles = append(itemTiles, CreateItemTile(pos, item))
}
}
@ -240,7 +240,7 @@ func (d *DungeonLevel) DropEntity(uuid uuid.UUID) {
d.entityLevel.DropEntity(uuid)
}
func (d *DungeonLevel) AddEntity(entity model.MovableEntity, presentation rune, style tcell.Style) {
func (d *DungeonLevel) AddEntity(entity npc.MovableEntity, presentation rune, style tcell.Style) {
d.entityLevel.AddEntity(entity, presentation, style)
}
@ -291,6 +291,14 @@ func (d *DungeonLevel) IsTilePassable(x, y int) bool {
return d.TileAt(x, y).Passable()
}
func (d *DungeonLevel) IsGroundTileOpaque(x, y int) bool {
if !d.groundLevel.Size().Contains(x, y) {
return false
}
return d.TileAt(x, y).Opaque()
}
func (d *DungeonLevel) Flatten() Map {
return d.multilevel
}

View file

@ -3,7 +3,7 @@ package world
import (
"maps"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/npc"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
@ -43,7 +43,7 @@ func (em *EntityMap) FindEntityByUuid(uuid uuid.UUID) (key int, entity EntityTil
return -1, nil
}
func (em *EntityMap) AddEntity(entity model.MovableEntity, presentation rune, style tcell.Style) {
func (em *EntityMap) AddEntity(entity npc.MovableEntity, presentation rune, style tcell.Style) {
if !em.FitsWithin(entity.Position().XY()) {
return
}

View file

@ -3,7 +3,7 @@ package world
import (
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/npc"
"github.com/gdamore/tcell/v2"
)
@ -231,18 +231,18 @@ func (it *ItemTile) Type() TileType {
}
type EntityTile interface {
Entity() model.MovableEntity
Entity() npc.MovableEntity
Tile
}
type BasicEntityTile struct {
entity model.MovableEntity
entity npc.MovableEntity
presentation rune
style tcell.Style
}
func CreateBasicEntityTile(entity model.MovableEntity, presentation rune, style tcell.Style) *BasicEntityTile {
func CreateBasicEntityTile(entity npc.MovableEntity, presentation rune, style tcell.Style) *BasicEntityTile {
return &BasicEntityTile{
entity: entity,
presentation: presentation,
@ -250,7 +250,7 @@ func CreateBasicEntityTile(entity model.MovableEntity, presentation rune, style
}
}
func (bet *BasicEntityTile) Entity() model.MovableEntity {
func (bet *BasicEntityTile) Entity() npc.MovableEntity {
return bet.entity
}