refactor: extract all screens from model.go into Screen implementations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
718
ui/model.go
718
ui/model.go
@@ -2,61 +2,22 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/tolelom/catacombs/entity"
|
||||
"github.com/tolelom/catacombs/game"
|
||||
"github.com/tolelom/catacombs/store"
|
||||
)
|
||||
|
||||
type screen int
|
||||
|
||||
const (
|
||||
screenTitle screen = iota
|
||||
screenLobby
|
||||
screenClassSelect
|
||||
screenGame
|
||||
screenShop
|
||||
screenResult
|
||||
screenHelp
|
||||
screenStats
|
||||
screenAchievements
|
||||
screenLeaderboard
|
||||
screenNickname
|
||||
)
|
||||
|
||||
// StateUpdateMsg is sent by GameSession to update the view
|
||||
type StateUpdateMsg struct {
|
||||
State game.GameState
|
||||
}
|
||||
|
||||
type tickMsg struct{}
|
||||
|
||||
type Model struct {
|
||||
width int
|
||||
height int
|
||||
fingerprint string
|
||||
playerName string
|
||||
screen screen
|
||||
|
||||
// Shared references (set by server)
|
||||
lobby *game.Lobby
|
||||
store *store.DB
|
||||
|
||||
// Per-session state
|
||||
session *game.GameSession
|
||||
roomCode string
|
||||
gameState game.GameState
|
||||
lobbyState lobbyState
|
||||
classState classSelectState
|
||||
inputBuffer string
|
||||
targetCursor int
|
||||
moveCursor int // selected neighbor index during exploration
|
||||
chatting bool
|
||||
chatInput string
|
||||
rankingSaved bool
|
||||
shopMsg string
|
||||
nicknameInput string
|
||||
currentScreen Screen
|
||||
ctx *Context
|
||||
}
|
||||
|
||||
func NewModel(width, height int, fingerprint string, lobby *game.Lobby, db *store.DB) Model {
|
||||
@@ -66,13 +27,26 @@ func NewModel(width, height int, fingerprint string, lobby *game.Lobby, db *stor
|
||||
if height == 0 {
|
||||
height = 24
|
||||
}
|
||||
ctx := &Context{
|
||||
Width: width,
|
||||
Height: height,
|
||||
Fingerprint: fingerprint,
|
||||
Lobby: lobby,
|
||||
Store: db,
|
||||
}
|
||||
|
||||
// Determine initial screen
|
||||
var initialScreen Screen
|
||||
if fingerprint != "" && db != nil {
|
||||
if name, err := db.GetProfile(fingerprint); err == nil {
|
||||
ctx.PlayerName = name
|
||||
}
|
||||
}
|
||||
initialScreen = NewTitleScreen()
|
||||
|
||||
return Model{
|
||||
width: width,
|
||||
height: height,
|
||||
fingerprint: fingerprint,
|
||||
screen: screenTitle,
|
||||
lobby: lobby,
|
||||
store: db,
|
||||
currentScreen: initialScreen,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,95 +57,35 @@ func (m Model) Init() tea.Cmd {
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
if m.width == 0 {
|
||||
m.width = 80
|
||||
m.ctx.Width = msg.Width
|
||||
m.ctx.Height = msg.Height
|
||||
if m.ctx.Width == 0 {
|
||||
m.ctx.Width = 80
|
||||
}
|
||||
if m.height == 0 {
|
||||
m.height = 24
|
||||
if m.ctx.Height == 0 {
|
||||
m.ctx.Height = 24
|
||||
}
|
||||
return m, nil
|
||||
case StateUpdateMsg:
|
||||
m.gameState = msg.State
|
||||
if gs, ok := m.currentScreen.(*GameScreen); ok {
|
||||
gs.gameState = msg.State
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch m.screen {
|
||||
case screenTitle:
|
||||
return m.updateTitle(msg)
|
||||
case screenLobby:
|
||||
return m.updateLobby(msg)
|
||||
case screenClassSelect:
|
||||
return m.updateClassSelect(msg)
|
||||
case screenGame:
|
||||
return m.updateGame(msg)
|
||||
case screenShop:
|
||||
return m.updateShop(msg)
|
||||
case screenResult:
|
||||
return m.updateResult(msg)
|
||||
case screenHelp:
|
||||
return m.updateHelp(msg)
|
||||
case screenStats:
|
||||
return m.updateStats(msg)
|
||||
case screenAchievements:
|
||||
return m.updateAchievements(msg)
|
||||
case screenLeaderboard:
|
||||
return m.updateLeaderboard(msg)
|
||||
case screenNickname:
|
||||
return m.updateNickname(msg)
|
||||
}
|
||||
return m, nil
|
||||
next, cmd := m.currentScreen.Update(msg, m.ctx)
|
||||
m.currentScreen = next
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
if m.width < 80 || m.height < 24 {
|
||||
return fmt.Sprintf("Terminal too small (%dx%d). Minimum: 80x24.", m.width, m.height)
|
||||
if m.ctx.Width < 80 || m.ctx.Height < 24 {
|
||||
return fmt.Sprintf("Terminal too small (%dx%d). Minimum: 80x24.", m.ctx.Width, m.ctx.Height)
|
||||
}
|
||||
switch m.screen {
|
||||
case screenTitle:
|
||||
return renderTitle(m.width, m.height)
|
||||
case screenLobby:
|
||||
return renderLobby(m.lobbyState, m.width, m.height)
|
||||
case screenClassSelect:
|
||||
return renderClassSelect(m.classState, m.width, m.height)
|
||||
case screenGame:
|
||||
return renderGame(m.gameState, m.width, m.height, m.targetCursor, m.moveCursor, m.chatting, m.chatInput)
|
||||
case screenShop:
|
||||
return renderShop(m.gameState, m.width, m.height, m.shopMsg)
|
||||
case screenResult:
|
||||
var rankings []store.RunRecord
|
||||
if m.store != nil {
|
||||
rankings, _ = m.store.TopRuns(10)
|
||||
}
|
||||
return renderResult(m.gameState, rankings)
|
||||
case screenHelp:
|
||||
return renderHelp(m.width, m.height)
|
||||
case screenStats:
|
||||
var stats store.PlayerStats
|
||||
if m.store != nil {
|
||||
stats, _ = m.store.GetStats(m.playerName)
|
||||
}
|
||||
return renderStats(m.playerName, stats, m.width, m.height)
|
||||
case screenAchievements:
|
||||
var achievements []store.Achievement
|
||||
if m.store != nil {
|
||||
achievements, _ = m.store.GetAchievements(m.playerName)
|
||||
}
|
||||
return renderAchievements(m.playerName, achievements, m.width, m.height)
|
||||
case screenLeaderboard:
|
||||
var byFloor, byGold []store.RunRecord
|
||||
if m.store != nil {
|
||||
byFloor, _ = m.store.TopRuns(10)
|
||||
byGold, _ = m.store.TopRunsByGold(10)
|
||||
}
|
||||
return renderLeaderboard(byFloor, byGold, m.width, m.height)
|
||||
case screenNickname:
|
||||
return renderNickname(m.nicknameInput, m.width, m.height)
|
||||
}
|
||||
return ""
|
||||
return m.currentScreen.View(m.ctx)
|
||||
}
|
||||
|
||||
// Key helper functions used by all screens.
|
||||
func isKey(key tea.KeyMsg, names ...string) bool {
|
||||
s := key.String()
|
||||
for _, n := range names {
|
||||
@@ -198,515 +112,63 @@ func isDown(key tea.KeyMsg) bool {
|
||||
return isKey(key, "down") || key.Type == tea.KeyDown
|
||||
}
|
||||
|
||||
func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
if isEnter(key) {
|
||||
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") {
|
||||
m.screen = screenHelp
|
||||
} else if isKey(key, "s") {
|
||||
m.screen = screenStats
|
||||
} else if isKey(key, "a") {
|
||||
m.screen = screenAchievements
|
||||
} else if isKey(key, "l") {
|
||||
m.screen = screenLeaderboard
|
||||
} else if isQuit(key) {
|
||||
return m, tea.Quit
|
||||
}
|
||||
// Keep these for backward compatibility with tests
|
||||
// screen enum kept temporarily for test compatibility
|
||||
type screen int
|
||||
|
||||
const (
|
||||
screenTitle screen = iota
|
||||
screenLobby
|
||||
screenClassSelect
|
||||
screenGame
|
||||
screenShop
|
||||
screenResult
|
||||
screenHelp
|
||||
screenStats
|
||||
screenAchievements
|
||||
screenLeaderboard
|
||||
screenNickname
|
||||
)
|
||||
|
||||
// screenType returns the screen enum for the current screen (for test compatibility).
|
||||
func (m Model) screenType() screen {
|
||||
switch m.currentScreen.(type) {
|
||||
case *TitleScreen:
|
||||
return screenTitle
|
||||
case *LobbyScreen:
|
||||
return screenLobby
|
||||
case *ClassSelectScreen:
|
||||
return screenClassSelect
|
||||
case *GameScreen:
|
||||
return screenGame
|
||||
case *ShopScreen:
|
||||
return screenShop
|
||||
case *ResultScreen:
|
||||
return screenResult
|
||||
case *HelpScreen:
|
||||
return screenHelp
|
||||
case *StatsScreen:
|
||||
return screenStats
|
||||
case *AchievementsScreen:
|
||||
return screenAchievements
|
||||
case *LeaderboardScreen:
|
||||
return screenLeaderboard
|
||||
case *NicknameScreen:
|
||||
return screenNickname
|
||||
}
|
||||
return m, nil
|
||||
return screenTitle
|
||||
}
|
||||
|
||||
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
|
||||
// Convenience accessors for test compatibility
|
||||
func (m Model) playerName() string {
|
||||
return m.ctx.PlayerName
|
||||
}
|
||||
|
||||
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) {
|
||||
m.screen = screenTitle
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
func (m Model) roomCode() string {
|
||||
return m.ctx.RoomCode
|
||||
}
|
||||
|
||||
func (m Model) updateAchievements(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
if isKey(key, "a") || isEnter(key) || isQuit(key) {
|
||||
m.screen = screenTitle
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
func (m Model) session() *game.GameSession {
|
||||
return m.ctx.Session
|
||||
}
|
||||
|
||||
func (m Model) updateLeaderboard(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
if isKey(key, "l") || isEnter(key) || isQuit(key) {
|
||||
m.screen = screenTitle
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateHelp(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
if isKey(key, "h") || isEnter(key) || isQuit(key) {
|
||||
m.screen = screenTitle
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
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, m.fingerprint); 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")
|
||||
m.lobby.JoinRoom(code, m.playerName, m.fingerprint)
|
||||
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--
|
||||
}
|
||||
} else if isDown(key) {
|
||||
if m.lobbyState.cursor < len(m.lobbyState.rooms)-1 {
|
||||
m.lobbyState.cursor++
|
||||
}
|
||||
} else if isEnter(key) {
|
||||
if m.lobby != nil && len(m.lobbyState.rooms) > 0 {
|
||||
r := m.lobbyState.rooms[m.lobbyState.cursor]
|
||||
if err := m.lobby.JoinRoom(r.Code, m.playerName, m.fingerprint); err == nil {
|
||||
m.roomCode = r.Code
|
||||
m.screen = screenClassSelect
|
||||
}
|
||||
}
|
||||
} else if isKey(key, "q") {
|
||||
if m.lobby != nil {
|
||||
m.lobby.PlayerOffline(m.fingerprint)
|
||||
}
|
||||
m.screen = screenTitle
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateClassSelect(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
if isUp(key) {
|
||||
if m.classState.cursor > 0 {
|
||||
m.classState.cursor--
|
||||
}
|
||||
} else if isDown(key) {
|
||||
if m.classState.cursor < len(classOptions)-1 {
|
||||
m.classState.cursor++
|
||||
}
|
||||
} else if isEnter(key) {
|
||||
if m.lobby != nil {
|
||||
selectedClass := classOptions[m.classState.cursor].class
|
||||
m.lobby.SetPlayerClass(m.roomCode, m.fingerprint, selectedClass.String())
|
||||
room := m.lobby.GetRoom(m.roomCode)
|
||||
if room != nil {
|
||||
if room.Session == nil {
|
||||
room.Session = game.NewGameSession(m.lobby.Cfg())
|
||||
}
|
||||
m.session = room.Session
|
||||
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()
|
||||
m.screen = screenGame
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// pollState returns a Cmd that waits briefly then refreshes game state
|
||||
func (m Model) pollState() tea.Cmd {
|
||||
return tea.Tick(time.Millisecond*200, func(t time.Time) tea.Msg {
|
||||
return tickMsg{}
|
||||
})
|
||||
}
|
||||
|
||||
type tickMsg struct{}
|
||||
|
||||
func (m Model) updateGame(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if m.session != nil && m.fingerprint != "" {
|
||||
m.session.TouchActivity(m.fingerprint)
|
||||
}
|
||||
// Refresh state on every update
|
||||
if m.session != nil {
|
||||
m.gameState = m.session.GetState()
|
||||
// Clamp target cursor to valid range after monsters die
|
||||
if len(m.gameState.Monsters) > 0 {
|
||||
if m.targetCursor >= len(m.gameState.Monsters) {
|
||||
m.targetCursor = len(m.gameState.Monsters) - 1
|
||||
}
|
||||
} else {
|
||||
m.targetCursor = 0
|
||||
}
|
||||
}
|
||||
|
||||
if m.gameState.GameOver {
|
||||
if m.store != nil && !m.rankingSaved {
|
||||
score := 0
|
||||
for _, p := range m.gameState.Players {
|
||||
score += p.Gold
|
||||
}
|
||||
// Find the current player's class
|
||||
playerClass := ""
|
||||
for _, p := range m.gameState.Players {
|
||||
if p.Fingerprint == m.fingerprint {
|
||||
playerClass = p.Class.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
m.store.SaveRun(m.playerName, m.gameState.FloorNum, score, playerClass)
|
||||
// Check achievements
|
||||
if m.gameState.FloorNum >= 5 {
|
||||
m.store.UnlockAchievement(m.playerName, "first_clear")
|
||||
}
|
||||
if m.gameState.FloorNum >= 10 {
|
||||
m.store.UnlockAchievement(m.playerName, "floor10")
|
||||
}
|
||||
if m.gameState.Victory {
|
||||
m.store.UnlockAchievement(m.playerName, "floor20")
|
||||
}
|
||||
if m.gameState.SoloMode && m.gameState.FloorNum >= 5 {
|
||||
m.store.UnlockAchievement(m.playerName, "solo_clear")
|
||||
}
|
||||
if m.gameState.BossKilled {
|
||||
m.store.UnlockAchievement(m.playerName, "boss_slayer")
|
||||
}
|
||||
if m.gameState.FleeSucceeded {
|
||||
m.store.UnlockAchievement(m.playerName, "flee_master")
|
||||
}
|
||||
for _, p := range m.gameState.Players {
|
||||
if p.Gold >= 200 {
|
||||
m.store.UnlockAchievement(p.Name, "gold_hoarder")
|
||||
}
|
||||
if len(p.Relics) >= 3 {
|
||||
m.store.UnlockAchievement(p.Name, "relic_collector")
|
||||
}
|
||||
}
|
||||
if len(m.gameState.Players) >= 4 {
|
||||
m.store.UnlockAchievement(m.playerName, "full_party")
|
||||
}
|
||||
m.rankingSaved = true
|
||||
}
|
||||
m.screen = screenResult
|
||||
return m, nil
|
||||
}
|
||||
if m.gameState.Phase == game.PhaseShop {
|
||||
m.screen = screenShop
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch msg.(type) {
|
||||
case tickMsg:
|
||||
if m.session != nil {
|
||||
m.session.RevealNextLog()
|
||||
}
|
||||
// Keep polling during combat or while there are pending logs to reveal
|
||||
if m.gameState.Phase == game.PhaseCombat {
|
||||
return m, m.pollState()
|
||||
}
|
||||
if len(m.gameState.PendingLogs) > 0 {
|
||||
return m, m.pollState()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
// Chat mode
|
||||
if m.chatting {
|
||||
if isEnter(key) && len(m.chatInput) > 0 {
|
||||
if m.session != nil {
|
||||
m.session.SendChat(m.playerName, m.chatInput)
|
||||
m.gameState = m.session.GetState()
|
||||
}
|
||||
m.chatting = false
|
||||
m.chatInput = ""
|
||||
} else if isKey(key, "esc") || key.Type == tea.KeyEsc {
|
||||
m.chatting = false
|
||||
m.chatInput = ""
|
||||
} else if key.Type == tea.KeyBackspace && len(m.chatInput) > 0 {
|
||||
m.chatInput = m.chatInput[:len(m.chatInput)-1]
|
||||
} else if len(key.Runes) == 1 && len(m.chatInput) < 40 {
|
||||
m.chatInput += string(key.Runes)
|
||||
}
|
||||
if m.gameState.Phase == game.PhaseCombat {
|
||||
return m, m.pollState()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if isKey(key, "/") {
|
||||
m.chatting = true
|
||||
m.chatInput = ""
|
||||
if m.gameState.Phase == game.PhaseCombat {
|
||||
return m, m.pollState()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch m.gameState.Phase {
|
||||
case game.PhaseExploring:
|
||||
// Dead players can only observe, not move
|
||||
for _, p := range m.gameState.Players {
|
||||
if p.Fingerprint == m.fingerprint && p.IsDead() {
|
||||
if isQuit(key) {
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
neighbors := m.getNeighbors()
|
||||
if isUp(key) {
|
||||
if m.moveCursor > 0 {
|
||||
m.moveCursor--
|
||||
}
|
||||
} else if isDown(key) {
|
||||
if m.moveCursor < len(neighbors)-1 {
|
||||
m.moveCursor++
|
||||
}
|
||||
} else if isEnter(key) {
|
||||
if m.session != nil && len(neighbors) > 0 {
|
||||
roomIdx := neighbors[m.moveCursor]
|
||||
m.session.EnterRoom(roomIdx)
|
||||
m.gameState = m.session.GetState()
|
||||
m.moveCursor = 0
|
||||
if m.gameState.Phase == game.PhaseCombat {
|
||||
return m, m.pollState()
|
||||
}
|
||||
}
|
||||
} else if isQuit(key) {
|
||||
return m, tea.Quit
|
||||
}
|
||||
case game.PhaseCombat:
|
||||
isPlayerDead := false
|
||||
for _, p := range m.gameState.Players {
|
||||
if p.Fingerprint == m.fingerprint && p.IsDead() {
|
||||
isPlayerDead = true
|
||||
break
|
||||
}
|
||||
}
|
||||
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.fingerprint, game.PlayerAction{Type: game.ActionAttack, TargetIdx: m.targetCursor})
|
||||
case "2":
|
||||
m.session.SubmitAction(m.fingerprint, game.PlayerAction{Type: game.ActionSkill, TargetIdx: m.targetCursor})
|
||||
case "3":
|
||||
m.session.SubmitAction(m.fingerprint, game.PlayerAction{Type: game.ActionItem})
|
||||
case "4":
|
||||
m.session.SubmitAction(m.fingerprint, game.PlayerAction{Type: game.ActionFlee})
|
||||
case "5":
|
||||
m.session.SubmitAction(m.fingerprint, game.PlayerAction{Type: game.ActionWait})
|
||||
}
|
||||
// After submitting, poll for turn resolution
|
||||
return m, m.pollState()
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) getNeighbors() []int {
|
||||
if m.gameState.Floor == nil {
|
||||
return nil
|
||||
}
|
||||
cur := m.gameState.Floor.CurrentRoom
|
||||
if cur < 0 || cur >= len(m.gameState.Floor.Rooms) {
|
||||
return nil
|
||||
}
|
||||
return m.gameState.Floor.Rooms[cur].Neighbors
|
||||
}
|
||||
|
||||
func (m Model) updateShop(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if key, ok := msg.(tea.KeyMsg); ok {
|
||||
switch key.String() {
|
||||
case "1", "2", "3":
|
||||
if m.session != nil {
|
||||
idx := int(key.String()[0] - '1')
|
||||
if m.session.BuyItem(m.fingerprint, idx) {
|
||||
m.shopMsg = "Purchased!"
|
||||
} else {
|
||||
m.shopMsg = "Not enough gold!"
|
||||
}
|
||||
m.gameState = m.session.GetState()
|
||||
}
|
||||
case "q":
|
||||
if m.session != nil {
|
||||
m.session.LeaveShop()
|
||||
m.gameState = m.session.GetState()
|
||||
m.screen = screenGame
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if m.lobby != nil && m.roomCode != "" {
|
||||
m.lobby.RemoveRoom(m.roomCode)
|
||||
}
|
||||
m.roomCode = ""
|
||||
m.rankingSaved = false
|
||||
m.screen = screenLobby
|
||||
m = m.withRefreshedLobby()
|
||||
} else if isQuit(key) {
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) withRefreshedLobby() Model {
|
||||
if m.lobby == nil {
|
||||
return m
|
||||
}
|
||||
rooms := m.lobby.ListRooms()
|
||||
m.lobbyState.rooms = make([]roomInfo, len(rooms))
|
||||
for i, r := range rooms {
|
||||
status := "Waiting"
|
||||
if r.Status == game.RoomPlaying {
|
||||
status = "Playing"
|
||||
}
|
||||
players := make([]playerInfo, len(r.Players))
|
||||
for j, p := range r.Players {
|
||||
players[j] = playerInfo{Name: p.Name, Class: p.Class, Ready: p.Ready}
|
||||
}
|
||||
m.lobbyState.rooms[i] = roomInfo{
|
||||
Code: r.Code,
|
||||
Name: r.Name,
|
||||
Players: players,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
m.lobbyState.online = len(m.lobby.ListOnline())
|
||||
m.lobbyState.cursor = 0
|
||||
return m
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user