feat: lobby join-by-code with J key and 4-char input

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 00:52:58 +09:00
parent 6809e49226
commit a1e9e0ef68
2 changed files with 55 additions and 15 deletions

View File

@@ -2,16 +2,19 @@ package ui
import ( import (
"fmt" "fmt"
"strings"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
type lobbyState struct { type lobbyState struct {
rooms []roomInfo rooms []roomInfo
input string input string
cursor int cursor int
creating bool creating bool
roomName string roomName string
joining bool
codeInput string
} }
type roomInfo struct { type roomInfo struct {
@@ -31,7 +34,7 @@ func renderLobby(state lobbyState, width, height int) string {
Padding(0, 1) Padding(0, 1)
header := headerStyle.Render("── Lobby ──") header := headerStyle.Render("── Lobby ──")
menu := "[C] Create Room [J] Join by Code [Q] Back" menu := "[C] Create Room [J] Join by Code [Up/Down] Select [Enter] Join [Q] Back"
roomList := "" roomList := ""
for i, r := range state.rooms { for i, r := range state.rooms {
@@ -45,6 +48,10 @@ func renderLobby(state lobbyState, width, height int) string {
if roomList == "" { if roomList == "" {
roomList = " No rooms available. Create one!" roomList = " No rooms available. Create one!"
} }
if state.joining {
inputStr := state.codeInput + strings.Repeat("_", 4-len(state.codeInput))
roomList += fmt.Sprintf("\n Enter room code: [%s] (Esc to cancel)\n", inputStr)
}
return lipgloss.JoinVertical(lipgloss.Left, return lipgloss.JoinVertical(lipgloss.Left,
header, header,

View File

@@ -38,12 +38,13 @@ type Model struct {
store *store.DB store *store.DB
// Per-session state // Per-session state
session *game.GameSession session *game.GameSession
roomCode string roomCode string
gameState game.GameState gameState game.GameState
lobbyState lobbyState lobbyState lobbyState
classState classSelectState classState classSelectState
inputBuffer string inputBuffer string
targetCursor int
} }
func NewModel(width, height int, fingerprint string, lobby *game.Lobby, db *store.DB) Model { func NewModel(width, height int, fingerprint string, lobby *game.Lobby, db *store.DB) Model {
@@ -113,7 +114,7 @@ func (m Model) View() string {
case screenClassSelect: case screenClassSelect:
return renderClassSelect(m.classState, m.width, m.height) return renderClassSelect(m.classState, m.width, m.height)
case screenGame: case screenGame:
return renderGame(m.gameState, m.width, m.height) return renderGame(m.gameState, m.width, m.height, m.targetCursor)
case screenShop: case screenShop:
return renderShop(m.gameState, m.width, m.height) return renderShop(m.gameState, m.width, m.height)
case screenResult: case screenResult:
@@ -179,6 +180,29 @@ func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m Model) updateLobby(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) updateLobby(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok { if key, ok := msg.(tea.KeyMsg); ok {
// Join-by-code input mode
if m.lobbyState.joining {
if isEnter(key) && len(m.lobbyState.codeInput) == 4 {
if m.lobby != nil {
if err := m.lobby.JoinRoom(m.lobbyState.codeInput, m.playerName); err == nil {
m.roomCode = m.lobbyState.codeInput
m.screen = screenClassSelect
}
}
m.lobbyState.joining = false
m.lobbyState.codeInput = ""
} else if isKey(key, "esc") || key.Type == tea.KeyEsc {
m.lobbyState.joining = false
m.lobbyState.codeInput = ""
} else if key.Type == tea.KeyBackspace && len(m.lobbyState.codeInput) > 0 {
m.lobbyState.codeInput = m.lobbyState.codeInput[:len(m.lobbyState.codeInput)-1]
} else if len(key.Runes) == 1 && len(m.lobbyState.codeInput) < 4 {
ch := strings.ToUpper(string(key.Runes))
m.lobbyState.codeInput += ch
}
return m, nil
}
// Normal lobby key handling
if isKey(key, "c") { if isKey(key, "c") {
if m.lobby != nil { if m.lobby != nil {
code := m.lobby.CreateRoom(m.playerName + "'s Room") code := m.lobby.CreateRoom(m.playerName + "'s Room")
@@ -186,6 +210,9 @@ func (m Model) updateLobby(msg tea.Msg) (tea.Model, tea.Cmd) {
m.roomCode = code m.roomCode = code
m.screen = screenClassSelect m.screen = screenClassSelect
} }
} else if isKey(key, "j") {
m.lobbyState.joining = true
m.lobbyState.codeInput = ""
} else if isUp(key) { } else if isUp(key) {
if m.lobbyState.cursor > 0 { if m.lobbyState.cursor > 0 {
m.lobbyState.cursor-- m.lobbyState.cursor--
@@ -306,12 +333,18 @@ func (m Model) updateGame(msg tea.Msg) (tea.Model, tea.Cmd) {
if isPlayerDead { if isPlayerDead {
return m, m.pollState() return m, m.pollState()
} }
if isKey(key, "tab") || key.Type == tea.KeyTab {
if len(m.gameState.Monsters) > 0 {
m.targetCursor = (m.targetCursor + 1) % len(m.gameState.Monsters)
}
return m, m.pollState()
}
if m.session != nil { if m.session != nil {
switch key.String() { switch key.String() {
case "1": case "1":
m.session.SubmitAction(m.playerName, game.PlayerAction{Type: game.ActionAttack, TargetIdx: 0}) m.session.SubmitAction(m.playerName, game.PlayerAction{Type: game.ActionAttack, TargetIdx: m.targetCursor})
case "2": case "2":
m.session.SubmitAction(m.playerName, game.PlayerAction{Type: game.ActionSkill, TargetIdx: 0}) m.session.SubmitAction(m.playerName, game.PlayerAction{Type: game.ActionSkill, TargetIdx: m.targetCursor})
case "3": case "3":
m.session.SubmitAction(m.playerName, game.PlayerAction{Type: game.ActionItem}) m.session.SubmitAction(m.playerName, game.PlayerAction{Type: game.ActionItem})
case "4": case "4":