feat: help screen, detailed result screen, death/revive messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 14:44:03 +09:00
parent 80c1988719
commit 29387ebaa0
5 changed files with 109 additions and 25 deletions

View File

@@ -287,6 +287,7 @@ func (s *GameSession) advanceFloor() {
for _, p := range s.state.Players {
if p.IsDead() {
p.Revive(0.30)
s.addLog(fmt.Sprintf("✦ %s revived at %d HP!", p.Name, p.HP))
}
p.Fled = false
}
@@ -323,6 +324,9 @@ func (s *GameSession) resolveMonsterActions() {
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 0.5)
p.TakeDamage(dmg)
s.addLog(fmt.Sprintf("%s AoE hits %s for %d dmg", m.Name, p.Name, dmg))
if p.IsDead() {
s.addLog(fmt.Sprintf("☠ %s has fallen!", p.Name))
}
}
}
} else {
@@ -332,6 +336,9 @@ func (s *GameSession) resolveMonsterActions() {
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 1.0)
p.TakeDamage(dmg)
s.addLog(fmt.Sprintf("%s attacks %s for %d dmg", m.Name, p.Name, dmg))
if p.IsDead() {
s.addLog(fmt.Sprintf("☠ %s has fallen!", p.Name))
}
}
}
}

44
ui/help_view.go Normal file
View File

@@ -0,0 +1,44 @@
package ui
import (
"github.com/charmbracelet/lipgloss"
)
func renderHelp(width, height int) string {
title := styleHeader.Render("── Controls ──")
sections := []struct{ header, body string }{
{"Exploration", ` [Up/Down] Select room
[Enter] Move to room
[/] Chat
[Q] Quit`},
{"Combat", ` [1] Attack [2] Skill
[3] Use Item [4] Flee
[5] Defend [Tab] Switch Target
[/] Chat`},
{"Shop", ` [1-3] Buy item
[Q] Leave shop`},
{"Classes", ` Warrior 120HP 12ATK 8DEF Taunt (draw fire 2t)
Mage 70HP 20ATK 3DEF Fireball (AoE 0.8x)
Healer 90HP 8ATK 5DEF Heal (restore 30HP)
Rogue 85HP 15ATK 4DEF Scout (reveal rooms)`},
{"Tips", ` • Skills have 3 uses per combat
• Co-op bonus: 10% extra when 2+ attack same target
• Items are limited to 10 per player
• Dead players revive next floor at 30% HP`},
}
var content string
headerStyle := lipgloss.NewStyle().Foreground(colorCyan).Bold(true)
bodyStyle := lipgloss.NewStyle().Foreground(colorWhite)
for _, s := range sections {
content += headerStyle.Render(s.header) + "\n"
content += bodyStyle.Render(s.body) + "\n\n"
}
footer := styleSystem.Render("[H] Back")
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center,
lipgloss.JoinVertical(lipgloss.Center, title, "", content, footer))
}

View File

@@ -20,6 +20,7 @@ const (
screenGame
screenShop
screenResult
screenHelp
)
// StateUpdateMsg is sent by GameSession to update the view
@@ -104,6 +105,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateShop(msg)
case screenResult:
return m.updateResult(msg)
case screenHelp:
return m.updateHelp(msg)
}
return m, nil
}
@@ -128,7 +131,9 @@ func (m Model) View() string {
if m.store != nil {
rankings, _ = m.store.TopRuns(10)
}
return renderResult(m.gameState.Victory, m.gameState.FloorNum, rankings)
return renderResult(m.gameState, rankings)
case screenHelp:
return renderHelp(m.width, m.height)
}
return ""
}
@@ -180,6 +185,8 @@ func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.screen = screenLobby
m = m.withRefreshedLobby()
} else if isKey(key, "h") {
m.screen = screenHelp
} else if isQuit(key) {
return m, tea.Quit
}
@@ -187,6 +194,15 @@ func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m Model) updateHelp(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok {
if isKey(key, "h") || isEnter(key) || isQuit(key) {
m.screen = screenTitle
}
}
return m, nil
}
func (m Model) updateLobby(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok {
// Join-by-code input mode

View File

@@ -2,39 +2,56 @@ package ui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/tolelom/catacombs/game"
"github.com/tolelom/catacombs/store"
)
func renderResult(won bool, floorReached int, rankings []store.RunRecord) string {
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205"))
func renderResult(state game.GameState, rankings []store.RunRecord) string {
var sb strings.Builder
var title string
if won {
title = titleStyle.Render("VICTORY! You escaped the Catacombs!")
// Title
if state.Victory {
sb.WriteString(styleHeal.Render("VICTORY ✦ ") + "\n\n")
sb.WriteString(styleSystem.Render(" You conquered the Catacombs!") + "\n\n")
} else {
title = titleStyle.Render("GAME OVER")
sb.WriteString(styleDamage.Render(" ✦ DEFEAT ✦ ") + "\n\n")
sb.WriteString(styleSystem.Render(fmt.Sprintf(" Fallen on floor B%d", state.FloorNum)) + "\n\n")
}
floorInfo := fmt.Sprintf("Floor Reached: B%d", floorReached)
// Player summary
sb.WriteString(styleHeader.Render("── Party Summary ──") + "\n\n")
totalGold := 0
for _, p := range state.Players {
status := styleHeal.Render("Alive")
if p.IsDead() {
status = styleDamage.Render("Dead")
}
sb.WriteString(fmt.Sprintf(" %s (%s) %s Gold: %d Items: %d Relics: %d\n",
stylePlayer.Render(p.Name), p.Class, status, p.Gold, len(p.Inventory), len(p.Relics)))
totalGold += p.Gold
}
sb.WriteString(fmt.Sprintf("\n Total Gold: %s\n", styleGold.Render(fmt.Sprintf("%d", totalGold))))
rankHeader := lipgloss.NewStyle().Bold(true).Render("── Rankings ──")
rankList := ""
for i, r := range rankings {
rankList += fmt.Sprintf(" %d. %s — B%d (Score: %d)\n", i+1, r.Player, r.Floor, r.Score)
// Rankings
if len(rankings) > 0 {
sb.WriteString("\n" + styleHeader.Render("── Top Runs ──") + "\n\n")
for i, r := range rankings {
medal := " "
switch i {
case 0:
medal = styleGold.Render("🥇")
case 1:
medal = styleSystem.Render("🥈")
case 2:
medal = styleGold.Render("🥉")
}
sb.WriteString(fmt.Sprintf(" %s %s Floor B%d Score: %d\n", medal, r.Player, r.Floor, r.Score))
}
}
menu := "[Enter] Return to Lobby [Q] Quit"
sb.WriteString("\n" + styleAction.Render(" [Enter] Return to Lobby") + "\n")
return lipgloss.JoinVertical(lipgloss.Center,
title,
"",
floorInfo,
"",
rankHeader,
rankList,
"",
menu,
)
return sb.String()
}

View File

@@ -44,7 +44,7 @@ func renderTitle(width, height int) string {
menu := lipgloss.NewStyle().
Foreground(colorWhite).
Bold(true).
Render("[Enter] Start [Q] Quit")
Render("[Enter] Start [H] Help [Q] Quit")
content := lipgloss.JoinVertical(lipgloss.Center,
logo,