feat: help screen, detailed result screen, death/revive messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
44
ui/help_view.go
Normal 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))
|
||||
}
|
||||
18
ui/model.go
18
ui/model.go
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user