Add character creation screen

This commit is contained in:
Miroslav Vasilev 2024-05-31 23:37:06 +03:00
parent 0f093dd7f9
commit 9fdf117c24
10 changed files with 396 additions and 29 deletions

View file

@ -131,6 +131,10 @@ func LimitDecrement(i int, limit int) int {
} }
func RandInt(min, max int) int { func RandInt(min, max int) int {
if min == max {
return min
}
return min + rand.Intn(max-min) return min + rand.Intn(max-min)
} }

View file

@ -35,8 +35,7 @@ const (
InputAction_PickUpItem InputAction_PickUpItem
InputAction_OpenLogs InputAction_OpenLogs
InputAction_DropItem InputAction_DropItem
InputAction_EquipItem InputAction_InteractItem
InputAction_UnequipItem
InputAction_PauseGame InputAction_PauseGame
@ -76,7 +75,7 @@ func CreateInputSystemWithDefaultBindings() *InputSystem {
InputKeyOf(InputContext_Menu, 0, tcell.KeyCR, 13): InputAction_Menu_Select, 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.KeyESC, 0): InputAction_Menu_Exit,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyRune, 'i'): 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.KeyRune, 'd'): InputAction_DropItem,
InputKeyOf(InputContext_Inventory, 0, tcell.KeyLeft, 0): InputAction_Menu_HighlightLeft, 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.KeyRight, 0): InputAction_Menu_HighlightRight,

View file

@ -18,19 +18,14 @@ type Player struct {
*rpg.BasicRPGEntity *rpg.BasicRPGEntity
} }
func CreatePlayer(x, y int) *Player { func CreatePlayer(x, y int, playerStats map[rpg.Stat]int) *Player {
p := new(Player) p := new(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{ playerStats,
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{}, map[rpg.Stat][]rpg.StatModifier{},
) )

View file

@ -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
}

View file

@ -36,6 +36,21 @@ const (
Stat_MaxHealthBonus Stat = 140 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 StatModifierId string
type StatModifier struct { type StatModifier struct {

View file

@ -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}
}

View file

@ -84,7 +84,7 @@ func (mms *MainMenuState) OnTick(dt int64) GameState {
} }
if mms.startNewGame { if mms.startNewGame {
return CreatePlayingState(mms.turnSystem, mms.inputSystem) return CreateCharacterCreationState(mms.turnSystem, mms.inputSystem)
} }
return mms return mms

View file

@ -35,7 +35,7 @@ type PlayingState struct {
nextGameState GameState 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() turnSystem.Clear()
s := new(PlayingState) 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.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.player.Heal(rpg.BaseMaxHealth(s.player))
s.turnSystem.Schedule(10, func() (complete bool, requeue bool) { s.turnSystem.Schedule(10, func() (complete bool, requeue bool) {

View file

@ -45,6 +45,10 @@ func (t *UILabel) Position() engine.Position {
return t.text.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 { func (t *UILabel) Size() engine.Size {
return t.text.Size() return t.text.Size()
} }

View file

@ -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)
}