mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-18 20:29:52 +03:00
Add character creation screen
This commit is contained in:
parent
0f093dd7f9
commit
9fdf117c24
10 changed files with 396 additions and 29 deletions
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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{},
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
164
game/state/character_creation_state.go
Normal file
164
game/state/character_creation_state.go
Normal 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}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
198
game/ui/menu/character_creation_menu.go
Normal file
198
game/ui/menu/character_creation_menu.go
Normal 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)
|
||||
}
|
Loading…
Add table
Reference in a new issue