From 9fdf117c2421aad13a36e31c924622d88b22105c Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Fri, 31 May 2024 23:37:06 +0300 Subject: [PATCH] Add character creation screen --- engine/util.go | 4 + game/input/input_system.go | 5 +- game/player/player.go | 9 +- game/player/player_logic.go | 16 -- game/rpg/rpg_system.go | 15 ++ game/state/character_creation_state.go | 164 ++++++++++++++++++++ game/state/main_menu_state.go | 2 +- game/state/playing_state.go | 8 +- game/ui/label.go | 4 + game/ui/menu/character_creation_menu.go | 198 ++++++++++++++++++++++++ 10 files changed, 396 insertions(+), 29 deletions(-) delete mode 100644 game/player/player_logic.go create mode 100644 game/state/character_creation_state.go create mode 100644 game/ui/menu/character_creation_menu.go diff --git a/engine/util.go b/engine/util.go index a3c2575..eca0461 100644 --- a/engine/util.go +++ b/engine/util.go @@ -131,6 +131,10 @@ func LimitDecrement(i int, limit int) int { } func RandInt(min, max int) int { + if min == max { + return min + } + return min + rand.Intn(max-min) } diff --git a/game/input/input_system.go b/game/input/input_system.go index fb4688d..7347e1c 100644 --- a/game/input/input_system.go +++ b/game/input/input_system.go @@ -35,8 +35,7 @@ const ( InputAction_PickUpItem InputAction_OpenLogs InputAction_DropItem - InputAction_EquipItem - InputAction_UnequipItem + InputAction_InteractItem InputAction_PauseGame @@ -76,7 +75,7 @@ func CreateInputSystemWithDefaultBindings() *InputSystem { 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, 'e'): InputAction_InteractItem, 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, diff --git a/game/player/player.go b/game/player/player.go index 32bd69c..20a05b4 100644 --- a/game/player/player.go +++ b/game/player/player.go @@ -18,19 +18,14 @@ type Player struct { *rpg.BasicRPGEntity } -func CreatePlayer(x, y int) *Player { +func CreatePlayer(x, y int, playerStats map[rpg.Stat]int) *Player { p := new(Player) p.id = uuid.New() p.position = engine.PositionAt(x, y) p.inventory = item.CreateEquippedInventory() 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, - }, + playerStats, map[rpg.Stat][]rpg.StatModifier{}, ) diff --git a/game/player/player_logic.go b/game/player/player_logic.go deleted file mode 100644 index 67f0a16..0000000 --- a/game/player/player_logic.go +++ /dev/null @@ -1,16 +0,0 @@ -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/rpg_system.go b/game/rpg/rpg_system.go index edc3451..fd86903 100644 --- a/game/rpg/rpg_system.go +++ b/game/rpg/rpg_system.go @@ -36,6 +36,21 @@ const ( Stat_MaxHealthBonus Stat = 140 ) +func StatLongName(stat Stat) string { + switch stat { + case Stat_Attributes_Strength: + return "Strength" + case Stat_Attributes_Intelligence: + return "Intelligence" + case Stat_Attributes_Dexterity: + return "Dexterity" + case Stat_Attributes_Constitution: + return "Constitution" + default: + return "Unknown" + } +} + type StatModifierId string type StatModifier struct { diff --git a/game/state/character_creation_state.go b/game/state/character_creation_state.go new file mode 100644 index 0000000..474de3c --- /dev/null +++ b/game/state/character_creation_state.go @@ -0,0 +1,164 @@ +package state + +import ( + "mvvasilev/last_light/engine" + "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/rpg" + "mvvasilev/last_light/game/turns" + "mvvasilev/last_light/game/ui/menu" + + "github.com/gdamore/tcell/v2" +) + +const ( + MinStatValue = 1 + MaxStatValue = 20 +) + +type CharacterCreationState struct { + turnSystem *turns.TurnSystem + inputSystem *input.InputSystem + + startGame bool + + menuState *menu.CharacterCreationMenuState + ccMenu *menu.CharacterCreationMenu +} + +func CreateCharacterCreationState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *CharacterCreationState { + + menuState := &menu.CharacterCreationMenuState{ + AvailablePoints: 21, + CurrentHighlight: 0, + Stats: []*menu.StatState{ + { + Stat: rpg.Stat_Attributes_Strength, + Value: 1, + }, + { + Stat: rpg.Stat_Attributes_Dexterity, + Value: 1, + }, + { + Stat: rpg.Stat_Attributes_Intelligence, + Value: 1, + }, + { + Stat: rpg.Stat_Attributes_Constitution, + Value: 1, + }, + }, + } + + ccs := &CharacterCreationState{ + turnSystem: turnSystem, + inputSystem: inputSystem, + menuState: menuState, + ccMenu: menu.CreateCharacterCreationMenu(menuState, tcell.StyleDefault), + } + + ccs.menuState.RandomizeCharacter = func() { + ccs.menuState.AvailablePoints = 21 + + for _, s := range ccs.menuState.Stats { + if ccs.menuState.AvailablePoints == 0 { + break + } + + limit := ccs.menuState.AvailablePoints + + if limit > 20 { + limit = 20 + } + + s.Value = engine.RandInt(1, limit+1) + ccs.menuState.AvailablePoints -= s.Value - 1 + } + + ccs.ccMenu.UpdateState(ccs.menuState) + } + + ccs.menuState.StartGame = func() { + if ccs.menuState.AvailablePoints > 0 { + return + } + + ccs.startGame = true + } + + return ccs +} + +func (ccs *CharacterCreationState) InputContext() input.Context { + return input.InputContext_Menu +} + +func (ccs *CharacterCreationState) IncreaseStatValue() { + // If there are no points to allocate, stop + if ccs.menuState.AvailablePoints == 0 { + return + } + + // If the current highlight is beyond the array range, stop + if ccs.menuState.CurrentHighlight < 0 || ccs.menuState.CurrentHighlight >= len(ccs.menuState.Stats) { + return + } + + // If the allowed max state value has already been reached + if ccs.menuState.Stats[ccs.menuState.CurrentHighlight].Value+1 > MaxStatValue { + return + } + + ccs.menuState.Stats[ccs.menuState.CurrentHighlight].Value++ + ccs.menuState.AvailablePoints-- +} + +func (ccs *CharacterCreationState) DecreaseStatValue() { + // If the current highlight is beyond the array range, stop + if ccs.menuState.CurrentHighlight < 0 || ccs.menuState.CurrentHighlight >= len(ccs.menuState.Stats) { + return + } + + // If the allowed min state value has already been reached + if ccs.menuState.Stats[ccs.menuState.CurrentHighlight].Value-1 < MinStatValue { + return + } + + ccs.menuState.Stats[ccs.menuState.CurrentHighlight].Value-- + ccs.menuState.AvailablePoints++ +} + +func (ccs *CharacterCreationState) OnTick(dt int64) GameState { + if ccs.startGame { + stats := map[rpg.Stat]int{} + + for _, s := range ccs.menuState.Stats { + stats[s.Stat] = s.Value + } + + return CreatePlayingState(ccs.turnSystem, ccs.inputSystem, stats) + } + + action := ccs.inputSystem.NextAction() + + switch action { + case input.InputAction_Menu_HighlightRight: + ccs.IncreaseStatValue() + case input.InputAction_Menu_HighlightLeft: + ccs.DecreaseStatValue() + case input.InputAction_Menu_HighlightDown: + ccs.menuState.CurrentHighlight++ + case input.InputAction_Menu_HighlightUp: + ccs.menuState.CurrentHighlight-- + case input.InputAction_Menu_Select: + ccs.ccMenu.SelectHighlight() + } + + ccs.ccMenu.UpdateState(ccs.menuState) + + return ccs +} + +func (ccs *CharacterCreationState) CollectDrawables() []engine.Drawable { + return []engine.Drawable{ccs.ccMenu} +} diff --git a/game/state/main_menu_state.go b/game/state/main_menu_state.go index c878cd7..23e473e 100644 --- a/game/state/main_menu_state.go +++ b/game/state/main_menu_state.go @@ -84,7 +84,7 @@ func (mms *MainMenuState) OnTick(dt int64) GameState { } if mms.startNewGame { - return CreatePlayingState(mms.turnSystem, mms.inputSystem) + return CreateCharacterCreationState(mms.turnSystem, mms.inputSystem) } return mms diff --git a/game/state/playing_state.go b/game/state/playing_state.go index 5fbad66..456427b 100644 --- a/game/state/playing_state.go +++ b/game/state/playing_state.go @@ -35,7 +35,7 @@ type PlayingState struct { nextGameState GameState } -func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem) *PlayingState { +func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSystem, playerStats map[rpg.Stat]int) *PlayingState { turnSystem.Clear() s := new(PlayingState) @@ -47,7 +47,11 @@ func CreatePlayingState(turnSystem *turns.TurnSystem, inputSystem *input.InputSy 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().X(), + s.dungeon.CurrentLevel().PlayerSpawnPoint().Y(), + playerStats, + ) s.player.Heal(rpg.BaseMaxHealth(s.player)) s.turnSystem.Schedule(10, func() (complete bool, requeue bool) { diff --git a/game/ui/label.go b/game/ui/label.go index 06be436..a4c3813 100644 --- a/game/ui/label.go +++ b/game/ui/label.go @@ -45,6 +45,10 @@ func (t *UILabel) Position() engine.Position { return t.text.Position() } +func (t *UILabel) SetContent(content string) { + t.text = engine.CreateText(t.text.Position().X(), t.text.Position().Y(), int(t.text.Size().Width()), int(t.Size().Height()), content, t.text.Style()) +} + func (t *UILabel) Size() engine.Size { return t.text.Size() } diff --git a/game/ui/menu/character_creation_menu.go b/game/ui/menu/character_creation_menu.go new file mode 100644 index 0000000..74c930c --- /dev/null +++ b/game/ui/menu/character_creation_menu.go @@ -0,0 +1,198 @@ +package menu + +import ( + "fmt" + "mvvasilev/last_light/engine" + "mvvasilev/last_light/game/input" + "mvvasilev/last_light/game/rpg" + "mvvasilev/last_light/game/ui" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type statSelection struct { + stat rpg.Stat + label *ui.UILabel + plusButton *ui.UILabel + statNumberLabel *ui.UILabel + minusButton *ui.UILabel +} + +type StatState struct { + Stat rpg.Stat + Value int +} + +type CharacterCreationMenuState struct { + AvailablePoints int + CurrentHighlight int + Stats []*StatState + RandomizeCharacter func() + StartGame func() +} + +type CharacterCreationMenu struct { + window *ui.UIWindow + + availablePointsLabel *ui.UILabel + + state *CharacterCreationMenuState + + stats []*statSelection + randomizeButton *ui.UISimpleButton + startGameButton *ui.UISimpleButton + + style tcell.Style +} + +func CreateCharacterCreationMenu(state *CharacterCreationMenuState, style tcell.Style) *CharacterCreationMenu { + ccm := &CharacterCreationMenu{ + state: state, + window: ui.CreateWindow(0, 0, engine.TERMINAL_SIZE_WIDTH, engine.TERMINAL_SIZE_HEIGHT, "Create Character", style), + style: style, + } + + ccm.UpdateState(state) + + return ccm +} + +func (ccm *CharacterCreationMenu) UpdateState(state *CharacterCreationMenuState) { + ccm.state = state + + width, height := ccm.Size().WH() + + availablePointsText := fmt.Sprintf("Available Points: %v", state.AvailablePoints) + + ccm.availablePointsLabel = ui.CreateSingleLineUILabel( + width/2-len(availablePointsText)/2, + 1, + availablePointsText, + ccm.style, + ) + + statPlaceholderText := fmt.Sprintf("%-12s [ < ] 00 [ > ]", "Placeholder") + statX := width/2 - len(statPlaceholderText)/2 + + statsArr := make([]*statSelection, 0, 4) + + for i, s := range state.Stats { + + labelStyle := ccm.style + + if i == state.CurrentHighlight { + labelStyle = ccm.style.Attributes(tcell.AttrBold).Background(tcell.ColorWhite).Foreground(tcell.ColorBlack) + } + + statsArr = append( + statsArr, + &statSelection{ + stat: s.Stat, + label: ui.CreateSingleLineUILabel( + statX, + 3+i, + rpg.StatLongName(s.Stat), + labelStyle, + ), + minusButton: ui.CreateSingleLineUILabel( + statX+12+3, // Account for highlighting brackets + 3+i, + "<", + ccm.style, + ), + statNumberLabel: ui.CreateSingleLineUILabel( + statX+12+3+1+3, + 3+i, + fmt.Sprintf("%02v", s.Value), + ccm.style, + ), + plusButton: ui.CreateSingleLineUILabel( + statX+12+3+1+3+2+3, // Account for highlighting brackets + 3+i, + ">", + ccm.style, + ), + }, + ) + } + + ccm.stats = statsArr + + randomizeLabel := "Randomize" + + ccm.randomizeButton = ui.CreateSimpleButton( + width/2-len(randomizeLabel)/2, + 3+len(statsArr)+1, + randomizeLabel, + ccm.style, + ccm.style.Attributes(tcell.AttrBold), + state.RandomizeCharacter, + ) + + startGameLabel := "Start Game" + + ccm.startGameButton = ui.CreateSimpleButton( + width/2-len(startGameLabel)/2, + height-2, + startGameLabel, + ccm.style, + ccm.style.Attributes(tcell.AttrBold), + state.StartGame, + ) + + if state.CurrentHighlight == len(state.Stats) { + ccm.randomizeButton.Highlight() + } + + if state.CurrentHighlight == len(state.Stats)+1 { + ccm.startGameButton.Highlight() + } +} + +func (ccm *CharacterCreationMenu) SelectHighlight() { + if ccm.state.CurrentHighlight == len(ccm.state.Stats) { + ccm.randomizeButton.Select() + } + + if ccm.state.CurrentHighlight == len(ccm.state.Stats)+1 { + ccm.startGameButton.Select() + } +} + +func (ccm *CharacterCreationMenu) MoveTo(x int, y int) { +} + +func (ccm *CharacterCreationMenu) Position() engine.Position { + return engine.PositionAt(0, 0) +} + +func (ccm *CharacterCreationMenu) Size() engine.Size { + return engine.SizeOf(engine.TERMINAL_SIZE_WIDTH, engine.TERMINAL_SIZE_HEIGHT) +} + +func (ccm *CharacterCreationMenu) Input(inputAction input.InputAction) { + +} + +func (ccm *CharacterCreationMenu) UniqueId() uuid.UUID { + return uuid.New() +} + +func (ccm *CharacterCreationMenu) Draw(v views.View) { + ccm.window.Draw(v) + + ccm.availablePointsLabel.Draw(v) + + for _, val := range ccm.stats { + val.label.Draw(v) + val.minusButton.Draw(v) + val.statNumberLabel.Draw(v) + val.plusButton.Draw(v) + } + + ccm.randomizeButton.Draw(v) + + ccm.startGameButton.Draw(v) +}