From 84431c888a769424e3150dbdd90b448d2ce2c236 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 24 Mar 2026 12:35:20 +0900 Subject: [PATCH] docs: terminal visuals implementation plan 5 tasks: styles, ASCII art, colored log, combat layout, title screen. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-24-terminal-visuals.md | 558 ++++++++++++++++++ 1 file changed, 558 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-24-terminal-visuals.md diff --git a/docs/superpowers/plans/2026-03-24-terminal-visuals.md b/docs/superpowers/plans/2026-03-24-terminal-visuals.md new file mode 100644 index 0000000..9f23d60 --- /dev/null +++ b/docs/superpowers/plans/2026-03-24-terminal-visuals.md @@ -0,0 +1,558 @@ +# Terminal Visuals Enhancement — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Enhance Catacombs terminal visuals — monster ASCII art, combat layout with box-drawing panels, semantic color effects, and improved title screen. + +**Architecture:** New files for styles and ASCII art data. Rewrite of `game_view.go` render functions. All changes are UI-only — no game logic modifications. + +**Tech Stack:** Go, charmbracelet/lipgloss (box-drawing, colors, layout) + +**Spec:** `docs/superpowers/specs/2026-03-24-terminal-visuals-design.md` + +--- + +## File Map + +``` +ui/styles.go — NEW: shared lipgloss style constants +ui/ascii_art.go — NEW: monster ASCII art data + Art() function +ui/game_view.go — REWRITE: combat layout, colored log, HP bar 3-color +ui/title.go — MODIFY: gradient logo, centered layout +``` + +--- + +### Task 1: Shared Styles (ui/styles.go) + +**Files:** +- Create: `ui/styles.go` + +- [ ] **Step 1: Create styles.go with color constants and reusable styles** + +```go +package ui + +import "github.com/charmbracelet/lipgloss" + +// Colors +var ( + colorRed = lipgloss.Color("196") + colorGreen = lipgloss.Color("46") + colorYellow = lipgloss.Color("226") + colorCyan = lipgloss.Color("51") + colorMagenta = lipgloss.Color("201") + colorWhite = lipgloss.Color("255") + colorGray = lipgloss.Color("240") + colorOrange = lipgloss.Color("208") + colorPink = lipgloss.Color("205") +) + +// Text styles +var ( + styleDamage = lipgloss.NewStyle().Foreground(colorRed).Bold(true) + styleHeal = lipgloss.NewStyle().Foreground(colorGreen).Bold(true) + styleCoop = lipgloss.NewStyle().Foreground(colorYellow).Bold(true) + styleFlee = lipgloss.NewStyle().Foreground(colorCyan) + styleStatus = lipgloss.NewStyle().Foreground(colorMagenta) + styleGold = lipgloss.NewStyle().Foreground(colorYellow) + styleSystem = lipgloss.NewStyle().Foreground(colorGray).Italic(true) + styleEnemy = lipgloss.NewStyle().Foreground(colorRed) + stylePlayer = lipgloss.NewStyle().Foreground(colorWhite).Bold(true) + styleHeader = lipgloss.NewStyle().Foreground(colorPink).Bold(true) + styleAction = lipgloss.NewStyle().Bold(true) + styleTimer = lipgloss.NewStyle().Foreground(colorYellow).Bold(true) +) + +// Panel styles +var ( + panelBorder = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(colorGray). + Padding(0, 1) + + panelHeader = lipgloss.NewStyle(). + Foreground(colorPink). + Bold(true). + Align(lipgloss.Center) +) +``` + +- [ ] **Step 2: Build** + +```bash +go build ./... +``` + +- [ ] **Step 3: Commit** + +```bash +git add ui/styles.go +git commit -m "feat: add shared lipgloss styles for terminal visuals" +``` + +--- + +### Task 2: Monster ASCII Art (ui/ascii_art.go) + +**Files:** +- Create: `ui/ascii_art.go` + +- [ ] **Step 1: Create ascii_art.go with art data** + +```go +package ui + +import "github.com/tolelom/catacombs/entity" + +// MonsterArt returns ASCII art lines for a monster type. +func MonsterArt(mt entity.MonsterType) []string { + switch mt { + case entity.MonsterSlime: + return []string{ + ` /\OO/\ `, + ` \ / `, + ` |__| `, + } + case entity.MonsterSkeleton: + return []string{ + ` ,--. `, + ` |oo| `, + ` /||\ `, + } + case entity.MonsterOrc: + return []string{ + ` .---. `, + `/o o\`, + `| --- |`, + } + case entity.MonsterDarkKnight: + return []string{ + ` /|||\ `, + ` |===| `, + ` | | `, + } + case entity.MonsterBoss5: + return []string{ + ` /\ /\ `, + `| @ @ |`, + `| || |`, + `| \__/ |`, + ` \ / `, + } + case entity.MonsterBoss10: + return []string{ + ` __|__ `, + ` /|o o|\ `, + ` | === | `, + ` |\___/| `, + ` |___| `, + } + case entity.MonsterBoss15: + return []string{ + ` ,=====. `, + `/ \ / \`, + `| (O) |`, + ` \ |=| / `, + ` '===' `, + } + case entity.MonsterBoss20: + return []string{ + ` ___/\___ `, + `| x x |`, + `| === |`, + `|\_____/|`, + `|_| |_|`, + } + default: + return []string{` ??? `} + } +} +``` + +- [ ] **Step 2: Build** + +```bash +go build ./... +``` + +- [ ] **Step 3: Commit** + +```bash +git add ui/ascii_art.go +git commit -m "feat: add monster ASCII art data" +``` + +--- + +### Task 3: HP Bar 3-Color + Colored Combat Log (ui/game_view.go) + +**Files:** +- Modify: `ui/game_view.go` + +- [ ] **Step 1: Rewrite renderHPBar with 3-color thresholds** + +Replace the `renderHPBar` function: +```go +func renderHPBar(current, max, width int) string { + if max == 0 { + return "" + } + filled := current * width / max + if filled < 0 { + filled = 0 + } + if filled > width { + filled = width + } + empty := width - filled + + pct := float64(current) / float64(max) + var barStyle lipgloss.Style + switch { + case pct > 0.5: + barStyle = lipgloss.NewStyle().Foreground(colorGreen) + case pct > 0.25: + barStyle = lipgloss.NewStyle().Foreground(colorYellow) + default: + barStyle = lipgloss.NewStyle().Foreground(colorRed) + } + emptyStyle := lipgloss.NewStyle().Foreground(colorGray) + + return barStyle.Render(strings.Repeat("█", filled)) + + emptyStyle.Render(strings.Repeat("░", empty)) +} +``` + +- [ ] **Step 2: Rewrite renderCombatLog with pattern-based coloring** + +Replace `renderCombatLog`: +```go +func renderCombatLog(log []string) string { + if len(log) == 0 { + return "" + } + border := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(colorGray). + Padding(0, 1) + + var sb strings.Builder + for _, msg := range log { + colored := colorizeLog(msg) + sb.WriteString(" > " + colored + "\n") + } + return border.Render(sb.String()) +} + +func colorizeLog(msg string) string { + // Apply semantic colors based on keywords + switch { + case strings.Contains(msg, "fled"): + return styleFlee.Render(msg) + case strings.Contains(msg, "co-op"): + return styleCoop.Render(msg) + case strings.Contains(msg, "healed") || strings.Contains(msg, "Heal") || strings.Contains(msg, "Blessing"): + return styleHeal.Render(msg) + case strings.Contains(msg, "dmg") || strings.Contains(msg, "hit") || strings.Contains(msg, "attacks") || strings.Contains(msg, "Trap"): + return styleDamage.Render(msg) + case strings.Contains(msg, "Taunt") || strings.Contains(msg, "scouted"): + return styleStatus.Render(msg) + case strings.Contains(msg, "gold") || strings.Contains(msg, "Gold") || strings.Contains(msg, "found"): + return styleGold.Render(msg) + case strings.Contains(msg, "defeated") || strings.Contains(msg, "cleared") || strings.Contains(msg, "Descending"): + return styleSystem.Render(msg) + default: + return msg + } +} +``` + +- [ ] **Step 3: Build and test** + +```bash +go build ./... +go test ./ui/ -timeout 15s +``` + +- [ ] **Step 4: Commit** + +```bash +git add ui/game_view.go +git commit -m "feat: 3-color HP bar and semantic combat log coloring" +``` + +--- + +### Task 4: Combat Layout Redesign (ui/game_view.go) + +**Files:** +- Modify: `ui/game_view.go` + +This is the largest task. Rewrite `renderHUD` to use a 2-panel layout in combat mode. + +- [ ] **Step 1: Add renderPartyPanel helper** + +Add to `ui/game_view.go`: +```go +func renderPartyPanel(players []*entity.Player) string { + var sb strings.Builder + sb.WriteString(styleHeader.Render(" PARTY") + "\n\n") + + for _, p := range players { + nameStr := stylePlayer.Render(fmt.Sprintf(" ♦ %s", p.Name)) + classStr := styleSystem.Render(fmt.Sprintf(" (%s)", p.Class)) + status := "" + if p.IsDead() { + status = styleDamage.Render(" [DEAD]") + } + sb.WriteString(nameStr + classStr + status + "\n") + + hpBar := renderHPBar(p.HP, p.MaxHP, 16) + sb.WriteString(fmt.Sprintf(" %s %d/%d\n", hpBar, p.HP, p.MaxHP)) + + sb.WriteString(fmt.Sprintf(" ATK:%-3d DEF:%-3d ", p.EffectiveATK(), p.EffectiveDEF())) + sb.WriteString(styleGold.Render(fmt.Sprintf("G:%d", p.Gold))) + sb.WriteString("\n\n") + } + return sb.String() +} +``` + +- [ ] **Step 2: Add renderEnemyPanel helper** + +```go +func renderEnemyPanel(monsters []*entity.Monster, targetCursor int) string { + var sb strings.Builder + sb.WriteString(styleHeader.Render(" ENEMIES") + "\n\n") + + for i, m := range monsters { + if m.IsDead() { + continue + } + // ASCII art + art := MonsterArt(m.Type) + for _, line := range art { + sb.WriteString(styleEnemy.Render(" " + line) + "\n") + } + + // Name + HP + marker := " " + if i == targetCursor { + marker = "> " + } + hpBar := renderHPBar(m.HP, m.MaxHP, 12) + taunt := "" + if m.TauntTarget { + taunt = styleStatus.Render(" [TAUNTED]") + } + sb.WriteString(fmt.Sprintf(" %s[%d] %s %s %d/%d%s\n\n", + marker, i, styleEnemy.Render(m.Name), hpBar, m.HP, m.MaxHP, taunt)) + } + return sb.String() +} +``` + +- [ ] **Step 3: Rewrite renderHUD for combat mode** + +Replace the combat section of `renderHUD` (the `if state.Phase == game.PhaseCombat` block): +```go + if state.Phase == game.PhaseCombat { + // Two-panel layout: PARTY | ENEMIES + partyContent := renderPartyPanel(state.Players) + enemyContent := renderEnemyPanel(state.Monsters, targetCursor) + + partyPanel := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(colorGray). + Width(35). + Padding(0, 1). + Render(partyContent) + + enemyPanel := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(colorGray). + Width(38). + Padding(0, 1). + Render(enemyContent) + + panels := lipgloss.JoinHorizontal(lipgloss.Top, partyPanel, enemyPanel) + sb.WriteString(panels) + sb.WriteString("\n") + + // Action bar + sb.WriteString(styleAction.Render("[1]Attack [2]Skill [3]Item [4]Flee [5]Wait [Tab]Target [/]Chat")) + sb.WriteString("\n") + + // Timer + if !state.TurnDeadline.IsZero() { + remaining := time.Until(state.TurnDeadline) + if remaining < 0 { + remaining = 0 + } + sb.WriteString(styleTimer.Render(fmt.Sprintf(" Timer: %.1fs", remaining.Seconds()))) + sb.WriteString("\n") + } + + // Skill description for current player (first alive) + for _, p := range state.Players { + if !p.IsDead() { + var skillDesc string + switch p.Class { + case entity.ClassWarrior: + skillDesc = "Skill: Taunt — enemies attack you for 2 turns" + case entity.ClassMage: + skillDesc = "Skill: Fireball — AoE 0.8x dmg to all enemies" + case entity.ClassHealer: + skillDesc = "Skill: Heal — restore 30 HP to an ally" + case entity.ClassRogue: + skillDesc = "Skill: Scout — reveal neighboring rooms" + } + sb.WriteString(styleSystem.Render(skillDesc)) + sb.WriteString("\n") + break // show only current player's skill + } + } + } +``` + +- [ ] **Step 4: Update renderHUD to not wrap combat in border (panels have their own)** + +The exploration mode still uses the old `border` style. Change the function to only apply `border.Render()` for exploration, not combat: + +Restructure `renderHUD` so that: +- Combat: return `sb.String()` directly (panels already have borders) +- Exploring: wrap in `border.Render(sb.String())` + +Replace the return at the end of `renderHUD`: +```go + if state.Phase == game.PhaseCombat { + return sb.String() + } + return border.Render(sb.String()) +``` + +- [ ] **Step 5: Remove the unused roomTypeSymbol function** + +Delete `roomTypeSymbol` (lines ~185-202) — it's dead code. + +- [ ] **Step 6: Build and run all tests** + +```bash +go build ./... +go test ./... -timeout 30s +``` + +- [ ] **Step 7: Commit** + +```bash +git add ui/game_view.go +git commit -m "feat: two-panel combat layout with monster ASCII art" +``` + +--- + +### Task 5: Title Screen Enhancement (ui/title.go) + +**Files:** +- Modify: `ui/title.go` + +- [ ] **Step 1: Rewrite renderTitle with gradient colors and centered layout** + +Replace `ui/title.go` entirely: +```go +package ui + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +var titleLines = []string{ + ` ██████╗ █████╗ ████████╗ █████╗ ██████╗ ██████╗ ███╗ ███╗██████╗ ███████╗`, + `██╔════╝██╔══██╗╚══██╔══╝██╔══██╗██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔════╝`, + `██║ ███████║ ██║ ███████║██║ ██║ ██║██╔████╔██║██████╔╝███████╗`, + `██║ ██╔══██║ ██║ ██╔══██║██║ ██║ ██║██║╚██╔╝██║██╔══██╗╚════██║`, + `╚██████╗██║ ██║ ██║ ██║ ██║╚██████╗╚██████╔╝██║ ╚═╝ ██║██████╔╝███████║`, + `╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝`, +} + +// gradient from red → orange → yellow +var titleColors = []lipgloss.Color{ + lipgloss.Color("196"), // red + lipgloss.Color("202"), // orange-red + lipgloss.Color("208"), // orange + lipgloss.Color("214"), // yellow-orange + lipgloss.Color("220"), // yellow + lipgloss.Color("226"), // bright yellow +} + +func renderTitle(width, height int) string { + // Render logo with gradient + var logoLines []string + for i, line := range titleLines { + color := titleColors[i%len(titleColors)] + style := lipgloss.NewStyle().Foreground(color).Bold(true) + logoLines = append(logoLines, style.Render(line)) + } + logo := strings.Join(logoLines, "\n") + + subtitle := lipgloss.NewStyle(). + Foreground(colorGray). + Render("⚔ A Cooperative Dungeon Crawler ⚔") + + server := lipgloss.NewStyle(). + Foreground(colorCyan). + Render("ssh catacombs.tolelom.xyz") + + menu := lipgloss.NewStyle(). + Foreground(colorWhite). + Bold(true). + Render("[Enter] Start [Q] Quit") + + content := lipgloss.JoinVertical(lipgloss.Center, + logo, + "", + subtitle, + server, + "", + "", + menu, + ) + + // Center on screen + return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, content) +} +``` + +- [ ] **Step 2: Build and test** + +```bash +go build ./... +go test ./ui/ -timeout 15s +``` + +- [ ] **Step 3: Commit** + +```bash +git add ui/title.go +git commit -m "feat: gradient title screen with centered layout" +``` + +--- + +### Task 6: Final integration test + +- [ ] **Step 1: Build and run all tests** + +```bash +go build ./... +go test ./... -timeout 30s +``` + +- [ ] **Step 2: Verify no dead code or unused imports** + +```bash +go vet ./... +```