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 return rect.position
} }
func (rect Rectangle) Size() Size {
return rect.size
}
func (rect Rectangle) drawBorders(v views.View) { func (rect Rectangle) drawBorders(v views.View) {
width := rect.size.Width() width := rect.size.Width()
height := rect.size.Height() height := rect.size.Height()
@ -170,3 +174,43 @@ func (rect Rectangle) Draw(v views.View) {
rect.drawFill(v) 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 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 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 ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/state" "mvvasilev/last_light/game/state"
"mvvasilev/last_light/game/turns"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
type Game struct { type Game struct {
turnSystem *turns.TurnSystem
inputSystem *input.InputSystem
state state.GameState state state.GameState
quitGame bool quitGame bool
@ -16,7 +21,11 @@ type Game struct {
func CreateGame() *Game { func CreateGame() *Game {
game := new(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 return game
} }
@ -26,24 +35,22 @@ func (g *Game) Input(ev *tcell.EventKey) {
g.quitGame = true g.quitGame = true
} }
g.state.OnInput(ev) g.inputSystem.Input(g.state.InputContext(), ev)
} }
func (g *Game) Tick(dt int64) bool { func (g *Game) Tick(dt int64) (continueGame bool) {
if g.quitGame { continueGame = !g.quitGame
return false
}
s := g.state.OnTick(dt) s := g.state.OnTick(dt)
switch s.(type) { switch s.(type) {
case *state.QuitState: case *state.QuitState:
return false g.quitGame = true
} }
g.state = s g.state = s
return true return
} }
func (g *Game) CollectDrawables() []engine.Drawable { 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 // Try to first find a matching item with capacity
for index, existingItem := range inv.contents { 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() { if existingItem.Quantity()+1 > existingItem.Type().MaxStack() {
continue continue
} }

View file

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

View file

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

View file

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

View file

@ -24,7 +24,15 @@ func CreatePlayer(x, y int) *Player {
p.id = uuid.New() p.id = uuid.New()
p.position = engine.PositionAt(x, y) p.position = engine.PositionAt(x, y)
p.inventory = item.CreateEquippedInventory() 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 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" "math/rand"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item" "mvvasilev/last_light/game/item"
"slices"
"unicode"
"unicode/utf8"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/google/uuid" "github.com/google/uuid"
) )
// TODO: figure out event logging. Need to inform the player of the things that are happening...
const MaxNumberOfModifiers = 6
type ItemSupplier func() item.Item type ItemSupplier func() item.Item
type LootTable struct { 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) { func generateItemName(itemType RPGItemType, rarity ItemRarity) (string, tcell.Style) {
switch rarity { switch rarity {
case ItemRarity_Common: case ItemRarity_Common:
return itemType.Name(), tcell.StyleDefault return itemType.Name(), tcell.StyleDefault
case ItemRarity_Uncommon: case ItemRarity_Uncommon:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorLime) return randomAdjective() + " " + itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorLime)
case ItemRarity_Rare: case ItemRarity_Rare:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorBlue) return itemType.Name() + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorBlue)
case ItemRarity_Epic: case ItemRarity_Epic:
return itemType.Name(), tcell.StyleDefault.Foreground(tcell.ColorPurple) return randomAdjective() + " " + itemType.Name() + " " + randomSuffix(), tcell.StyleDefault.Foreground(tcell.ColorPurple)
case ItemRarity_Legendary: 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: default:
return itemType.Name(), tcell.StyleDefault return itemType.Name(), tcell.StyleDefault
} }
} }
func randomStat() Stat { func randomStat(metaItemTypes []RPGItemMetaType) Stat {
stats := []Stat{ stats := make(map[RPGItemMetaType][]Stat, 0)
stats[MetaItemType_Weapon] = []Stat{
Stat_Attributes_Strength, Stat_Attributes_Strength,
Stat_Attributes_Dexterity, Stat_Attributes_Dexterity,
Stat_Attributes_Intelligence, Stat_Attributes_Intelligence,
Stat_Attributes_Constitution, Stat_Attributes_Constitution,
Stat_PhysicalPrecisionBonus,
Stat_EvasionBonus,
Stat_MagicPrecisionBonus,
Stat_TotalPrecisionBonus, Stat_TotalPrecisionBonus,
Stat_DamageBonus_Physical_Unarmed, }
stats[MetaItemType_Physical_Weapon] = []Stat{
Stat_PhysicalPrecisionBonus,
Stat_DamageBonus_Physical_Slashing, Stat_DamageBonus_Physical_Slashing,
Stat_DamageBonus_Physical_Piercing, Stat_DamageBonus_Physical_Piercing,
Stat_DamageBonus_Physical_Bludgeoning, Stat_DamageBonus_Physical_Bludgeoning,
@ -95,37 +157,102 @@ func randomStat() Stat {
Stat_DamageBonus_Magic_Thunder, Stat_DamageBonus_Magic_Thunder,
Stat_DamageBonus_Magic_Acid, Stat_DamageBonus_Magic_Acid,
Stat_DamageBonus_Magic_Poison, 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, 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,
} }
func generateItemStatModifiers(rarity ItemRarity) []StatModifier { 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(itemType RPGItemType, rarity ItemRarity) []StatModifier {
points := pointPerRarity(rarity) points := pointPerRarity(rarity)
modifiers := []StatModifier{} modifiers := make(map[Stat]*StatModifier, 0)
for { 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 break
} }
modAmount := engine.RandInt(-points/2, points) // Random chance to increase or decrease a stat
modAmount := engine.RandInt(-points, points)
if modAmount == 0 { if modAmount == 0 {
continue continue
} }
modifiers = append(modifiers, StatModifier{ stat := randomStat(itemType.MetaTypes())
Id: StatModifierId(uuid.New().String()),
Stat: randomStat(),
Bonus: modAmount,
})
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
} }
return modifiers } 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)
}
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 // 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, name,
style, style,
itemType, itemType,
generateItemStatModifiers(rarity), generateItemStatModifiers(itemType, rarity),
) )
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,13 +2,18 @@ package state
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui" "mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
type PauseGameState struct { type PauseGameState struct {
prevState PausableState turnSystem *turns.TurnSystem
inputSystem *input.InputSystem
prevState GameState
unpauseGame bool unpauseGame bool
returnToMainMenu bool returnToMainMenu bool
@ -18,9 +23,11 @@ type PauseGameState struct {
currButtonSelected int currButtonSelected int
} }
func PauseGame(prevState PausableState) *PauseGameState { func PauseGame(prevState GameState, turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *PauseGameState {
s := new(PauseGameState) s := new(PauseGameState)
s.turnSystem = turnSystem
s.inputSystem = inputSystem
s.prevState = prevState s.prevState = prevState
highlightStyle := tcell.StyleDefault.Attributes(tcell.AttrBold) highlightStyle := tcell.StyleDefault.Attributes(tcell.AttrBold)
@ -60,36 +67,32 @@ func PauseGame(prevState PausableState) *PauseGameState {
return s return s
} }
func (pg *PauseGameState) OnInput(e *tcell.EventKey) { func (s *PauseGameState) InputContext() input.Context {
if e.Key() == tcell.KeyEsc { return input.InputContext_Menu
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 (pg *PauseGameState) OnTick(dt int64) GameState { 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 { if pg.unpauseGame {
pg.prevState.Unpause()
return pg.prevState return pg.prevState
} }
if pg.returnToMainMenu { if pg.returnToMainMenu {
return NewMainMenuState() return CreateMainMenuState(pg.turnSystem, pg.inputSystem)
} }
return pg return pg

View file

@ -2,8 +2,11 @@ package state
import ( import (
"mvvasilev/last_light/engine" "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/player"
"mvvasilev/last_light/game/rpg"
"mvvasilev/last_light/game/turns"
"mvvasilev/last_light/game/ui" "mvvasilev/last_light/game/ui"
"mvvasilev/last_light/game/world" "mvvasilev/last_light/game/world"
@ -12,33 +15,88 @@ import (
) )
type PlayingState struct { type PlayingState struct {
turnSystem *turns.TurnSystem
inputSystem *input.InputSystem
player *player.Player player *player.Player
someNPC *model.BasicNPC someNPC *npc.BasicNPC
eventLog *engine.GameEventLog
uiEventLog *ui.UIEventLog
healthBar *ui.UIHealthBar
dungeon *world.Dungeon dungeon *world.Dungeon
viewport *engine.Viewport viewport *engine.Viewport
movePlayerDirection model.Direction viewShortLogs bool
pauseGame bool
openInventory bool
pickUpUnderPlayer bool
interact bool
moveEntities bool
nextGameState GameState nextGameState GameState
} }
func BeginPlayingState() *PlayingState { func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *PlayingState {
turnSystem.Clear()
s := new(PlayingState) s := new(PlayingState)
s.turnSystem = turnSystem
s.inputSystem = inputSystem
mapSize := engine.SizeOf(128, 128) mapSize := engine.SizeOf(128, 128)
s.dungeon = world.CreateDungeon(mapSize.Width(), mapSize.Height(), 1) s.dungeon = world.CreateDungeon(mapSize.Width(), mapSize.Height(), 1)
s.player = player.CreatePlayer(s.dungeon.CurrentLevel().PlayerSpawnPoint().XY()) 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.player, '@', tcell.StyleDefault)
s.dungeon.CurrentLevel().AddEntity(s.someNPC, 'N', tcell.StyleDefault) s.dungeon.CurrentLevel().AddEntity(s.someNPC, 'N', tcell.StyleDefault)
@ -55,32 +113,24 @@ func BeginPlayingState() *PlayingState {
return s return s
} }
func (ps *PlayingState) Pause() { func (s *PlayingState) InputContext() input.Context {
ps.pauseGame = true return input.InputContext_Play
} }
func (ps *PlayingState) Unpause() { func (ps *PlayingState) MovePlayer(direction npc.Direction) {
ps.pauseGame = false if direction == npc.DirectionNone {
}
func (ps *PlayingState) SetPaused(paused bool) {
ps.pauseGame = paused
}
func (ps *PlayingState) MovePlayer() {
if ps.movePlayerDirection == model.DirectionNone {
return 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()) { 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.dungeon.CurrentLevel().MoveEntity(ps.player.UniqueId(), dx, dy)
ps.viewport.SetCenter(ps.player.Position()) ps.viewport.SetCenter(ps.player.Position())
} }
ps.movePlayerDirection = model.DirectionNone ps.eventLog.Log("You moved " + npc.DirectionName(direction))
} }
func (ps *PlayingState) InteractBelowPlayer() { func (ps *PlayingState) InteractBelowPlayer() {
@ -100,6 +150,8 @@ func (ps *PlayingState) InteractBelowPlayer() {
func (ps *PlayingState) SwitchToNextLevel() { func (ps *PlayingState) SwitchToNextLevel() {
if !ps.dungeon.HasNextLevel() { if !ps.dungeon.HasNextLevel() {
ps.nextGameState = CreateDialogState( ps.nextGameState = CreateDialogState(
ps.inputSystem,
ps.turnSystem,
ui.CreateOkDialog( ui.CreateOkDialog(
"The Unknown Depths", "The Unknown Depths",
"The staircases descent down to the lower levels is seemingly blocked by multiple large boulders. They appear immovable.", "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() { func (ps *PlayingState) SwitchToPreviousLevel() {
if !ps.dungeon.HasPreviousLevel() { if !ps.dungeon.HasPreviousLevel() {
ps.nextGameState = CreateDialogState( ps.nextGameState = CreateDialogState(
ps.inputSystem,
ps.turnSystem,
ui.CreateOkDialog( ui.CreateOkDialog(
"The Surface", "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.", "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 { if !success {
ps.dungeon.CurrentLevel().SetItemAt(pos.X(), pos.Y(), item) 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() { 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 return
} }
@ -212,81 +312,16 @@ func (ps *PlayingState) CalcPathToPlayerAndMove() {
ps.dungeon.CurrentLevel().MoveEntityTo(ps.someNPC.UniqueId(), nextPos.X(), nextPos.Y()) ps.dungeon.CurrentLevel().MoveEntityTo(ps.someNPC.UniqueId(), nextPos.X(), nextPos.Y())
} }
func (ps *PlayingState) OnInput(e *tcell.EventKey) { func (ps *PlayingState) OnTick(dt int64) (nextState GameState) {
ps.player.Input(e) ps.nextGameState = ps
if e.Key() == tcell.KeyEsc { ps.turnSystem.NextTurn()
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()
}
return ps.nextGameState return ps.nextGameState
} }
func (ps *PlayingState) CollectDrawables() []engine.Drawable { 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( visibilityMap := engine.ComputeFOV(
func(x, y int) world.Tile { func(x, y int) world.Tile {
ps.dungeon.CurrentLevel().Flatten().MarkExplored(x, y) ps.dungeon.CurrentLevel().Flatten().MarkExplored(x, y)
@ -314,5 +349,17 @@ func (ps *PlayingState) CollectDrawables() []engine.Drawable {
return ' ', tcell.StyleDefault 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 ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"github.com/gdamore/tcell/v2"
) )
type QuitState struct { 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 { 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 ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views" "github.com/gdamore/tcell/v2/views"
@ -90,13 +91,13 @@ func (d *UIDialog) Size() engine.Size {
return d.window.Size() return d.window.Size()
} }
func (d *UIDialog) Input(e *tcell.EventKey) { func (d *UIDialog) Input(inputAction input.InputAction) {
if e.Key() == tcell.KeyLeft { if inputAction == input.InputAction_Menu_HighlightLeft {
if !d.yesBtn.IsHighlighted() { if !d.yesBtn.IsHighlighted() {
d.noBtn.Unhighlight() d.noBtn.Unhighlight()
d.yesBtn.Highlight() d.yesBtn.Highlight()
} }
} else if e.Key() == tcell.KeyRight { } else if inputAction == input.InputAction_Menu_HighlightRight {
if d.noBtn == nil { if d.noBtn == nil {
return 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 ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"unicode/utf8" "unicode/utf8"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -52,4 +53,4 @@ func (t *UILabel) Draw(v views.View) {
t.text.Draw(v) 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 ( import (
"fmt" "fmt"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"mvvasilev/last_light/game/item" "mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/rpg"
"mvvasilev/last_light/game/ui" "mvvasilev/last_light/game/ui"
"github.com/gdamore/tcell/v2" "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) { 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()) item := playerInventory.ItemAt(menu.selectedInventorySlot.XY())
if item == nil { if item == nil {
return return
} }
name, nameStyle := item.Name() switch it := item.(type) {
case rpg.RPGItem:
ui.CreateSingleLineUILabel(x+3, y+15, name, nameStyle).Draw(v) ui.CreateUIRPGItem(x+2, y+14, it, style).Draw(v)
default:
// |Stt:+00|Stt:+00|Stt:+00|Stt:+00| ui.CreateUIBasicItem(x+2, y+14, it, style).Draw(v)
// switch it := item.(type) { }
// case rpg.RPGItem:
// //statModifiers := it.Modifiers()
// default:
// }
}) })
menu.help = ui.CreateSingleLineUILabel(x+2, y+22, "hjkl - move, x - drop, e - equip", style) menu.help = ui.CreateSingleLineUILabel(x+2, y+22, "hjkl - move, x - drop, e - equip", style)
@ -154,7 +149,7 @@ func (pim *PlayerInventoryMenu) Size() engine.Size {
return pim.inventoryMenu.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) { func (pim *PlayerInventoryMenu) SelectSlot(x, y int) {
if pim.selectedInventorySlot.X() == x && pim.selectedInventorySlot.Y() == y {
return
}
pim.inventoryGrid.Unhighlight() pim.inventoryGrid.Unhighlight()
pim.selectedInventorySlot = engine.PositionAt(x, y) pim.selectedInventorySlot = engine.PositionAt(x, y)
pim.inventoryGrid.Highlight(pim.selectedInventorySlot) pim.inventoryGrid.Highlight(pim.selectedInventorySlot)

View file

@ -2,6 +2,7 @@ package ui
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@ -96,6 +97,6 @@ func (sb *UISimpleButton) Draw(v views.View) {
sb.text.Draw(v) 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 ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"github.com/gdamore/tcell/v2"
) )
type UIElement interface { type UIElement interface {
MoveTo(x, y int) MoveTo(x, y int)
Position() engine.Position Position() engine.Position
Size() engine.Size Size() engine.Size
Input(e *tcell.EventKey) Input(inputAction input.InputAction)
engine.Drawable engine.Drawable
} }

View file

@ -2,6 +2,7 @@ package ui
import ( import (
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/input"
"unicode/utf8" "unicode/utf8"
"github.com/gdamore/tcell/v2" "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) titlePos := (width / 2) - int(titleLen/2)
if title != "" {
w.title = engine.CreateText(x+titlePos, y, int(titleLen), 1, title, style) w.title = engine.CreateText(x+titlePos, y, int(titleLen), 1, title, style)
}
w.box = engine.CreateRectangle( w.box = engine.CreateRectangle(
x, y, width, height, x, y, width, height,
@ -49,13 +52,16 @@ func (w *UIWindow) Position() engine.Position {
} }
func (w *UIWindow) Size() engine.Size { func (w *UIWindow) Size() engine.Size {
return engine.SizeOf(0, 0) return w.box.Size()
} }
func (w *UIWindow) Draw(v views.View) { func (w *UIWindow) Draw(v views.View) {
w.box.Draw(v) w.box.Draw(v)
if w.title != nil {
w.title.Draw(v) 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" "math/rand"
"mvvasilev/last_light/engine" "mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item" "mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/model" "mvvasilev/last_light/game/npc"
"mvvasilev/last_light/game/rpg" "mvvasilev/last_light/game/rpg"
"slices" "slices"
@ -113,7 +113,6 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLeve
return item.CreateBasicItem(item.ItemTypeFish(), 1) return item.CreateBasicItem(item.ItemTypeFish(), 1)
}) })
genTable.Add(1, func() item.Item {
itemTypes := []rpg.RPGItemType{ itemTypes := []rpg.RPGItemType{
rpg.ItemTypeBow(), rpg.ItemTypeBow(),
rpg.ItemTypeLongsword(), rpg.ItemTypeLongsword(),
@ -128,6 +127,7 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLeve
rpg.ItemTypeSpear(), rpg.ItemTypeSpear(),
} }
genTable.Add(1, func() item.Item {
itemType := itemTypes[rand.Intn(len(itemTypes))] itemType := itemTypes[rand.Intn(len(itemTypes))]
rarities := []rpg.ItemRarity{ rarities := []rpg.ItemRarity{
@ -156,7 +156,7 @@ func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLeve
groundLevel = CreateBSPDungeonMap(width, height, 4) groundLevel = CreateBSPDungeonMap(width, height, 4)
} }
items := SpawnItems(groundLevel.Rooms(), 0.02, genTable, []engine.Position{ items := SpawnItems(groundLevel.Rooms(), 0.1, genTable, []engine.Position{
groundLevel.NextLevelStaircasePosition(), groundLevel.NextLevelStaircasePosition(),
groundLevel.PlayerSpawnPoint(), groundLevel.PlayerSpawnPoint(),
groundLevel.PreviousLevelStaircasePosition(), groundLevel.PreviousLevelStaircasePosition(),
@ -202,9 +202,9 @@ func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTa
numItems := rand.Intn(maxItems) numItems := rand.Intn(maxItems)
for range numItems { for range numItems {
itemType := genTable.Generate() item := genTable.Generate()
if itemType == nil { if item == nil {
continue continue
} }
@ -217,7 +217,7 @@ func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTa
continue 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) 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) d.entityLevel.AddEntity(entity, presentation, style)
} }
@ -291,6 +291,14 @@ func (d *DungeonLevel) IsTilePassable(x, y int) bool {
return d.TileAt(x, y).Passable() 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 { func (d *DungeonLevel) Flatten() Map {
return d.multilevel return d.multilevel
} }

View file

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

View file

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