From a1e9e0ef681605bf1d22748e5b1be03b1e0390b9 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 24 Mar 2026 00:52:58 +0900 Subject: [PATCH] feat: lobby join-by-code with J key and 4-char input Co-Authored-By: Claude Sonnet 4.6 --- ui/lobby_view.go | 19 ++++++++++++------ ui/model.go | 51 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/ui/lobby_view.go b/ui/lobby_view.go index 3c53a13..df822df 100644 --- a/ui/lobby_view.go +++ b/ui/lobby_view.go @@ -2,16 +2,19 @@ package ui import ( "fmt" + "strings" "github.com/charmbracelet/lipgloss" ) type lobbyState struct { - rooms []roomInfo - input string - cursor int - creating bool - roomName string + rooms []roomInfo + input string + cursor int + creating bool + roomName string + joining bool + codeInput string } type roomInfo struct { @@ -31,7 +34,7 @@ func renderLobby(state lobbyState, width, height int) string { Padding(0, 1) 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 := "" for i, r := range state.rooms { @@ -45,6 +48,10 @@ func renderLobby(state lobbyState, width, height int) string { if roomList == "" { 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, header, diff --git a/ui/model.go b/ui/model.go index e5a6307..18862c1 100644 --- a/ui/model.go +++ b/ui/model.go @@ -38,12 +38,13 @@ type Model struct { store *store.DB // Per-session state - session *game.GameSession - roomCode string - gameState game.GameState - lobbyState lobbyState - classState classSelectState - inputBuffer string + session *game.GameSession + roomCode string + gameState game.GameState + lobbyState lobbyState + classState classSelectState + inputBuffer string + targetCursor int } 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: return renderClassSelect(m.classState, m.width, m.height) case screenGame: - return renderGame(m.gameState, m.width, m.height) + return renderGame(m.gameState, m.width, m.height, m.targetCursor) case screenShop: return renderShop(m.gameState, m.width, m.height) 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) { 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 m.lobby != nil { 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.screen = screenClassSelect } + } else if isKey(key, "j") { + m.lobbyState.joining = true + m.lobbyState.codeInput = "" } else if isUp(key) { if m.lobbyState.cursor > 0 { m.lobbyState.cursor-- @@ -306,12 +333,18 @@ func (m Model) updateGame(msg tea.Msg) (tea.Model, tea.Cmd) { if isPlayerDead { 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 { switch key.String() { 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": - 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": m.session.SubmitAction(m.playerName, game.PlayerAction{Type: game.ActionItem}) case "4":