From cf37eef1b18d36c97f3126ed4e02ebda06406474 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 25 Mar 2026 16:39:08 +0900 Subject: [PATCH] feat: add codex UI screen with completion tracking Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/codex_view.go | 169 +++++++++++++++++++++++++++++++++++++++++++++++ ui/title.go | 4 +- 2 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 ui/codex_view.go diff --git a/ui/codex_view.go b/ui/codex_view.go new file mode 100644 index 0000000..f9ce5e0 --- /dev/null +++ b/ui/codex_view.go @@ -0,0 +1,169 @@ +package ui + +import ( + "fmt" + "sort" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/tolelom/catacombs/store" +) + +// Total known entries for completion calculation +const ( + totalMonsters = 16 // 8 regular + 4 bosses + 4 mini-bosses + totalItems = 15 // weapons + armor + potions + relics + totalEvents = 8 // random events from game/random_event.go +) + +// All known entry names for display +var allMonsters = []string{ + "Goblin", "Skeleton", "Bat", "Slime", "Zombie", "Spider", "Rat", "Ghost", + "Dragon", "Lich", "Demon Lord", "Hydra", + "Troll", "Wraith", "Golem", "Minotaur", +} + +var allItems = []string{ + "Iron Sword", "Steel Axe", "Magic Staff", "Shadow Dagger", "Holy Mace", + "Leather Armor", "Chain Mail", "Plate Armor", + "Health Potion", "Mana Potion", "Strength Potion", + "Shield Relic", "Amulet of Life", "Ring of Power", "Boots of Speed", +} + +var allEvents = []string{ + "altar", "fountain", "merchant", "trap_room", + "shrine", "chest", "ghost", "mushroom", +} + +// CodexScreen displays the player's codex with discovered entries. +type CodexScreen struct { + codex store.Codex + tab int // 0=monsters, 1=items, 2=events +} + +func NewCodexScreen(ctx *Context) *CodexScreen { + var codex store.Codex + if ctx.Store != nil { + codex, _ = ctx.Store.GetCodex(ctx.Fingerprint) + } else { + codex = store.Codex{ + Monsters: make(map[string]bool), + Items: make(map[string]bool), + Events: make(map[string]bool), + } + } + return &CodexScreen{codex: codex} +} + +func (s *CodexScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) { + if key, ok := msg.(tea.KeyMsg); ok { + if isKey(key, "esc", "c", "q") || key.Type == tea.KeyEsc { + return NewTitleScreen(), nil + } + if key.Type == tea.KeyTab || isKey(key, "right", "l") || key.Type == tea.KeyRight { + s.tab = (s.tab + 1) % 3 + } + if isKey(key, "left", "h") || key.Type == tea.KeyLeft { + s.tab = (s.tab + 2) % 3 + } + } + return s, nil +} + +func (s *CodexScreen) View(ctx *Context) string { + title := styleHeader.Render("-- Codex --") + + // Tab headers + tabNames := []string{"Monsters", "Items", "Events"} + var tabs []string + for i, name := range tabNames { + if i == s.tab { + tabs = append(tabs, lipgloss.NewStyle(). + Foreground(colorYellow).Bold(true). + Render(fmt.Sprintf("[ %s ]", name))) + } else { + tabs = append(tabs, lipgloss.NewStyle(). + Foreground(colorGray). + Render(fmt.Sprintf(" %s ", name))) + } + } + tabBar := lipgloss.JoinHorizontal(lipgloss.Center, tabs...) + + // Entries + var entries string + var discovered map[string]bool + var allNames []string + var total int + + switch s.tab { + case 0: + discovered = s.codex.Monsters + allNames = allMonsters + total = totalMonsters + case 1: + discovered = s.codex.Items + allNames = allItems + total = totalItems + case 2: + discovered = s.codex.Events + allNames = allEvents + total = totalEvents + } + + count := len(discovered) + pct := 0.0 + if total > 0 { + pct = float64(count) / float64(total) * 100 + } + + completion := lipgloss.NewStyle().Foreground(colorCyan). + Render(fmt.Sprintf("Discovered: %d/%d (%.0f%%)", count, total, pct)) + + // Sort discovered keys for consistent display + discoveredKeys := make([]string, 0, len(discovered)) + for k := range discovered { + discoveredKeys = append(discoveredKeys, k) + } + sort.Strings(discoveredKeys) + + // Build a set of discovered for quick lookup + discoveredSet := discovered + + for _, name := range allNames { + if discoveredSet[name] { + entries += fmt.Sprintf(" [x] %s\n", lipgloss.NewStyle().Foreground(colorGreen).Render(name)) + } else { + entries += fmt.Sprintf(" [ ] %s\n", lipgloss.NewStyle().Foreground(colorGray).Render("???")) + } + } + + // Show any discovered entries not in the known list + for _, k := range discoveredKeys { + found := false + for _, name := range allNames { + if name == k { + found = true + break + } + } + if !found { + entries += fmt.Sprintf(" [x] %s\n", lipgloss.NewStyle().Foreground(colorGreen).Render(k)) + } + } + + footer := styleSystem.Render("[Tab/Left/Right] Switch Tab [Esc] Back") + + content := lipgloss.JoinVertical(lipgloss.Center, + title, + "", + tabBar, + "", + completion, + "", + entries, + "", + footer, + ) + + return lipgloss.Place(ctx.Width, ctx.Height, lipgloss.Center, lipgloss.Center, content) +} diff --git a/ui/title.go b/ui/title.go index d0970a1..d2fd40c 100644 --- a/ui/title.go +++ b/ui/title.go @@ -59,6 +59,8 @@ func (s *TitleScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) { return NewAchievementsScreen(), nil } else if isKey(key, "l") { return NewLeaderboardScreen(), nil + } else if isKey(key, "c") { + return NewCodexScreen(ctx), nil } else if isQuit(key) { return s, tea.Quit } @@ -108,7 +110,7 @@ func renderTitle(width, height int) string { menu := lipgloss.NewStyle(). Foreground(colorWhite). Bold(true). - Render("[Enter] Start [H] Help [S] Stats [A] Achievements [L] Leaderboard [Q] Quit") + Render("[Enter] Start [H] Help [S] Stats [A] Achievements [L] Leaderboard [C] Codex [Q] Quit") content := lipgloss.JoinVertical(lipgloss.Center, logo,