5 tasks: styles, ASCII art, colored log, combat layout, title screen. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 KiB
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
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
go build ./...
- Step 3: Commit
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
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
go build ./...
- Step 3: Commit
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:
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:
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
go build ./...
go test ./ui/ -timeout 15s
- Step 4: Commit
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:
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
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):
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:
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
go build ./...
go test ./... -timeout 30s
- Step 7: Commit
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:
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
go build ./...
go test ./ui/ -timeout 15s
- Step 3: Commit
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
go build ./...
go test ./... -timeout 30s
- Step 2: Verify no dead code or unused imports
go vet ./...