feat: party action status display and sequential turn result replay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 14:23:44 +09:00
parent 9221cfa7c6
commit 80c1988719
4 changed files with 95 additions and 23 deletions

View File

@@ -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()

View File

@@ -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() {

View File

@@ -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()
}

View File

@@ -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
}