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:
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user