From 0f093dd7f93650c352040d4010f7dc7bb7ed9ff7 Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Thu, 30 May 2024 23:39:54 +0300 Subject: [PATCH] Turn system, bunch of other things --- engine/event_logger.go | 50 ++++++ engine/priority_queue.go | 73 ++++++++ engine/raycasting.go | 53 ++++++ engine/rectangle.go | 44 +++++ engine/text.go | 12 ++ engine/util.go | 10 ++ game/game.go | 23 ++- game/input/input_system.go | 111 ++++++++++++ game/item/inventory.go | 2 +- game/item/item_type.go | 12 ++ game/{model => npc}/entity.go | 33 +++- game/{model => npc}/npc.go | 2 +- game/player/player.go | 10 +- game/player/player_logic.go | 16 ++ game/rpg/generate_items.go | 173 +++++++++++++++--- game/rpg/rpg_entity.go | 8 +- game/rpg/rpg_items.go | 40 +++++ game/rpg/rpg_system.go | 16 +- game/state/dialog_state.go | 24 ++- game/state/game_state.go | 13 +- game/state/inventory_screen_state.go | 111 +++++------- game/state/main_menu_state.go | 30 +++- game/state/pause_game_state.go | 51 +++--- game/state/playing_state.go | 241 +++++++++++++++----------- game/state/quit_state.go | 7 +- game/turns/turn_system.go | 59 +++++++ game/ui/container.go | 82 --------- game/ui/dialog.go | 7 +- game/ui/event_logger.go | 62 +++++++ game/ui/health_bar.go | 104 +++++++++++ game/ui/item.go | 166 ++++++++++++++++++ game/ui/label.go | 3 +- game/ui/menu/player_inventory_menu.go | 27 ++- game/ui/simple_button.go | 3 +- game/ui/ui.go | 5 +- game/ui/window.go | 14 +- game/world/dungeon.go | 48 ++--- game/world/entity_map.go | 4 +- game/world/tile.go | 10 +- 39 files changed, 1339 insertions(+), 420 deletions(-) create mode 100644 engine/event_logger.go create mode 100644 engine/priority_queue.go create mode 100644 engine/raycasting.go create mode 100644 game/input/input_system.go rename game/{model => npc}/entity.go (63%) rename game/{model => npc}/npc.go (97%) create mode 100644 game/player/player_logic.go create mode 100644 game/turns/turn_system.go delete mode 100644 game/ui/container.go create mode 100644 game/ui/event_logger.go create mode 100644 game/ui/health_bar.go create mode 100644 game/ui/item.go diff --git a/engine/event_logger.go b/engine/event_logger.go new file mode 100644 index 0000000..e7c7886 --- /dev/null +++ b/engine/event_logger.go @@ -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)] +} diff --git a/engine/priority_queue.go b/engine/priority_queue.go new file mode 100644 index 0000000..8e12147 --- /dev/null +++ b/engine/priority_queue.go @@ -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) +} diff --git a/engine/raycasting.go b/engine/raycasting.go new file mode 100644 index 0000000..164ad2e --- /dev/null +++ b/engine/raycasting.go @@ -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 +} diff --git a/engine/rectangle.go b/engine/rectangle.go index 596f0a3..aa12dc6 100644 --- a/engine/rectangle.go +++ b/engine/rectangle.go @@ -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) + } + } +} diff --git a/engine/text.go b/engine/text.go index e2b011e..86ed39f 100644 --- a/engine/text.go +++ b/engine/text.go @@ -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++ + } +} diff --git a/engine/util.go b/engine/util.go index eba5131..a3c2575 100644 --- a/engine/util.go +++ b/engine/util.go @@ -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 +} diff --git a/game/game.go b/game/game.go index f663a07..e6cf33e 100644 --- a/game/game.go +++ b/game/game.go @@ -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 { diff --git a/game/input/input_system.go b/game/input/input_system.go new file mode 100644 index 0000000..fb4688d --- /dev/null +++ b/game/input/input_system.go @@ -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 +} diff --git a/game/item/inventory.go b/game/item/inventory.go index 2c5ac04..eb8ea54 100644 --- a/game/item/inventory.go +++ b/game/item/inventory.go @@ -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 } diff --git a/game/item/item_type.go b/game/item/item_type.go index d3a175a..014c98a 100644 --- a/game/item/item_type.go +++ b/game/item/item_type.go @@ -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: '¬', diff --git a/game/model/entity.go b/game/npc/entity.go similarity index 63% rename from game/model/entity.go rename to game/npc/entity.go index d841fc0..4a72a64 100644 --- a/game/model/entity.go +++ b/game/npc/entity.go @@ -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 } diff --git a/game/model/npc.go b/game/npc/npc.go similarity index 97% rename from game/model/npc.go rename to game/npc/npc.go index b4b4a1e..8aa4b47 100644 --- a/game/model/npc.go +++ b/game/npc/npc.go @@ -1,4 +1,4 @@ -package model +package npc import ( "mvvasilev/last_light/engine" diff --git a/game/player/player.go b/game/player/player.go index 6488fa3..32bd69c 100644 --- a/game/player/player.go +++ b/game/player/player.go @@ -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 } diff --git a/game/player/player_logic.go b/game/player/player_logic.go new file mode 100644 index 0000000..67f0a16 --- /dev/null +++ b/game/player/player_logic.go @@ -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 +} diff --git a/game/rpg/generate_items.go b/game/rpg/generate_items.go index babf506..5c876d8 100644 --- a/game/rpg/generate_items.go +++ b/game/rpg/generate_items.go @@ -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), ) } diff --git a/game/rpg/rpg_entity.go b/game/rpg/rpg_entity.go index 4f5c98a..e59b066 100644 --- a/game/rpg/rpg_entity.go +++ b/game/rpg/rpg_entity.go @@ -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 { diff --git a/game/rpg/rpg_items.go b/game/rpg/rpg_items.go index 0d53740..f323dc7 100644 --- a/game/rpg/rpg_items.go +++ b/game/rpg/rpg_items.go @@ -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", 'Î', diff --git a/game/rpg/rpg_system.go b/game/rpg/rpg_system.go index fabea95..edc3451 100644 --- a/game/rpg/rpg_system.go +++ b/game/rpg/rpg_system.go @@ -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 } diff --git a/game/state/dialog_state.go b/game/state/dialog_state.go index 4dce277..e20056a 100644 --- a/game/state/dialog_state.go +++ b/game/state/dialog_state.go @@ -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() } diff --git a/game/state/game_state.go b/game/state/game_state.go index 23e52ad..b04fcf9 100644 --- a/game/state/game_state.go +++ b/game/state/game_state.go @@ -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 -} diff --git a/game/state/inventory_screen_state.go b/game/state/inventory_screen_state.go index 4ff6fde..e225c61 100644 --- a/game/state/inventory_screen_state.go +++ b/game/state/inventory_screen_state.go @@ -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 { diff --git a/game/state/main_menu_state.go b/game/state/main_menu_state.go index f6b42b1..c878cd7 100644 --- a/game/state/main_menu_state.go +++ b/game/state/main_menu_state.go @@ -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 diff --git a/game/state/pause_game_state.go b/game/state/pause_game_state.go index 426dfe2..92c3f00 100644 --- a/game/state/pause_game_state.go +++ b/game/state/pause_game_state.go @@ -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 diff --git a/game/state/playing_state.go b/game/state/playing_state.go index 86fdcf1..5fbad66 100644 --- a/game/state/playing_state.go +++ b/game/state/playing_state.go @@ -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 } diff --git a/game/state/quit_state.go b/game/state/quit_state.go index ed07b27..60bb702 100644 --- a/game/state/quit_state.go +++ b/game/state/quit_state.go @@ -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 { diff --git a/game/turns/turn_system.go b/game/turns/turn_system.go new file mode 100644 index 0000000..3cf3a36 --- /dev/null +++ b/game/turns/turn_system.go @@ -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() +} diff --git a/game/ui/container.go b/game/ui/container.go deleted file mode 100644 index 0dde369..0000000 --- a/game/ui/container.go +++ /dev/null @@ -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) - } -} diff --git a/game/ui/dialog.go b/game/ui/dialog.go index 733dba9..a3f0d76 100644 --- a/game/ui/dialog.go +++ b/game/ui/dialog.go @@ -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 } diff --git a/game/ui/event_logger.go b/game/ui/event_logger.go new file mode 100644 index 0000000..a395bf4 --- /dev/null +++ b/game/ui/event_logger.go @@ -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) + } +} diff --git a/game/ui/health_bar.go b/game/ui/health_bar.go new file mode 100644 index 0000000..fc41104 --- /dev/null +++ b/game/ui/health_bar.go @@ -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, + ) +} diff --git a/game/ui/item.go b/game/ui/item.go new file mode 100644 index 0000000..e9be428 --- /dev/null +++ b/game/ui/item.go @@ -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) + } +} diff --git a/game/ui/label.go b/game/ui/label.go index 4db66c7..06be436 100644 --- a/game/ui/label.go +++ b/game/ui/label.go @@ -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) {} diff --git a/game/ui/menu/player_inventory_menu.go b/game/ui/menu/player_inventory_menu.go index 8585977..f66a62b 100644 --- a/game/ui/menu/player_inventory_menu.go +++ b/game/ui/menu/player_inventory_menu.go @@ -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) diff --git a/game/ui/simple_button.go b/game/ui/simple_button.go index e6536eb..2302eff 100644 --- a/game/ui/simple_button.go +++ b/game/ui/simple_button.go @@ -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) { } diff --git a/game/ui/ui.go b/game/ui/ui.go index adbb5d4..42bcff4 100644 --- a/game/ui/ui.go +++ b/game/ui/ui.go @@ -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 } diff --git a/game/ui/window.go b/game/ui/window.go index 9652639..c47184b 100644 --- a/game/ui/window.go +++ b/game/ui/window.go @@ -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) { } diff --git a/game/world/dungeon.go b/game/world/dungeon.go index 15d89d4..f0872e3 100644 --- a/game/world/dungeon.go +++ b/game/world/dungeon.go @@ -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 } diff --git a/game/world/entity_map.go b/game/world/entity_map.go index 9a21891..f294276 100644 --- a/game/world/entity_map.go +++ b/game/world/entity_map.go @@ -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 } diff --git a/game/world/tile.go b/game/world/tile.go index d4a9e09..ed277a1 100644 --- a/game/world/tile.go +++ b/game/world/tile.go @@ -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 }