Files
Catacombs/ui/model.go
tolelom 1563091de1 fix: 13 bugs found via systematic code review and testing
Multiplayer:
- Add WaitingScreen between class select and game start; previously
  selecting a class immediately started the game and locked the room,
  preventing other players from joining
- Add periodic lobby room list refresh (2s interval)
- Add LeaveRoom method for backing out of waiting room

Combat & mechanics:
- Mark invalid attack targets with TargetIdx=-1 to suppress misleading
  "0 dmg" combat log entries
- Make Freeze effect actually skip frozen player's action (was purely
  cosmetic before - expired during tick before action processing)
- Implement Life Siphon relic heal-on-damage effect (was defined but
  never applied in combat)
- Fix combo matching to track used actions and prevent reuse

Game modes:
- Wire up weekly mutations to GameSession via ApplyWeeklyMutation()
- Implement 3 mutation runtime effects: no_shop, glass_cannon, elite_flood
- Pass HardMode toggle from lobby UI through Context to GameSession
- Apply HardMode difficulty multipliers (1.5x monsters, 2x shop, 0.5x heal)

Polish:
- Set starting room (index 0) to always be Empty (safe start)
- Distinguish shop purchase errors: "Not enough gold" vs "Inventory full"
- Record random events in codex for discovery tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 20:45:56 +09:00

168 lines
3.2 KiB
Go

package ui
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/tolelom/catacombs/game"
"github.com/tolelom/catacombs/store"
)
type tickMsg struct{}
type Model struct {
currentScreen Screen
ctx *Context
}
func NewModel(width, height int, fingerprint string, lobby *game.Lobby, db *store.DB) Model {
if width == 0 {
width = 80
}
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{
currentScreen: initialScreen,
ctx: ctx,
}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.ctx.Width = msg.Width
m.ctx.Height = msg.Height
if m.ctx.Width == 0 {
m.ctx.Width = 80
}
if m.ctx.Height == 0 {
m.ctx.Height = 24
}
return m, nil
}
next, cmd := m.currentScreen.Update(msg, m.ctx)
m.currentScreen = next
return m, cmd
}
func (m Model) View() string {
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)
}
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 {
if s == n {
return true
}
}
return false
}
func isEnter(key tea.KeyMsg) bool {
return isKey(key, "enter") || key.Type == tea.KeyEnter
}
func isQuit(key tea.KeyMsg) bool {
return isKey(key, "q", "ctrl+c") || key.Type == tea.KeyCtrlC
}
func isUp(key tea.KeyMsg) bool {
return isKey(key, "up") || key.Type == tea.KeyUp
}
func isDown(key tea.KeyMsg) bool {
return isKey(key, "down") || key.Type == tea.KeyDown
}
// Keep these for backward compatibility with tests
// screen enum kept temporarily for test compatibility
type screen int
const (
screenTitle screen = iota
screenLobby
screenClassSelect
screenWaiting
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 *WaitingScreen:
return screenWaiting
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 screenTitle
}
// Convenience accessors for test compatibility
func (m Model) playerName() string {
return m.ctx.PlayerName
}
func (m Model) roomCode() string {
return m.ctx.RoomCode
}
func (m Model) session() *game.GameSession {
return m.ctx.Session
}