Skip to content

refactor: replace marker battle events with typed dispatch#49

Merged
liang799 merged 2 commits into
mainfrom
tianpok/report-battle-event-type-params
Apr 22, 2026
Merged

refactor: replace marker battle events with typed dispatch#49
liang799 merged 2 commits into
mainfrom
tianpok/report-battle-event-type-params

Conversation

@liang799
Copy link
Copy Markdown
Owner

@liang799 liang799 commented Apr 22, 2026

Closes #50

Summary

  • Refactor
    • Refactored battle event system to use the visitor pattern for streamlined event handling and improved code architecture.
    • Modernized event classes with Java records, enhancing immutability and reducing code boilerplate.

Summary by CodeRabbit

  • Refactor
    • Improved internal code structure and immutability within the battle system components
    • Enhanced data validation and null-safety checks for battle event handling
    • Streamlined event dispatching logic in the battle UI layer for better maintainability

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

Multiple report event classes were converted to Java records and BattleEvent was extended with a generic Visitor interface. Consumers (console, GUI dialogue, arena model) were refactored to dispatch events via the new visitor callbacks instead of runtime instanceof checks.

Changes

Cohort / File(s) Summary
Event Class-to-Record Conversions
src/main/java/sc2002/turnbased/report/ActionEvent.java, src/main/java/sc2002/turnbased/report/NarrationEvent.java, src/main/java/sc2002/turnbased/report/RoundStartEvent.java, src/main/java/sc2002/turnbased/report/RoundSummaryEvent.java, src/main/java/sc2002/turnbased/report/SkippedTurnEvent.java
Converted classes to records; added compact constructors with non-null validation; defensive copying of list/map fields (List.copyOf / unmodifiableMap); implemented visit(Visitor<T>) to dispatch to visitor callbacks.
Event Data Model
src/main/java/sc2002/turnbased/report/CombatantSummary.java
Converted to record; non-null checks in canonical constructor; activeStatuses stored via List.copyOf(...) and returned directly.
Base Event Interface Enhancement
src/main/java/sc2002/turnbased/report/BattleEvent.java
Added <T> T visit(Visitor<T> visitor) and nested interface Visitor<T> with callbacks: onAction, onNarration, onRoundStart, onRoundSummary, onSkippedTurn, onStatusEffectReport.
Status Effect Event Integration
src/main/java/sc2002/turnbased/report/StatusEffectReportEvent.java
Added visit(Visitor<T>) override delegating to visitor.onStatusEffectReport(this).
UI Formatter & Model Visitor Refactoring
src/main/java/sc2002/turnbased/ui/BattleConsoleFormatter.java, src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java, src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneModel.java
Replaced instanceof/conditional chains with event.visit(new BattleEvent.Visitor<>() { ... }) implementations; preserved existing formatting and state-update logic but centralized dispatch.
Test Double Update
src/test/java/sc2002/turnbased/domain/EnemyCombatantTest.java
Updated test TestBattleEvent to implement visit(Visitor<T>) and throw UnsupportedOperationException to enforce visitor contract.

Sequence Diagram(s)

sequenceDiagram
    participant E as Event (BattleEvent)
    participant V as Visitor (BattleEvent.Visitor)
    participant C as Consumer (Formatter / Model)
    C->>E: call event.visit(visitorInstance)
    E->>V: dispatch to onX(this)
    V-->>C: return formatted data / perform state update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped in, records in tow,

Visitors guide each event's show,
No casts or checks to make me pause,
Immutable hops, applause, applause! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring: replacing instanceof dispatch with the visitor pattern for typed event handling.
Linked Issues check ✅ Passed The PR fully implements the visitor pattern across all event types [#50], converting classes to records, adding visit() methods, and updating all consumers to use visitor dispatch.
Out of Scope Changes check ✅ Passed All changes are within scope: converting events to records, implementing visitor pattern, and updating consumers. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tianpok/report-battle-event-type-params

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java (1)

46-80: Consider caching the formatted text to avoid double dispatch.

playbackDelayMillis visits the event twice: once for baseDelay (line 47) and once via format(event) (line 78). This is a minor inefficiency.

♻️ Optional optimization
 public int playbackDelayMillis(BattleEvent event) {
+    String formatted = format(event);
     int baseDelay = event.visit(new BattleEvent.Visitor<>() {
         // ... visitor callbacks unchanged ...
     });
-    int typewriterDelay = format(event).length() * 12 + 520;
+    int typewriterDelay = formatted.length() * 12 + 520;
     return Math.max(baseDelay, Math.min(2_000, typewriterDelay));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java`
around lines 46 - 80, Cache the formatted string once and avoid visiting the
event twice: call String formatted = format(event) and compute typewriterDelay
from formatted, then determine baseDelay by checking the event concrete type
(use instanceof against ActionEvent, NarrationEvent, RoundStartEvent,
RoundSummaryEvent, SkippedTurnEvent, StatusEffectReportEvent) instead of calling
event.visit again; update playbackDelayMillis to use the single cached formatted
value and the instanceof-based branch to compute the same numeric baseDelay
values.
src/main/java/sc2002/turnbased/report/ActionEvent.java (1)

24-27: Tighten constructor null-safety for event payload fields.

Please validate actorName, actionName, targetName in the canonical constructor, and fail fast in the convenience constructor for actor, target, and attackResolution so callers get clearer errors.

Proposed refactor
 public ActionEvent {
     actorId = Objects.requireNonNull(actorId, "actorId");
+    actorName = Objects.requireNonNull(actorName, "actorName");
+    actionName = Objects.requireNonNull(actionName, "actionName");
     targetId = Objects.requireNonNull(targetId, "targetId");
+    targetName = Objects.requireNonNull(targetName, "targetName");
     statusEffectNotes = List.copyOf(Objects.requireNonNull(statusEffectNotes, "statusEffectNotes"));
 }

 public ActionEvent(Combatant actor, String actionName, Combatant target, AttackResolution attackResolution) {
     this(
-        actor.combatantId(),
-        actor.getName(),
-        actionName,
-        target.combatantId(),
-        target.getName(),
-        attackResolution.hpBefore(),
-        attackResolution.hpAfter(),
-        attackResolution.attackUsed(),
-        attackResolution.targetDefense(),
-        attackResolution.damage(),
-        attackResolution.targetEliminated(),
-        StatusEffectReportMapper.toNotes(attackResolution.statusEffectOutcomes())
+        Objects.requireNonNull(actor, "actor").combatantId(),
+        Objects.requireNonNull(actor, "actor").getName(),
+        Objects.requireNonNull(actionName, "actionName"),
+        Objects.requireNonNull(target, "target").combatantId(),
+        Objects.requireNonNull(target, "target").getName(),
+        Objects.requireNonNull(attackResolution, "attackResolution").hpBefore(),
+        Objects.requireNonNull(attackResolution, "attackResolution").hpAfter(),
+        Objects.requireNonNull(attackResolution, "attackResolution").attackUsed(),
+        Objects.requireNonNull(attackResolution, "attackResolution").targetDefense(),
+        Objects.requireNonNull(attackResolution, "attackResolution").damage(),
+        Objects.requireNonNull(attackResolution, "attackResolution").targetEliminated(),
+        StatusEffectReportMapper.toNotes(
+            Objects.requireNonNull(attackResolution, "attackResolution").statusEffectOutcomes()
+        )
     );
 }

Also applies to: 30-44

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/sc2002/turnbased/report/ActionEvent.java` around lines 24 - 27,
The compact/canonical constructor for ActionEvent currently null-checks actorId,
targetId and statusEffectNotes but misses actorName, actionName and
targetName—add Objects.requireNonNull checks for actorName, actionName, and
targetName and copy statusEffectNotes as before; additionally, in the
convenience constructor(s) (the constructor taking Actor actor, Actor target,
and AttackResolution attackResolution or similar overloads referenced around the
other constructor block), perform immediate null checks for actor, target, and
attackResolution (and any derived names) so the constructor fails fast with
clear NPE messages rather than deferring validation—update the constructors that
delegate to the canonical one to validate inputs before using
actor.getId()/getName() or target.getId()/getName().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/sc2002/turnbased/report/ActionEvent.java`:
- Around line 24-27: The compact/canonical constructor for ActionEvent currently
null-checks actorId, targetId and statusEffectNotes but misses actorName,
actionName and targetName—add Objects.requireNonNull checks for actorName,
actionName, and targetName and copy statusEffectNotes as before; additionally,
in the convenience constructor(s) (the constructor taking Actor actor, Actor
target, and AttackResolution attackResolution or similar overloads referenced
around the other constructor block), perform immediate null checks for actor,
target, and attackResolution (and any derived names) so the constructor fails
fast with clear NPE messages rather than deferring validation—update the
constructors that delegate to the canonical one to validate inputs before using
actor.getId()/getName() or target.getId()/getName().

In `@src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java`:
- Around line 46-80: Cache the formatted string once and avoid visiting the
event twice: call String formatted = format(event) and compute typewriterDelay
from formatted, then determine baseDelay by checking the event concrete type
(use instanceof against ActionEvent, NarrationEvent, RoundStartEvent,
RoundSummaryEvent, SkippedTurnEvent, StatusEffectReportEvent) instead of calling
event.visit again; update playbackDelayMillis to use the single cached formatted
value and the instanceof-based branch to compute the same numeric baseDelay
values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71c9e4ef-3b98-46ee-96ce-8af56abe1967

📥 Commits

Reviewing files that changed from the base of the PR and between 81b2061 and 8aecc9d.

📒 Files selected for processing (12)
  • src/main/java/sc2002/turnbased/report/ActionEvent.java
  • src/main/java/sc2002/turnbased/report/BattleEvent.java
  • src/main/java/sc2002/turnbased/report/CombatantSummary.java
  • src/main/java/sc2002/turnbased/report/NarrationEvent.java
  • src/main/java/sc2002/turnbased/report/RoundStartEvent.java
  • src/main/java/sc2002/turnbased/report/RoundSummaryEvent.java
  • src/main/java/sc2002/turnbased/report/SkippedTurnEvent.java
  • src/main/java/sc2002/turnbased/report/StatusEffectReportEvent.java
  • src/main/java/sc2002/turnbased/ui/BattleConsoleFormatter.java
  • src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneModel.java
  • src/test/java/sc2002/turnbased/domain/EnemyCombatantTest.java

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java (1)

46-64: Consider using visitor pattern for playbackDelayMillis for consistency.

The format() method was refactored to use the visitor pattern, but playbackDelayMillis() still uses instanceof checks. This creates an inconsistency: if a new BattleEvent type is added, format() will fail to compile (enforcing the update), while playbackDelayMillis() will silently fall back to the default baseDelay = 1_000.

Additionally, format(event) is called here to compute typewriterDelay, but callers likely also call format() separately for display, resulting in redundant formatting.

♻️ Suggested refactor using visitor for delay calculation
     public int playbackDelayMillis(BattleEvent event) {
         String formatted = format(event);
         int typewriterDelay = formatted.length() * 12 + 520;
-        int baseDelay = 1_000;
-        if (event instanceof ActionEvent) {
-            baseDelay = 1_350;
-        } else if (event instanceof NarrationEvent) {
-            baseDelay = 1_250;
-        } else if (event instanceof RoundStartEvent) {
-            baseDelay = 850;
-        } else if (event instanceof RoundSummaryEvent) {
-            baseDelay = 900;
-        } else if (event instanceof SkippedTurnEvent) {
-            baseDelay = 1_150;
-        } else if (event instanceof StatusEffectReportEvent) {
-            baseDelay = 1_100;
-        }
+        int baseDelay = event.visit(new BattleEvent.Visitor<>() {
+            `@Override` public Integer onAction(ActionEvent e) { return 1_350; }
+            `@Override` public Integer onNarration(NarrationEvent e) { return 1_250; }
+            `@Override` public Integer onRoundStart(RoundStartEvent e) { return 850; }
+            `@Override` public Integer onRoundSummary(RoundSummaryEvent e) { return 900; }
+            `@Override` public Integer onSkippedTurn(SkippedTurnEvent e) { return 1_150; }
+            `@Override` public Integer onStatusEffectReport(StatusEffectReportEvent e) { return 1_100; }
+        });
         return Math.max(baseDelay, Math.min(2_000, typewriterDelay));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java`
around lines 46 - 64, Replace the instanceof chain in
playbackDelayMillis(BattleEvent) with a visitor-based dispatch: add a
playback-delay method to your BattleEvent visitor (e.g., PlaybackDelayVisitor or
add a getPlaybackBaseDelay(BattleEventVisitor) visit method for each concrete
event) and implement it in all concrete events (ActionEvent, NarrationEvent,
RoundStartEvent, RoundSummaryEvent, SkippedTurnEvent, StatusEffectReportEvent)
so playbackDelayMillis calls event.accept(playbackDelayVisitor) to obtain
baseDelay instead of using instanceof. Also avoid reformatting by changing
playbackDelayMillis to accept the already-formatted string (or the formatted
length) so it computes typewriterDelay from that input (use
playbackDelayMillis(BattleEvent, String formatted) or
playbackDelayMillis(BattleEvent, int formattedLength)), and update callers to
pass the output of format(event) to eliminate redundant formatting; ensure all
new visitor methods are implemented for every BattleEvent concrete class to keep
compilation enforcement consistent with format().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java`:
- Around line 46-64: Replace the instanceof chain in
playbackDelayMillis(BattleEvent) with a visitor-based dispatch: add a
playback-delay method to your BattleEvent visitor (e.g., PlaybackDelayVisitor or
add a getPlaybackBaseDelay(BattleEventVisitor) visit method for each concrete
event) and implement it in all concrete events (ActionEvent, NarrationEvent,
RoundStartEvent, RoundSummaryEvent, SkippedTurnEvent, StatusEffectReportEvent)
so playbackDelayMillis calls event.accept(playbackDelayVisitor) to obtain
baseDelay instead of using instanceof. Also avoid reformatting by changing
playbackDelayMillis to accept the already-formatted string (or the formatted
length) so it computes typewriterDelay from that input (use
playbackDelayMillis(BattleEvent, String formatted) or
playbackDelayMillis(BattleEvent, int formattedLength)), and update callers to
pass the output of format(event) to eliminate redundant formatting; ensure all
new visitor methods are implemented for every BattleEvent concrete class to keep
compilation enforcement consistent with format().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78222dfa-f69a-4461-aceb-57bd82c397cd

📥 Commits

Reviewing files that changed from the base of the PR and between 8aecc9d and 5e1acbf.

📒 Files selected for processing (2)
  • src/main/java/sc2002/turnbased/report/ActionEvent.java
  • src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/sc2002/turnbased/report/ActionEvent.java

@liang799 liang799 merged commit 16f0ca7 into main Apr 22, 2026
4 checks passed
@liang799 liang799 deleted the tianpok/report-battle-event-type-params branch April 22, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor BattleEvent away from marker-interface dispatch

1 participant