diff --git a/game/session.go b/game/session.go index 3076da4..c9a07da 100644 --- a/game/session.go +++ b/game/session.go @@ -2,6 +2,7 @@ package game import ( "sync" + "time" "github.com/tolelom/catacombs/dungeon" "github.com/tolelom/catacombs/entity" @@ -43,7 +44,8 @@ type GameState struct { GameOver bool Victory bool ShopItems []entity.Item - CombatLog []string // recent combat messages + CombatLog []string // recent combat messages + TurnDeadline time.Time } func (s *GameSession) addLog(msg string) { diff --git a/game/turn.go b/game/turn.go index c9d3b2c..2e649a5 100644 --- a/game/turn.go +++ b/game/turn.go @@ -28,6 +28,9 @@ func (s *GameSession) RunTurn() { // Collect actions with timeout timer := time.NewTimer(TurnTimeout) + s.mu.Lock() + s.state.TurnDeadline = time.Now().Add(TurnTimeout) + s.mu.Unlock() collected := 0 for collected < aliveCount { select { @@ -45,6 +48,7 @@ func (s *GameSession) RunTurn() { resolve: s.mu.Lock() defer s.mu.Unlock() + s.state.TurnDeadline = time.Time{} // Default action for players who didn't submit: Wait for _, p := range s.state.Players { @@ -71,28 +75,6 @@ func (s *GameSession) resolvePlayerActions() { } } - // Check if ALL alive players chose flee - fleeCount := 0 - aliveCount := 0 - for _, p := range s.state.Players { - if p.IsDead() { - continue - } - aliveCount++ - if action, ok := s.actions[p.Name]; ok && action.Type == ActionFlee { - fleeCount++ - } - } - if fleeCount == aliveCount && aliveCount > 0 { - if combat.AttemptFlee() { - s.addLog("Fled from battle!") - s.state.Phase = PhaseExploring - return - } - s.addLog("Failed to flee!") - return - } - for _, p := range s.state.Players { if p.IsDead() { continue @@ -161,7 +143,15 @@ func (s *GameSession) resolvePlayerActions() { s.addLog(fmt.Sprintf("%s has no items to use!", p.Name)) } case ActionFlee: - s.addLog(fmt.Sprintf("%s tried to flee but party didn't agree", p.Name)) + if combat.AttemptFlee() { + s.addLog(fmt.Sprintf("%s fled from battle!", p.Name)) + if s.state.SoloMode { + s.state.Phase = PhaseExploring + return + } + } else { + s.addLog(fmt.Sprintf("%s failed to flee!", p.Name)) + } case ActionWait: s.addLog(fmt.Sprintf("%s is defending", p.Name)) } diff --git a/ui/game_view.go b/ui/game_view.go index 8ea5c8d..d0b7538 100644 --- a/ui/game_view.go +++ b/ui/game_view.go @@ -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 ""