From 80c1988719ca5c7a0737aba441b7253554669467 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 24 Mar 2026 14:23:44 +0900 Subject: [PATCH] feat: party action status display and sequential turn result replay Co-Authored-By: Claude Sonnet 4.6 --- game/session.go | 90 ++++++++++++++++++++++++++++++++++++++----------- game/turn.go | 5 +++ ui/game_view.go | 15 +++++++-- ui/model.go | 8 ++++- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/game/session.go b/game/session.go index ef57e54..3a55001 100644 --- a/game/session.go +++ b/game/session.go @@ -45,15 +45,21 @@ type GameState struct { GameOver bool Victory bool ShopItems []entity.Item - CombatLog []string // recent combat messages - TurnDeadline time.Time + CombatLog []string // recent combat messages + TurnDeadline time.Time + SubmittedActions map[string]string // fingerprint -> action description + PendingLogs []string // logs waiting to be revealed one by one + TurnResolving bool // true while logs are being replayed } func (s *GameSession) addLog(msg string) { - s.state.CombatLog = append(s.state.CombatLog, msg) - // Keep last 5 messages - if len(s.state.CombatLog) > 5 { - s.state.CombatLog = s.state.CombatLog[len(s.state.CombatLog)-5:] + if s.state.TurnResolving { + s.state.PendingLogs = append(s.state.PendingLogs, msg) + } else { + s.state.CombatLog = append(s.state.CombatLog, msg) + if len(s.state.CombatLog) > 8 { + s.state.CombatLog = s.state.CombatLog[len(s.state.CombatLog)-8:] + } } } @@ -242,30 +248,76 @@ func (s *GameSession) GetState() GameState { logCopy := make([]string, len(s.state.CombatLog)) copy(logCopy, s.state.CombatLog) + // Copy submitted actions + submittedCopy := make(map[string]string, len(s.state.SubmittedActions)) + for k, v := range s.state.SubmittedActions { + submittedCopy[k] = v + } + + // Copy pending logs + pendingCopy := make([]string, len(s.state.PendingLogs)) + copy(pendingCopy, s.state.PendingLogs) + return GameState{ - Floor: floorCopy, - Players: players, - Monsters: monsters, - Phase: s.state.Phase, - FloorNum: s.state.FloorNum, - TurnNum: s.state.TurnNum, - CombatTurn: s.state.CombatTurn, - SoloMode: s.state.SoloMode, - GameOver: s.state.GameOver, - Victory: s.state.Victory, - ShopItems: append([]entity.Item{}, s.state.ShopItems...), - CombatLog: logCopy, - TurnDeadline: s.state.TurnDeadline, + Floor: floorCopy, + Players: players, + Monsters: monsters, + Phase: s.state.Phase, + FloorNum: s.state.FloorNum, + TurnNum: s.state.TurnNum, + CombatTurn: s.state.CombatTurn, + SoloMode: s.state.SoloMode, + GameOver: s.state.GameOver, + Victory: s.state.Victory, + ShopItems: append([]entity.Item{}, s.state.ShopItems...), + CombatLog: logCopy, + TurnDeadline: s.state.TurnDeadline, + SubmittedActions: submittedCopy, + PendingLogs: pendingCopy, + TurnResolving: s.state.TurnResolving, } } func (s *GameSession) SubmitAction(playerID string, action PlayerAction) { s.mu.Lock() s.lastActivity[playerID] = time.Now() + desc := "" + switch action.Type { + case ActionAttack: + desc = "Attacking" + case ActionSkill: + desc = "Using Skill" + case ActionItem: + desc = "Using Item" + case ActionFlee: + desc = "Fleeing" + case ActionWait: + desc = "Defending" + } + if s.state.SubmittedActions == nil { + s.state.SubmittedActions = make(map[string]string) + } + s.state.SubmittedActions[playerID] = desc s.mu.Unlock() s.actionCh <- playerActionMsg{PlayerID: playerID, Action: action} } +// RevealNextLog moves one log from PendingLogs to CombatLog. Returns true if there was one to reveal. +func (s *GameSession) RevealNextLog() bool { + s.mu.Lock() + defer s.mu.Unlock() + if len(s.state.PendingLogs) == 0 { + return false + } + msg := s.state.PendingLogs[0] + s.state.PendingLogs = s.state.PendingLogs[1:] + s.state.CombatLog = append(s.state.CombatLog, msg) + if len(s.state.CombatLog) > 8 { + s.state.CombatLog = s.state.CombatLog[len(s.state.CombatLog)-8:] + } + return true +} + func (s *GameSession) TouchActivity(fingerprint string) { s.mu.Lock() defer s.mu.Unlock() diff --git a/game/turn.go b/game/turn.go index b07de9f..9a89358 100644 --- a/game/turn.go +++ b/game/turn.go @@ -18,6 +18,7 @@ func (s *GameSession) RunTurn() { s.state.CombatTurn++ s.clearLog() s.actions = make(map[string]PlayerAction) + s.state.SubmittedActions = make(map[string]string) aliveCount := 0 for _, p := range s.state.Players { if !p.IsOut() { @@ -63,8 +64,12 @@ collecting: } } + s.state.TurnResolving = true + s.state.PendingLogs = nil s.resolvePlayerActions() s.resolveMonsterActions() + s.state.TurnResolving = false + // PendingLogs now contains all turn results — UI will reveal them one by one via RevealNextLog } func (s *GameSession) resolvePlayerActions() { diff --git a/ui/game_view.go b/ui/game_view.go index 9123a35..527fe26 100644 --- a/ui/game_view.go +++ b/ui/game_view.go @@ -75,7 +75,7 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int) string { if state.Phase == game.PhaseCombat { // Two-panel layout: PARTY | ENEMIES - partyContent := renderPartyPanel(state.Players) + partyContent := renderPartyPanel(state.Players, state.SubmittedActions) enemyContent := renderEnemyPanel(state.Monsters, targetCursor) partyPanel := lipgloss.NewStyle(). @@ -232,7 +232,7 @@ func renderHPBar(current, max, width int) string { emptyStyle.Render(strings.Repeat("░", empty)) } -func renderPartyPanel(players []*entity.Player) string { +func renderPartyPanel(players []*entity.Player, submittedActions map[string]string) string { var sb strings.Builder sb.WriteString(styleHeader.Render(" PARTY") + "\n\n") @@ -250,7 +250,16 @@ func renderPartyPanel(players []*entity.Player) string { 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") + sb.WriteString("\n") + + if action, ok := submittedActions[p.Fingerprint]; ok { + sb.WriteString(styleHeal.Render(fmt.Sprintf(" ✓ %s", action))) + sb.WriteString("\n") + } else if !p.IsOut() { + sb.WriteString(styleSystem.Render(" ... Waiting")) + sb.WriteString("\n") + } + sb.WriteString("\n") } return sb.String() } diff --git a/ui/model.go b/ui/model.go index bfcc079..a036f11 100644 --- a/ui/model.go +++ b/ui/model.go @@ -323,10 +323,16 @@ func (m Model) updateGame(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.(type) { case tickMsg: - // State already refreshed above, just keep polling during combat + if m.session != nil { + m.session.RevealNextLog() + } + // Keep polling during combat or while there are pending logs to reveal if m.gameState.Phase == game.PhaseCombat { return m, m.pollState() } + if len(m.gameState.PendingLogs) > 0 { + return m, m.pollState() + } return m, nil }