feat: add codex UI screen with completion tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
169
ui/codex_view.go
Normal file
169
ui/codex_view.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -59,6 +59,8 @@ func (s *TitleScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) {
|
|||||||
return NewAchievementsScreen(), nil
|
return NewAchievementsScreen(), nil
|
||||||
} else if isKey(key, "l") {
|
} else if isKey(key, "l") {
|
||||||
return NewLeaderboardScreen(), nil
|
return NewLeaderboardScreen(), nil
|
||||||
|
} else if isKey(key, "c") {
|
||||||
|
return NewCodexScreen(ctx), nil
|
||||||
} else if isQuit(key) {
|
} else if isQuit(key) {
|
||||||
return s, tea.Quit
|
return s, tea.Quit
|
||||||
}
|
}
|
||||||
@@ -108,7 +110,7 @@ func renderTitle(width, height int) string {
|
|||||||
menu := lipgloss.NewStyle().
|
menu := lipgloss.NewStyle().
|
||||||
Foreground(colorWhite).
|
Foreground(colorWhite).
|
||||||
Bold(true).
|
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,
|
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
logo,
|
logo,
|
||||||
|
|||||||
Reference in New Issue
Block a user