feat: nickname input screen for first-time players
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
92
ui/model.go
92
ui/model.go
@@ -24,6 +24,7 @@ const (
|
||||
screenStats
|
||||
screenAchievements
|
||||
screenLeaderboard
|
||||
screenNickname
|
||||
)
|
||||
|
||||
// StateUpdateMsg is sent by GameSession to update the view
|
||||
@@ -55,6 +56,7 @@ type Model struct {
|
||||
chatInput string
|
||||
rankingSaved bool
|
||||
shopMsg string
|
||||
nicknameInput string
|
||||
}
|
||||
|
||||
func NewModel(width, height int, fingerprint string, lobby *game.Lobby, db *store.DB) Model {
|
||||
@@ -116,6 +118,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m.updateAchievements(msg)
|
||||
case screenLeaderboard:
|
||||
return m.updateLeaderboard(msg)
|
||||
case screenNickname:
|
||||
return m.updateNickname(msg)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -162,6 +166,8 @@ func (m Model) View() string {
|
||||
byGold, _ = m.store.TopRunsByGold(10)
|
||||
}
|
||||
return renderLeaderboard(byFloor, byGold, m.width, m.height)
|
||||
case screenNickname:
|
||||
return renderNickname(m.nicknameInput, m.width, m.height)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -195,25 +201,37 @@ func isDown(key tea.KeyMsg) bool {
|
||||
func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
if isEnter(key) {
|
||||
if m.store != nil {
|
||||
name, err := m.store.GetProfile(m.fingerprint)
|
||||
if err != nil {
|
||||
m.playerName = "Adventurer"
|
||||
if m.store != nil && m.fingerprint != "" {
|
||||
m.store.SaveProfile(m.fingerprint, m.playerName)
|
||||
}
|
||||
} else {
|
||||
m.playerName = name
|
||||
}
|
||||
} else {
|
||||
m.playerName = "Adventurer"
|
||||
}
|
||||
if m.fingerprint == "" {
|
||||
m.fingerprint = fmt.Sprintf("anon-%d", time.Now().UnixNano())
|
||||
}
|
||||
if m.store != nil {
|
||||
name, err := m.store.GetProfile(m.fingerprint)
|
||||
if err != nil {
|
||||
// First time player — show nickname input
|
||||
m.screen = screenNickname
|
||||
m.nicknameInput = ""
|
||||
return m, nil
|
||||
}
|
||||
m.playerName = name
|
||||
} else {
|
||||
m.playerName = "Adventurer"
|
||||
}
|
||||
if m.lobby != nil {
|
||||
m.lobby.PlayerOnline(m.fingerprint, m.playerName)
|
||||
}
|
||||
// Check for active session to reconnect
|
||||
if m.lobby != nil {
|
||||
code, session := m.lobby.GetActiveSession(m.fingerprint)
|
||||
if session != nil {
|
||||
m.roomCode = code
|
||||
m.session = session
|
||||
m.gameState = m.session.GetState()
|
||||
m.screen = screenGame
|
||||
m.session.TouchActivity(m.fingerprint)
|
||||
m.session.SendChat("System", m.playerName+" reconnected!")
|
||||
return m, m.pollState()
|
||||
}
|
||||
}
|
||||
m.screen = screenLobby
|
||||
m = m.withRefreshedLobby()
|
||||
} else if isKey(key, "h") {
|
||||
@@ -231,6 +249,48 @@ func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateNickname(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
if isEnter(key) && len(m.nicknameInput) > 0 {
|
||||
m.playerName = m.nicknameInput
|
||||
if m.store != nil && m.fingerprint != "" {
|
||||
m.store.SaveProfile(m.fingerprint, m.playerName)
|
||||
}
|
||||
m.nicknameInput = ""
|
||||
if m.lobby != nil {
|
||||
m.lobby.PlayerOnline(m.fingerprint, m.playerName)
|
||||
}
|
||||
// Check for active session to reconnect
|
||||
if m.lobby != nil {
|
||||
code, session := m.lobby.GetActiveSession(m.fingerprint)
|
||||
if session != nil {
|
||||
m.roomCode = code
|
||||
m.session = session
|
||||
m.gameState = m.session.GetState()
|
||||
m.screen = screenGame
|
||||
m.session.TouchActivity(m.fingerprint)
|
||||
m.session.SendChat("System", m.playerName+" reconnected!")
|
||||
return m, m.pollState()
|
||||
}
|
||||
}
|
||||
m.screen = screenLobby
|
||||
m = m.withRefreshedLobby()
|
||||
} else if isKey(key, "esc") || key.Type == tea.KeyEsc {
|
||||
m.nicknameInput = ""
|
||||
m.screen = screenTitle
|
||||
} else if key.Type == tea.KeyBackspace && len(m.nicknameInput) > 0 {
|
||||
m.nicknameInput = m.nicknameInput[:len(m.nicknameInput)-1]
|
||||
} else if len(key.Runes) == 1 && len(m.nicknameInput) < 12 {
|
||||
ch := string(key.Runes)
|
||||
// Only allow alphanumeric and some special chars
|
||||
if len(ch) == 1 && ch[0] >= 32 && ch[0] < 127 {
|
||||
m.nicknameInput += ch
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateStats(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
if isKey(key, "s") || isEnter(key) || isQuit(key) {
|
||||
@@ -351,6 +411,9 @@ func (m Model) updateClassSelect(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
player := entity.NewPlayer(m.playerName, selectedClass)
|
||||
player.Fingerprint = m.fingerprint
|
||||
m.session.AddPlayer(player)
|
||||
if m.lobby != nil {
|
||||
m.lobby.RegisterSession(m.fingerprint, m.roomCode)
|
||||
}
|
||||
m.session.StartGame()
|
||||
m.lobby.StartRoom(m.roomCode)
|
||||
m.gameState = m.session.GetState()
|
||||
@@ -600,6 +663,9 @@ func (m Model) updateShop(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m Model) updateResult(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
if isEnter(key) {
|
||||
if m.lobby != nil && m.fingerprint != "" {
|
||||
m.lobby.UnregisterSession(m.fingerprint)
|
||||
}
|
||||
if m.session != nil {
|
||||
m.session.Stop()
|
||||
m.session = nil
|
||||
|
||||
@@ -28,14 +28,28 @@ func TestTitleToLobby(t *testing.T) {
|
||||
t.Fatalf("initial screen: got %d, want screenTitle(0)", m.screen)
|
||||
}
|
||||
|
||||
// Press Enter
|
||||
// First-time player: Enter goes to nickname screen
|
||||
result, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||
m2 := result.(Model)
|
||||
|
||||
if m2.screen != screenLobby {
|
||||
t.Errorf("after Enter: screen=%d, want screenLobby(1)", m2.screen)
|
||||
if m2.screen != screenNickname {
|
||||
t.Errorf("after Enter (first time): screen=%d, want screenNickname(%d)", m2.screen, screenNickname)
|
||||
}
|
||||
if m2.playerName == "" {
|
||||
|
||||
// Type a name
|
||||
for _, ch := range []rune("Hero") {
|
||||
result, _ = m2.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{ch}})
|
||||
m2 = result.(Model)
|
||||
}
|
||||
|
||||
// Confirm nickname
|
||||
result, _ = m2.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||
m3 := result.(Model)
|
||||
|
||||
if m3.screen != screenLobby {
|
||||
t.Errorf("after nickname Enter: screen=%d, want screenLobby(1)", m3.screen)
|
||||
}
|
||||
if m3.playerName == "" {
|
||||
t.Error("playerName should be set")
|
||||
}
|
||||
}
|
||||
@@ -45,12 +59,20 @@ func TestLobbyCreateRoom(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer func() { db.Close(); os.Remove("test_ui.db") }()
|
||||
|
||||
m := NewModel(80, 24, "testfp", lobby, db)
|
||||
m := NewModel(80, 24, "testfp2", lobby, db)
|
||||
|
||||
// Go to lobby
|
||||
// Go to nickname screen (first-time player)
|
||||
result, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||
m2 := result.(Model)
|
||||
|
||||
// Type name and confirm
|
||||
for _, ch := range []rune("Hero") {
|
||||
result, _ = m2.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{ch}})
|
||||
m2 = result.(Model)
|
||||
}
|
||||
result, _ = m2.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||
m2 = result.(Model)
|
||||
|
||||
// Press 'c' to create room
|
||||
result, _ = m2.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'c'}})
|
||||
m3 := result.(Model)
|
||||
@@ -68,11 +90,19 @@ func TestClassSelectToGame(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer func() { db.Close(); os.Remove("test_ui.db") }()
|
||||
|
||||
m := NewModel(80, 24, "testfp", lobby, db)
|
||||
m := NewModel(80, 24, "testfp3", lobby, db)
|
||||
|
||||
// Title -> Lobby -> Class Select -> Game
|
||||
// Title -> Nickname -> Lobby
|
||||
result, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||
m2 := result.(Model)
|
||||
for _, ch := range []rune("Hero") {
|
||||
result, _ = m2.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{ch}})
|
||||
m2 = result.(Model)
|
||||
}
|
||||
result, _ = m2.Update(tea.KeyMsg{Type: tea.KeyEnter})
|
||||
m2 = result.(Model)
|
||||
|
||||
// Lobby -> Class Select
|
||||
result, _ = m2.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'c'}})
|
||||
m3 := result.(Model)
|
||||
|
||||
|
||||
31
ui/nickname_view.go
Normal file
31
ui/nickname_view.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func renderNickname(input string, width, height int) string {
|
||||
title := styleHeader.Render("── Enter Your Name ──")
|
||||
|
||||
display := input
|
||||
if display == "" {
|
||||
display = strings.Repeat("_", 12)
|
||||
} else {
|
||||
display = input + "_"
|
||||
}
|
||||
|
||||
inputBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(colorCyan).
|
||||
Padding(0, 2).
|
||||
Render(stylePlayer.Render(display))
|
||||
|
||||
hint := styleSystem.Render(fmt.Sprintf("(%d/12 characters)", len(input)))
|
||||
footer := styleAction.Render("[Enter] Confirm [Esc] Cancel")
|
||||
|
||||
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center,
|
||||
lipgloss.JoinVertical(lipgloss.Center, title, "", inputBox, hint, "", footer))
|
||||
}
|
||||
Reference in New Issue
Block a user