feat: display turn countdown timer in combat HUD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,19 +3,23 @@ package ui
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/tolelom/catacombs/dungeon"
|
||||
"github.com/tolelom/catacombs/entity"
|
||||
"github.com/tolelom/catacombs/game"
|
||||
)
|
||||
|
||||
func renderGame(state game.GameState, width, height int) string {
|
||||
func renderGame(state game.GameState, width, height int, targetCursor int) string {
|
||||
mapView := renderMap(state.Floor)
|
||||
hudView := renderHUD(state)
|
||||
hudView := renderHUD(state, targetCursor)
|
||||
logView := renderCombatLog(state.CombatLog)
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left,
|
||||
mapView,
|
||||
hudView,
|
||||
logView,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,10 +55,9 @@ func renderMap(floor *dungeon.Floor) string {
|
||||
sb.WriteString(hiddenStyle.Render("[?] ???"))
|
||||
}
|
||||
|
||||
// Show connections
|
||||
for _, n := range room.Neighbors {
|
||||
if n > i {
|
||||
sb.WriteString(" ─── ")
|
||||
sb.WriteString(" --- ")
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
@@ -63,31 +66,85 @@ func renderMap(floor *dungeon.Floor) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func renderHUD(state game.GameState) string {
|
||||
func renderHUD(state game.GameState, targetCursor int) string {
|
||||
var sb strings.Builder
|
||||
border := lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
Padding(0, 1)
|
||||
|
||||
// Player info
|
||||
for _, p := range state.Players {
|
||||
hpBar := renderHPBar(p.HP, p.MaxHP, 20)
|
||||
status := ""
|
||||
if p.IsDead() {
|
||||
status = " [DEAD]"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s (%s) %s %d/%d%s Gold: %d\n",
|
||||
sb.WriteString(fmt.Sprintf("%s (%s) %s %d/%d%s Gold: %d",
|
||||
p.Name, p.Class, hpBar, p.HP, p.MaxHP, status, p.Gold))
|
||||
|
||||
// Show inventory count
|
||||
itemCount := len(p.Inventory)
|
||||
relicCount := len(p.Relics)
|
||||
if itemCount > 0 || relicCount > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" Items:%d Relics:%d", itemCount, relicCount))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if state.Phase == game.PhaseCombat {
|
||||
sb.WriteString("\n")
|
||||
// Enemies
|
||||
enemyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
|
||||
for i, m := range state.Monsters {
|
||||
if !m.IsDead() {
|
||||
mhpBar := renderHPBar(m.HP, m.MaxHP, 15)
|
||||
sb.WriteString(fmt.Sprintf(" [%d] %s %s %d/%d\n", i, m.Name, mhpBar, m.HP, m.MaxHP))
|
||||
taunt := ""
|
||||
if m.TauntTarget {
|
||||
taunt = " [TAUNTED]"
|
||||
}
|
||||
marker := " "
|
||||
if i == targetCursor {
|
||||
marker = "> "
|
||||
}
|
||||
sb.WriteString(enemyStyle.Render(fmt.Sprintf("%s[%d] %s %s %d/%d%s", marker, i, m.Name, mhpBar, m.HP, m.MaxHP, taunt)))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
// Actions with skill description
|
||||
actionStyle := lipgloss.NewStyle().Bold(true)
|
||||
sb.WriteString(actionStyle.Render("[1]Attack [2]Skill [3]Item [4]Flee [5]Wait [Tab]Target"))
|
||||
sb.WriteString("\n")
|
||||
if !state.TurnDeadline.IsZero() {
|
||||
remaining := time.Until(state.TurnDeadline)
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
timerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("226")).Bold(true)
|
||||
sb.WriteString(timerStyle.Render(fmt.Sprintf(" Timer: %.1fs", remaining.Seconds())))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Skill description per class
|
||||
skillStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Italic(true)
|
||||
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(skillStyle.Render(skillDesc))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n[1]Attack [2]Skill [3]Item [4]Flee [5]Wait")
|
||||
} else if state.Phase == game.PhaseExploring {
|
||||
sb.WriteString("\nChoose a room to enter (number) or [Q] quit")
|
||||
}
|
||||
@@ -95,6 +152,21 @@ func renderHUD(state game.GameState) string {
|
||||
return border.Render(sb.String())
|
||||
}
|
||||
|
||||
func renderCombatLog(log []string) string {
|
||||
if len(log) == 0 {
|
||||
return ""
|
||||
}
|
||||
logStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("228")).
|
||||
PaddingLeft(1)
|
||||
|
||||
var sb strings.Builder
|
||||
for _, msg := range log {
|
||||
sb.WriteString(" > " + msg + "\n")
|
||||
}
|
||||
return logStyle.Render(sb.String())
|
||||
}
|
||||
|
||||
func renderHPBar(current, max, width int) string {
|
||||
if max == 0 {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user