Skip to content

Tianpok/2d gameplay upgrade#35

Merged
liang799 merged 27 commits into
mainfrom
tianpok/2d-gameplay-upgrade
Apr 15, 2026
Merged

Tianpok/2d gameplay upgrade#35
liang799 merged 27 commits into
mainfrom
tianpok/2d-gameplay-upgrade

Conversation

@liang799
Copy link
Copy Markdown
Owner

@liang799 liang799 commented Apr 15, 2026

If merged

This is a major change! Finally implemented proper GUI! wohoo!

It this merges, tag this with v2.0.0

Release Notes

  • New Features

    • Added enhanced 2D battle arena with sprite rendering, animations, and real-time positioning.
    • Introduced keyboard controls (WASD/arrows for movement, Q/E for targeting, 1–4 for actions, Esc for menu).
    • Implemented paced battle message playback with typewriter effects and automatic delays.
    • Added visual feedback including animated damage numbers, HP bars, and status indicators.
  • Documentation

    • Updated README with detailed GUI descriptions, controls reference, and build instructions.
  • Refactor

    • Restructured battle UI using MVC architecture with separate controller and view components.
  • Tests

    • Added comprehensive test coverage for battle logic, UI components, and dialogue formatting.

Summary by CodeRabbit

  • New Features

    • Playable animated 2D arena with sprite rendering, HP bars, status badges, floating damage text, and visual effects.
    • Responsive input: WASD/arrow movement, mouse targeting, Q/E target cycling, numeric hotkeys (1–4), and Esc for menu/navigation.
    • Enhanced battle UI: command panel, paced/typewriter dialogue playback, side battle log, setup presets/custom/replay, and explicit post‑game choices.
  • Documentation

    • README updated with 2D GUI details, controls, gameplay elements, and Maven run instructions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors the GUI into an MVC/controller-driven design: replaces modal decision dialogs with a controller-backed decision queue, introduces a 2D arena renderer and sprite pipeline with input/animation/playback, reorganizes UI into composed panels, and updates README run/controls documentation.

Changes

Cohort / File(s) Summary
Documentation
README.md
Expanded README to describe a “2D Swing GUI”, detailed gameplay/controls (WASD/arrows, Q/E, 1–4, Esc), playback and submenu behavior, and added Maven run instructions.
Main View / Integration
src/main/java/.../TurnBasedArenaGui.java
Converted main JFrame to implement BattleView; replaced inline battle loop with composed panels and controller callbacks; adjusted shutdown/exit, log layout, and startup text.
Controller / Orchestration
src/main/java/.../controller/BattleController.java, src/main/java/.../controller/GuiPlayerDecisionProvider.java
Added BattleController (executor, model/playback wiring, awaitPlayerDecision, handleCommand, shutdown) and a controller-backed GuiPlayerDecisionProvider delegating to awaitPlayerDecision.
Removed Legacy Provider
src/main/java/.../GuiPlayerDecisionProvider.java
Deleted previous modal-dialog-based GuiPlayerDecisionProvider.
Command Resolution
src/main/java/.../command/PlayerCommandResolver.java
New resolver mapping UI commands to validated ResolvedPlayerCommand/PlayerDecision (checks targeting, availability, inventory, special cooldown).
Session & Turn State
src/main/java/.../model/*.java
Added synchronized BattleSessionModel and immutable records PlayerTurnRequest and ResolvedPlayerCommand to manage active/queued turns and post-game config.
Playback & Dialogue
src/main/java/.../playback/*.java
Added BattlePlaybackController (EDT-only queued playback) and BattleDialogueFormatter (formats events, computes delays).
View Contract & Panels
src/main/java/.../view/BattleView.java, BattleSetupPanel.java, BattleCommandPanel.java
New BattleView interface and composed panels for setup/commands, menu states, hotkeys (1–4, Q/E, Esc), animated prompts and resolving UI.
2D Arena, Model & Rendering
src/main/java/.../view/ArenaScenePanel.java, ArenaSceneModel.java, ArenaSceneRenderer.java, FighterSpriteDto.java, FighterSpriteRenderer.java, *BodyRenderer.java, FighterSpriteHudRenderer.java, FighterSpriteEffectRenderer.java, FighterType.java, ArenaSceneLoop.java
Large new arena subsystem: movement (WASD/arrows), mouse targeting, scene loop, sprite DTOs, per-type body renderers, HUD/effects, animations, and view APIs to present turns and events.
Launch & Utilities
src/main/java/.../setup/BattleLaunchRequest.java, src/main/java/.../setup/PostGameConfig.java, src/main/java/.../util/SwingThread.java
Encapsulated launch modes (preset/custom/replay), added PostGameConfig sealed types, and an EDT run-and-wait helper.
Renderers & HUD Helpers
src/main/java/.../view/FighterSpriteHudRenderer.java, FighterSpriteEffectRenderer.java, FighterSpriteRenderer.java, FighterBodyRenderer.java, WarriorBodyRenderer.java, WizardBodyRenderer.java, GoblinBodyRenderer.java, WolfBodyRenderer.java, UnknownFighterBodyRenderer.java
New rendering pipeline with multiple body renderers, HUD and effect rendering, and type mapping.
Tests
src/test/java/.../*
Added many unit tests covering command resolution, session model, playback controller, dialogue formatter, launch request, sprite DTO/renderer, command-panel escaping, and arena model behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Player as Player
    participant View as BattleView
    participant Controller as BattleController
    participant Model as BattleSessionModel
    participant Engine as BattleEngine
    participant Playback as BattlePlaybackController

    Player->>View: startBattle(request)
    View->>Controller: startBattle(request)
    Controller->>Model: beginBattle()
    Controller->>View: showBattleLoading()
    Controller->>Engine: (async) run BattleEngine with provider

    Engine->>Controller: request decision (provider.decide)
    Controller->>Model: queuePlayerTurn(PlayerTurnRequest)
    Controller->>View: showPlayerTurn(turn)
    Player->>View: sendCommand(command)
    View->>Controller: handleCommand(command)
    Controller->>Controller: PlayerCommandResolver.resolve(...)
    Controller->>Model: takeQueuedPlayerTurn() -> active turn
    Controller->>Controller: offer decision into turn.responseQueue
    Controller->>View: showCommandResolving(actionName)

    Engine->>Controller: emits BattleEvent
    Controller->>Playback: enqueue(event)
    Playback->>View: showBattleEvent(event, message, transcript)
    View->>View: update sprites / HUD / banners

    Engine->>Controller: battle ends
    Controller->>View: showBattleComplete()
    View->>Player: askPostGameChoice()
    Player->>Controller: (REPLAY / NEW_SETUP / EXIT)
    Controller->>Model: finishBattle()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰
I hopped into code where sprites now play,
Controllers queue turns, and banners sway,
Playback paces lines in timed array,
Views paint the arena where goblins stray,
A rabbit cheers: MVC hooray!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The pull request title 'Tianpok/2d gameplay upgrade' is vague and uses non-descriptive branch-name-like phrasing that does not clearly convey the specific technical changes involved. Consider using a more descriptive title that highlights the primary change, such as 'Implement 2D battle arena with sprite rendering and keyboard controls' or 'Refactor GUI to MVC architecture with 2D gameplay features'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tianpok/2d-gameplay-upgrade

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.

Actionable comments posted: 10

🧹 Nitpick comments (9)
src/main/java/sc2002/turnbased/ui/gui/model/ResolvedPlayerCommand.java (1)

5-10: Add null-contract validation in the record constructor.

This model looks like a required-data transport object; allowing null decision, actionName, or targetLabel can push failures downstream.

Proposed fix
+import java.util.Objects;
+
 public record ResolvedPlayerCommand(
     PlayerDecision decision,
     String actionName,
     String targetLabel
 ) {
+    public ResolvedPlayerCommand {
+        decision = Objects.requireNonNull(decision, "decision");
+        actionName = Objects.requireNonNull(actionName, "actionName");
+        targetLabel = Objects.requireNonNull(targetLabel, "targetLabel");
+    }
 }
🤖 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/model/ResolvedPlayerCommand.java`
around lines 5 - 10, ResolvedPlayerCommand currently allows null values for its
components (decision, actionName, targetLabel); add a compact record constructor
in ResolvedPlayerCommand that validates each component (e.g., via
Objects.requireNonNull) and throws a clear NPE when any is null so callers fail
fast; reference the record name ResolvedPlayerCommand and component names
decision, actionName, targetLabel when making the change.
src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteDtoTest.java (1)

43-89: Add a regression test for mismatched CombatantId updates.

Given the mutable sprite state, it’s worth adding one negative test that updateFrom(...) rejects a different fighter id to prevent accidental state crossover.

Suggested test shape
+    `@Test`
+    void rejectsUpdateFromDifferentCombatantId() {
+        FighterSpriteDto sprite = FighterSpriteDto.fromSummary(
+            new CombatantSummary(CombatantId.generate(), "Wolf", 30, 45, 12, 12, true, List.of()),
+            false
+        );
+        CombatantSummary other = new CombatantSummary(
+            CombatantId.generate(), "Goblin", 20, 20, 8, 8, true, List.of()
+        );
+
+        assertThrows(IllegalArgumentException.class, () -> sprite.updateFrom(other));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteDtoTest.java` around
lines 43 - 89, Add a negative unit test that constructs a FighterSpriteDto via
FighterSpriteDto.fromSummary and then calls sprite.updateFrom(...) with a
CombatantSummary whose CombatantId differs from the sprite's original id,
asserting that updateFrom throws an exception (e.g., IllegalArgumentException)
and does not mutate sprite state; reference the methods/classes
CombatantId.generate(), FighterSpriteDto.fromSummary(...), and
FighterSpriteDto.updateFrom(...) so the test fails if updateFrom accepts
summaries for a different CombatantId.
src/main/java/sc2002/turnbased/ui/gui/view/BattleSetupPanel.java (1)

93-99: Redundant setEnabled calls after tree traversal.

Lines 96-97 explicitly set startButton and secondWaveCheck enabled state, but setComponentTreeEnabled on line 95 already traversed and set all children including these. If the intent is to ensure specific state, the current code works but is confusing. If these should have different behavior than the tree traversal, consider documenting why.

🤖 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/view/BattleSetupPanel.java` around
lines 93 - 99, The method setSetupControlsEnabled currently calls
setComponentTreeEnabled(this, enabled) which already sets enabled state for all
child components, making the explicit startButton.setEnabled(enabled) and
secondWaveCheck.setEnabled(enabled) redundant and confusing; remove those two
explicit calls (or, if they require special behavior different from the tree
traversal, document that intention and implement the differing logic
explicitly), and keep battleRunning toggle and updateSecondWaveEnabled() as-is
to preserve behavior in setSetupControlsEnabled.
src/main/java/sc2002/turnbased/ui/gui/playback/BattlePlaybackController.java (2)

12-18: Add a class-level Javadoc noting EDT-only usage.

This controller relies on javax.swing.Timer which fires callbacks on the EDT. Document that all public methods (enqueue, playNextIfIdle, reset) must be called from the EDT to avoid race conditions on the active flag and events queue.

🤖 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/BattlePlaybackController.java`
around lines 12 - 18, Add a class-level Javadoc to BattlePlaybackController that
states the class is EDT-only: mention it uses javax.swing.Timer which fires on
the Event Dispatch Thread and therefore all public methods (enqueue,
playNextIfIdle, reset) must be invoked from the EDT to avoid race conditions on
the events queue and the active flag; include a short example note suggesting
SwingUtilities.isEventDispatchThread() checks or
SwingUtilities.invokeLater/invokeAndWait when calling these methods.

35-56: Consider exception safety in the playback loop.

If eventPlayer.accept(event) on line 47 throws an exception, the timer is still created and started, but the exception propagates and the current event's playback state may be inconsistent. Consider wrapping the event dispatch in a try-catch to ensure active is reset on failure, preventing the queue from getting stuck.

🛡️ Proposed defensive fix
     public void playNextIfIdle() {
         if (active) {
             return;
         }

         BattleEvent event = events.poll();
         if (event == null) {
             drainedCallback.run();
             return;
         }

         active = true;
-        eventPlayer.accept(event);
+        try {
+            eventPlayer.accept(event);
+        } catch (RuntimeException ex) {
+            active = false;
+            throw ex;
+        }
         playbackTimer = new Timer(dialogueFormatter.playbackDelayMillis(event), e -> {
🤖 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/BattlePlaybackController.java`
around lines 35 - 56, playNextIfIdle can leave the controller in an inconsistent
state if eventPlayer.accept(event) throws; wrap the dispatch in a try/catch
(catch Throwable) so you always reset active to false and stop/clear
playbackTimer if it was created/started, and then either call playNextIfIdle()
to continue processing or rethrow the exception after cleanup; update references
in this method (playNextIfIdle, eventPlayer.accept, active, playbackTimer,
dialogueFormatter.playbackDelayMillis, drainedCallback, events.poll) to ensure
timer is not left running and active is never stuck true on exception.
src/main/java/sc2002/turnbased/ui/gui/view/BattleCommandPanel.java (2)

310-331: Timer may leak if component is removed during animation.

The messageTimer is created in animateMessage() but only stopped by explicit calls to stopMessageAnimation(). If the panel is removed from the component hierarchy while an animation is running, the timer continues firing and holds a reference to this panel, preventing garbage collection.

🛡️ Proposed fix: stop timer on component removal

Add a cleanup listener in the constructor:

addAncestorListener(new javax.swing.event.AncestorListener() {
    `@Override`
    public void ancestorRemoved(javax.swing.event.AncestorEvent event) {
        stopMessageAnimation();
    }
    `@Override`
    public void ancestorAdded(javax.swing.event.AncestorEvent event) {}
    `@Override`
    public void ancestorMoved(javax.swing.event.AncestorEvent event) {}
});
🤖 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/view/BattleCommandPanel.java` around
lines 310 - 331, The Timer started in animateMessage(...) can keep firing after
this panel is removed, leaking the panel; ensure the timer is stopped when the
component is removed by adding a cleanup hook in the panel's constructor that
calls stopMessageAnimation() on removal (e.g., attach an AncestorListener and
call stopMessageAnimation() from ancestorRemoved), leaving animateMessage(...)
and stopMessageAnimation() unchanged except they will now be reliably stopped
when the component is removed.

339-344: escapeHtml() doesn't escape quotes or newlines.

While &, <, and > are escaped, double quotes (") and newlines are not. If messages contain quotes, they could break attribute contexts. Consider also converting newlines to <br> for proper HTML rendering.

♻️ Proposed enhancement
 private static String escapeHtml(String text) {
     return text
         .replace("&", "&amp;")
         .replace("<", "&lt;")
-        .replace(">", "&gt;");
+        .replace(">", "&gt;")
+        .replace("\"", "&quot;")
+        .replace("\n", "<br>");
 }
🤖 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/view/BattleCommandPanel.java` around
lines 339 - 344, The escapeHtml method in BattleCommandPanel currently only
replaces &, < and >; update escapeHtml(String text) to also replace the
double-quote character (") with &quot; and normalize/convert newlines to HTML
line breaks (e.g., replace "\r\n" and "\n" with "<br/>") so messages can't break
attribute contexts and render newlines correctly; keep the existing replacements
and ensure the replacements are applied in a safe order (escape ampersand first)
within escapeHtml.
src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java (2)

508-508: Calling layoutEnemies() inside paintComponent() may cause visual inconsistencies.

layoutEnemies() modifies sprite positions (sprite.x, sprite.y) during the paint cycle. If multiple repaints occur rapidly, this could cause sprites to appear to "jump" or render inconsistently. Paint methods should ideally be side-effect free.

Consider moving the layout call to state-changing methods only (e.g., applyBattleEvent(), showPlayerTurn()) where it's already called, and removing it from paintComponent().

♻️ Proposed fix
     `@Override`
     protected void paintComponent(Graphics graphics) {
         super.paintComponent(graphics);
         Graphics2D g = (Graphics2D) graphics.create();
         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
         g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
-        layoutEnemies();
         drawBackground(g);
🤖 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/view/ArenaScenePanel.java` at line 508,
The call to layoutEnemies() inside paintComponent() causes side effects
(mutating sprite.x/y) during painting; remove that call from paintComponent()
and ensure layoutEnemies() is invoked only from state-changing methods that
already trigger repaints (e.g., applyBattleEvent(), showPlayerTurn(), any other
methods that change enemy/state before calling repaint()). Update callers so
they call layoutEnemies() immediately after state changes and before calling
repaint(), and verify paintComponent() remains side-effect free and only reads
sprite positions for rendering.

636-640: Enemy type detection by name is fragile.

Determining sprite type via sprite.name.toLowerCase().contains("wolf") is brittle - a player named "Wolfgang" or an enemy named "Wolfsbane Goblin" would incorrectly render as a wolf. Consider using a dedicated type field in FighterSpriteDto instead.

🤖 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/view/ArenaScenePanel.java` around lines
636 - 640, Replace fragile name-based detection with an explicit type field on
the sprite DTO: add a type (preferably an enum like FighterType {WOLF, GOBLIN,
PLAYER, ...} or a String) to FighterSpriteDto and populate it where
FighterSpriteDto instances are constructed, then update ArenaScenePanel.draw
logic to switch on sprite.getType() instead of sprite.name.contains(...),
calling drawWolfSprite(copy, sprite) for WOLF and drawGoblinSprite(copy, sprite)
for GOBLIN; ensure null/default handling for unknown types so existing names
don’t misclassify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/sc2002/turnbased/ui/gui/command/PlayerCommandResolver.java`:
- Around line 79-89: In findLivingEnemy(PlayerTurnRequest turn, CombatantId
selectedTarget) remove the redundant enemy.isAlive() condition because
turn.livingEnemies() already yields only living combatants; simply match
enemy.combatantId().equals(selectedTarget) and return that enemy (keeping the
null checks and fallback return null intact). Update the method body in
PlayerCommandResolver accordingly so it iterates turn.livingEnemies(), compares
IDs, and returns the match without calling Combatant.isAlive().

In `@src/main/java/sc2002/turnbased/ui/gui/model/BattleSessionModel.java`:
- Around line 51-56: The method takeQueuedPlayerTurn in BattleSessionModel
performs a non-atomic read-modify-write on the fields queuedPlayerTurn and
activePlayerTurn; make the operation atomic by synchronizing it (e.g., declare
takeQueuedPlayerTurn synchronized or wrap its body in a synchronized(this)
block) or by using an AtomicReference for queuedPlayerTurn and performing a
single atomic get-and-set while also updating activePlayerTurn inside the same
synchronized/atomic operation so no other thread can observe or mutate the
fields mid-update.
- Around line 5-9: The BattleSessionModel has race conditions: make its shared
state thread-safe by adding synchronization; specifically mark the mutable
fields (battleRunning, queuedPlayerTurn, activePlayerTurn, queuedPostGameConfig)
as volatile and/or make all public mutating and reading methods (beginBattle(),
endBattle(), queuePlayerTurn(), takeQueuedPlayerTurn(), onPlaybackDrained(), and
any methods called by BattleController.awaitPlayerDecision()) synchronized so
the check-then-set in beginBattle() becomes atomic and queuedPlayerTurn
visibility is guaranteed across the battle-engine thread and the Swing EDT;
ensure paired read/write accesses use the same synchronization strategy to
eliminate TOCTOU and visibility issues.

In `@src/main/java/sc2002/turnbased/ui/gui/model/PlayerTurnRequest.java`:
- Around line 10-15: Add a canonical constructor to the PlayerTurnRequest record
that defensively validates and snapshots inputs: use Objects.requireNonNull(...)
for player, livingEnemies, and responseQueue, and assign this.livingEnemies =
List.copyOf(livingEnemies) to take an immutable snapshot (so later mutations of
the original list don't affect the request); leave player and responseQueue
as-is after null checks (or wrap/validate them similarly if needed).

In `@src/main/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequest.java`:
- Around line 76-77: The describeCustomConfiguration() method risks
IndexOutOfBoundsException by directly calling config.selectedItems().get(0) and
get(1); update it to defensively handle variable-length selectedItems() (e.g.,
check size() before accessing indices) and produce a safe description when there
are 0, 1, or more items (for example, use a loop or join all item display names
instead of assuming two). Locate describeCustomConfiguration() and replace the
direct get(0)/get(1) usage with bounds-checked logic that falls back to an
appropriate message when items are missing and concatenates all available item
display names when present.

In `@src/main/java/sc2002/turnbased/ui/gui/TurnBasedArenaGui.java`:
- Around line 189-192: The exitGame() method currently calls System.exit(0)
which can bypass cleanup; update exitGame() in TurnBasedArenaGui to first invoke
controller.shutdown() (or check for null and handle exceptions) and wait/ensure
it completes before calling System.exit(0), so calls from handlePostGameChoice()
or elsewhere run the same shutdown path as the WindowAdapter; ensure you
reference the controller instance used by the WindowAdapter and perform graceful
shutdown (e.g., call controller.shutdown() then System.exit(0)).

In `@src/main/java/sc2002/turnbased/ui/gui/util/SwingThread.java`:
- Around line 21-27: The catch block handling InvocationTargetException in
SwingThread currently unwraps RuntimeException but wraps Error causes in an
IllegalStateException; change it to rethrow Error instances directly as well. In
the InvocationTargetException catch (the code that gets the Throwable cause and
currently has "if (cause instanceof RuntimeException runtimeException) { throw
runtimeException; } throw new IllegalStateException(cause);"), add a branch that
checks "if (cause instanceof Error) throw (Error) cause;" before wrapping, so
Errors are propagated unchanged while still unwrapping RuntimeExceptions and
wrapping other Throwables.

In `@src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java`:
- Around line 87-88: The Timer created in the ArenaScenePanel constructor is
started but not stored or stopped, which prevents the panel from being GC'd;
make the Timer a private field (e.g. private Timer tickTimer), assign the new
Timer(16, this::onTick) to that field in the constructor, and stop it when the
panel is removed or no longer displayable by overriding removeNotify() (or
adding a HierarchyListener/AncestorListener) to call tickTimer.stop() (and null
it) so the background ticking ceases and references are released; ensure onTick
remains unchanged.

In `@src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteDto.java`:
- Around line 85-105: The updateFrom(Combatant) and updateFrom(CombatantSummary)
methods copy mutable state but do not verify or update the sprite identity,
risking cross-combatant corruption; modify both methods to either validate the
source identity (compare this.id with combatant.getId()/summary.getId() and
throw/ignore if mismatched) or explicitly update this.id from the source before
copying fields so the DTO and source remain consistent; locate the two
updateFrom methods in FighterSpriteDto and implement one of these
identity-guarding behaviors consistently for both overloads.

In `@src/test/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequestTest.java`:
- Around line 61-62: The intro string currently always uses "enemies total";
update the pluralization in the code that builds wave summaries in
BattleLaunchRequest (the method that produces intro(), around the logic that
appends "enemies total") so that it uses "enemy total" when the count equals 1
and "enemies total" otherwise, and then change the test assertion in
BattleLaunchRequestTest to expect "Wave 2: 1 Goblin - 1 enemy total" instead of
"1 enemies total".

---

Nitpick comments:
In `@src/main/java/sc2002/turnbased/ui/gui/model/ResolvedPlayerCommand.java`:
- Around line 5-10: ResolvedPlayerCommand currently allows null values for its
components (decision, actionName, targetLabel); add a compact record constructor
in ResolvedPlayerCommand that validates each component (e.g., via
Objects.requireNonNull) and throws a clear NPE when any is null so callers fail
fast; reference the record name ResolvedPlayerCommand and component names
decision, actionName, targetLabel when making the change.

In
`@src/main/java/sc2002/turnbased/ui/gui/playback/BattlePlaybackController.java`:
- Around line 12-18: Add a class-level Javadoc to BattlePlaybackController that
states the class is EDT-only: mention it uses javax.swing.Timer which fires on
the Event Dispatch Thread and therefore all public methods (enqueue,
playNextIfIdle, reset) must be invoked from the EDT to avoid race conditions on
the events queue and the active flag; include a short example note suggesting
SwingUtilities.isEventDispatchThread() checks or
SwingUtilities.invokeLater/invokeAndWait when calling these methods.
- Around line 35-56: playNextIfIdle can leave the controller in an inconsistent
state if eventPlayer.accept(event) throws; wrap the dispatch in a try/catch
(catch Throwable) so you always reset active to false and stop/clear
playbackTimer if it was created/started, and then either call playNextIfIdle()
to continue processing or rethrow the exception after cleanup; update references
in this method (playNextIfIdle, eventPlayer.accept, active, playbackTimer,
dialogueFormatter.playbackDelayMillis, drainedCallback, events.poll) to ensure
timer is not left running and active is never stuck true on exception.

In `@src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java`:
- Line 508: The call to layoutEnemies() inside paintComponent() causes side
effects (mutating sprite.x/y) during painting; remove that call from
paintComponent() and ensure layoutEnemies() is invoked only from state-changing
methods that already trigger repaints (e.g., applyBattleEvent(),
showPlayerTurn(), any other methods that change enemy/state before calling
repaint()). Update callers so they call layoutEnemies() immediately after state
changes and before calling repaint(), and verify paintComponent() remains
side-effect free and only reads sprite positions for rendering.
- Around line 636-640: Replace fragile name-based detection with an explicit
type field on the sprite DTO: add a type (preferably an enum like FighterType
{WOLF, GOBLIN, PLAYER, ...} or a String) to FighterSpriteDto and populate it
where FighterSpriteDto instances are constructed, then update
ArenaScenePanel.draw logic to switch on sprite.getType() instead of
sprite.name.contains(...), calling drawWolfSprite(copy, sprite) for WOLF and
drawGoblinSprite(copy, sprite) for GOBLIN; ensure null/default handling for
unknown types so existing names don’t misclassify.

In `@src/main/java/sc2002/turnbased/ui/gui/view/BattleCommandPanel.java`:
- Around line 310-331: The Timer started in animateMessage(...) can keep firing
after this panel is removed, leaking the panel; ensure the timer is stopped when
the component is removed by adding a cleanup hook in the panel's constructor
that calls stopMessageAnimation() on removal (e.g., attach an AncestorListener
and call stopMessageAnimation() from ancestorRemoved), leaving
animateMessage(...) and stopMessageAnimation() unchanged except they will now be
reliably stopped when the component is removed.
- Around line 339-344: The escapeHtml method in BattleCommandPanel currently
only replaces &, < and >; update escapeHtml(String text) to also replace the
double-quote character (") with &quot; and normalize/convert newlines to HTML
line breaks (e.g., replace "\r\n" and "\n" with "<br/>") so messages can't break
attribute contexts and render newlines correctly; keep the existing replacements
and ensure the replacements are applied in a safe order (escape ampersand first)
within escapeHtml.

In `@src/main/java/sc2002/turnbased/ui/gui/view/BattleSetupPanel.java`:
- Around line 93-99: The method setSetupControlsEnabled currently calls
setComponentTreeEnabled(this, enabled) which already sets enabled state for all
child components, making the explicit startButton.setEnabled(enabled) and
secondWaveCheck.setEnabled(enabled) redundant and confusing; remove those two
explicit calls (or, if they require special behavior different from the tree
traversal, document that intention and implement the differing logic
explicitly), and keep battleRunning toggle and updateSecondWaveEnabled() as-is
to preserve behavior in setSetupControlsEnabled.

In `@src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteDtoTest.java`:
- Around line 43-89: Add a negative unit test that constructs a FighterSpriteDto
via FighterSpriteDto.fromSummary and then calls sprite.updateFrom(...) with a
CombatantSummary whose CombatantId differs from the sprite's original id,
asserting that updateFrom throws an exception (e.g., IllegalArgumentException)
and does not mutate sprite state; reference the methods/classes
CombatantId.generate(), FighterSpriteDto.fromSummary(...), and
FighterSpriteDto.updateFrom(...) so the test fails if updateFrom accepts
summaries for a different CombatantId.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83a12c39-ec62-4ceb-b686-405a888f8cd6

📥 Commits

Reviewing files that changed from the base of the PR and between ce38809 and 69e04e7.

📒 Files selected for processing (24)
  • README.md
  • src/main/java/sc2002/turnbased/ui/gui/GuiPlayerDecisionProvider.java
  • src/main/java/sc2002/turnbased/ui/gui/TurnBasedArenaGui.java
  • src/main/java/sc2002/turnbased/ui/gui/command/PlayerCommandResolver.java
  • src/main/java/sc2002/turnbased/ui/gui/controller/BattleController.java
  • src/main/java/sc2002/turnbased/ui/gui/controller/GuiPlayerDecisionProvider.java
  • src/main/java/sc2002/turnbased/ui/gui/model/BattleSessionModel.java
  • src/main/java/sc2002/turnbased/ui/gui/model/PlayerTurnRequest.java
  • src/main/java/sc2002/turnbased/ui/gui/model/ResolvedPlayerCommand.java
  • src/main/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatter.java
  • src/main/java/sc2002/turnbased/ui/gui/playback/BattlePlaybackController.java
  • src/main/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequest.java
  • src/main/java/sc2002/turnbased/ui/gui/util/SwingThread.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/BattleCommandPanel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/BattleSetupPanel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/BattleView.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteDto.java
  • src/main/java/sc2002/turnbased/ui/gui/view/PostGameChoice.java
  • src/test/java/sc2002/turnbased/ui/gui/command/PlayerCommandResolverTest.java
  • src/test/java/sc2002/turnbased/ui/gui/model/BattleSessionModelTest.java
  • src/test/java/sc2002/turnbased/ui/gui/playback/BattleDialogueFormatterTest.java
  • src/test/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequestTest.java
  • src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteDtoTest.java
💤 Files with no reviewable changes (1)
  • src/main/java/sc2002/turnbased/ui/gui/GuiPlayerDecisionProvider.java

Comment thread src/main/java/sc2002/turnbased/ui/gui/model/BattleSessionModel.java Outdated
Comment thread src/main/java/sc2002/turnbased/ui/gui/model/BattleSessionModel.java Outdated
Comment thread src/main/java/sc2002/turnbased/ui/gui/model/PlayerTurnRequest.java
Comment thread src/main/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequest.java Outdated
Comment thread src/main/java/sc2002/turnbased/ui/gui/TurnBasedArenaGui.java
Comment thread src/main/java/sc2002/turnbased/ui/gui/util/SwingThread.java
Comment thread src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java Outdated
Comment thread src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteDto.java
Comment thread src/test/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequestTest.java Outdated
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.

Actionable comments posted: 2

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

9-9: Use a typed post-game configuration instead of raw Object.

queuedPostGameConfig currently accepts any runtime type, which increases cast-risk at consumers. A typed model (or generic parameter) will make this contract safer.

♻️ Possible direction
-public final class BattleSessionModel {
+public final class BattleSessionModel<TPostGameConfig> {
...
-    private Object queuedPostGameConfig;
+    private TPostGameConfig queuedPostGameConfig;
...
-    public synchronized void queuePostGame(Object configuration) {
+    public synchronized void queuePostGame(TPostGameConfig configuration) {
         queuedPostGameConfig = configuration;
     }
...
-    public synchronized Optional<Object> takeQueuedPostGameConfig() {
-        Object configuration = queuedPostGameConfig;
+    public synchronized Optional<TPostGameConfig> takeQueuedPostGameConfig() {
+        TPostGameConfig configuration = queuedPostGameConfig;
         queuedPostGameConfig = null;
         return Optional.ofNullable(configuration);
     }

Also applies to: 58-66

🤖 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/model/BattleSessionModel.java` at line
9, Replace the raw Object typed field queuedPostGameConfig in BattleSessionModel
with a concrete, typed post-game configuration (e.g., a PostGameConfig class) or
make BattleSessionModel generic (e.g., BattleSessionModel<TPostGameConfig>) so
the field is strongly typed; update the field declaration, constructor,
getter/setter methods that reference queuedPostGameConfig and any usages around
the referenced block (the consumer code at the other affected lines) to use the
new concrete type or generic parameter and adjust casts/assignments accordingly
so consumers no longer rely on unchecked casts.
src/main/java/sc2002/turnbased/ui/gui/controller/BattleController.java (1)

140-145: Silent failure when offer() returns false.

If responseQueue.offer() fails (returns false), the resolved command is silently discarded. While this shouldn't happen under normal operation since the queue is drained by take(), a defensive log statement would help diagnose unexpected states.

♻️ Suggested logging for defensive debugging
         ResolvedPlayerCommand playerCommand = resolved.get();
-        if (activeTurn.get().responseQueue().offer(playerCommand.decision())) {
+        boolean offered = activeTurn.get().responseQueue().offer(playerCommand.decision());
+        if (offered) {
             model.clearActivePlayerTurn();
             view.showCommandResolving(playerCommand.actionName());
             view.appendLog(">> " + playerCommand.actionName() + playerCommand.targetLabel());
+        } else {
+            // This indicates a bug: multiple decisions attempted for the same turn
+            view.appendLog("Warning: command ignored - decision already submitted");
         }
🤖 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/controller/BattleController.java`
around lines 140 - 145, The block in BattleController that calls
activeTurn.get().responseQueue().offer(playerCommand.decision()) drops the
command silently when offer() returns false; add a defensive branch to handle
the false case by logging a warning (e.g., via view.appendLog or the controller
logger) that includes the ResolvedPlayerCommand.actionName() and targetLabel(),
and avoid clearing state unless the offer succeeded (so move
model.clearActivePlayerTurn(), view.showCommandResolving(), and
view.appendLog(...) into the true branch only); this will ensure unexpected
queue-rejections are visible for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteHudRenderer.java`:
- Line 13: The HP ratio calculation in FighterSpriteHudRenderer uses double
ratio = sprite.maxHp == 0 ? 0 : Math.max(0, sprite.hp / (double) sprite.maxHp)
which prevents negative values but not values >1, allowing overheal to overdraw
the bar; change the computation to also cap the upper bound (e.g., wrap the
computed value with Math.min(1.0, ...)) so ratio is always between 0 and 1 while
preserving the maxHp==0 guard and the use of sprite.hp/sprite.maxHp.

In `@src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteRenderer.java`:
- Around line 66-69: The bodyRendererFor method can return null if the injected
bodyRenderers map lacks FighterType.UNKNOWN; either validate in the class
constructor (the package-private constructor that accepts bodyRenderers) that
bodyRenderers.containsKey(FighterType.UNKNOWN) and throw an
IllegalArgumentException if missing, or change bodyRendererFor to ensure a safe
non-null fallback by resolving sprite.type, then falling back to
bodyRenderers.get(FighterType.UNKNOWN) and if that is still null throw a clear
IllegalStateException with context (mentioning FighterSpriteDto and
FighterType.UNKNOWN) so callers don't get a NullPointerException when calling
renderBody on the result.

---

Nitpick comments:
In `@src/main/java/sc2002/turnbased/ui/gui/controller/BattleController.java`:
- Around line 140-145: The block in BattleController that calls
activeTurn.get().responseQueue().offer(playerCommand.decision()) drops the
command silently when offer() returns false; add a defensive branch to handle
the false case by logging a warning (e.g., via view.appendLog or the controller
logger) that includes the ResolvedPlayerCommand.actionName() and targetLabel(),
and avoid clearing state unless the offer succeeded (so move
model.clearActivePlayerTurn(), view.showCommandResolving(), and
view.appendLog(...) into the true branch only); this will ensure unexpected
queue-rejections are visible for debugging.

In `@src/main/java/sc2002/turnbased/ui/gui/model/BattleSessionModel.java`:
- Line 9: Replace the raw Object typed field queuedPostGameConfig in
BattleSessionModel with a concrete, typed post-game configuration (e.g., a
PostGameConfig class) or make BattleSessionModel generic (e.g.,
BattleSessionModel<TPostGameConfig>) so the field is strongly typed; update the
field declaration, constructor, getter/setter methods that reference
queuedPostGameConfig and any usages around the referenced block (the consumer
code at the other affected lines) to use the new concrete type or generic
parameter and adjust casts/assignments accordingly so consumers no longer rely
on unchecked casts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f483eec-8674-4d32-9378-fb182415fcbf

📥 Commits

Reviewing files that changed from the base of the PR and between 69e04e7 and 6537c01.

📒 Files selected for processing (30)
  • src/main/java/sc2002/turnbased/ui/gui/TurnBasedArenaGui.java
  • src/main/java/sc2002/turnbased/ui/gui/command/PlayerCommandResolver.java
  • src/main/java/sc2002/turnbased/ui/gui/controller/BattleController.java
  • src/main/java/sc2002/turnbased/ui/gui/model/BattleSessionModel.java
  • src/main/java/sc2002/turnbased/ui/gui/model/PlayerTurnRequest.java
  • src/main/java/sc2002/turnbased/ui/gui/model/ResolvedPlayerCommand.java
  • src/main/java/sc2002/turnbased/ui/gui/playback/BattlePlaybackController.java
  • src/main/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequest.java
  • src/main/java/sc2002/turnbased/ui/gui/util/SwingThread.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/BattleCommandPanel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/BattleSetupPanel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterBodyRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteDto.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteEffectRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteHudRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterType.java
  • src/main/java/sc2002/turnbased/ui/gui/view/GoblinBodyRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/UnknownFighterBodyRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/WarriorBodyRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/WizardBodyRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/WolfBodyRenderer.java
  • src/test/java/sc2002/turnbased/ui/gui/model/PlayerTurnRequestTest.java
  • src/test/java/sc2002/turnbased/ui/gui/model/ResolvedPlayerCommandTest.java
  • src/test/java/sc2002/turnbased/ui/gui/playback/BattlePlaybackControllerTest.java
  • src/test/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequestTest.java
  • src/test/java/sc2002/turnbased/ui/gui/view/BattleCommandPanelTest.java
  • src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteDtoTest.java
  • src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteRendererTest.java
✅ Files skipped from review due to trivial changes (5)
  • src/main/java/sc2002/turnbased/ui/gui/view/UnknownFighterBodyRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/WizardBodyRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/WarriorBodyRenderer.java
  • src/test/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequestTest.java
  • src/main/java/sc2002/turnbased/ui/gui/command/PlayerCommandResolver.java
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/main/java/sc2002/turnbased/ui/gui/model/PlayerTurnRequest.java
  • src/main/java/sc2002/turnbased/ui/gui/model/ResolvedPlayerCommand.java
  • src/main/java/sc2002/turnbased/ui/gui/playback/BattlePlaybackController.java
  • src/main/java/sc2002/turnbased/ui/gui/util/SwingThread.java
  • src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteDtoTest.java
  • src/main/java/sc2002/turnbased/ui/gui/view/BattleSetupPanel.java
  • src/main/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequest.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteDto.java

Comment thread src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteHudRenderer.java Outdated
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.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java (2)

119-130: Graphics state not fully restored after drawFloatingTexts.

The composite is reset to AlphaComposite.SrcOver at line 129, which is good. However, the font set at line 120 persists. While drawOverlay immediately sets its own font (line 133), explicitly restoring graphics state would be more defensive.

Additionally, passing System.nanoTime() directly at line 24 creates a slight time skew from when model.floatingTexts() were created/updated. Consider accepting now as a parameter to render() for consistency.

🤖 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/view/ArenaSceneRenderer.java` around
lines 119 - 130, Restore the Graphics2D state after drawFloatingTexts by saving
the original Font and Composite at the start of
ArenaSceneRenderer.drawFloatingTexts and restoring them before returning (so the
font set at line 120 does not leak); also change the render flow to accept a
consistent timestamp by passing the same now value into render() and through to
drawFloatingTexts instead of calling System.nanoTime() in different places to
avoid time skew (update the render(...) signature and callers to forward the now
parameter to drawFloatingTexts).

88-99: Minor: Color reuse inside the loop may cause visual inconsistency.

Lines 95-97 set a different color for some elements but then reset to the original color. However, each iteration reuses the loop variable x, and the color change at line 95 persists to the next iteration's first fillRect calls (lines 92-94). This appears intentional for visual variety but could be clarified with a comment.

🤖 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/view/ArenaSceneRenderer.java` around
lines 88 - 99, In drawRuins(Graphics2D g, int width, int floorTop) the loop
temporarily switches color for the middle stripe (using new Color(120, 58, 58,
150)) then resets, which can be confusing because the color change could persist
between iterations; update the loop to either explicitly set the desired color
before every fillRect call (so colors are deterministic) or add a concise inline
comment explaining the intentional alternating color pattern (reference:
drawRuins, loop variable x and the Color(...) used for the middle stripe) to
make the intent clear.
src/test/java/sc2002/turnbased/ui/gui/view/ArenaSceneModelTest.java (1)

99-113: Consider adding edge case tests.

The current tests cover happy-path scenarios well. Consider adding tests for:

  • selectNextEnemy when all enemies are dead
  • selectEnemyAt with invalid/out-of-bounds points
  • showSetupPreview state reset behavior

These are optional enhancements for improved coverage.

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

In `@src/test/java/sc2002/turnbased/ui/gui/view/ArenaSceneModelTest.java` around
lines 99 - 113, Add unit tests in ArenaSceneModelTest to cover the suggested
edge cases: create a scenario (use setupWithTwoEnemies and spriteById helpers)
where all enemy combatants are marked dead and assert selectNextEnemy returns
the expected “none” behavior (e.g., empty optional or sentinel) and does not
throw; add tests for selectEnemyAt feeding out-of-bounds/invalid Points and
assert it returns no selection and is safe; and add tests for showSetupPreview
that toggle preview on then off and verify model state (selected sprite,
highlighted indices, or preview flag) resets to the initial state. Use the
existing helper methods (setupWithTwoEnemies, spriteById, ArenaSceneModel) to
locate where to wire these tests.
src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneModel.java (1)

102-111: Minor redundancy: selectedEnemyId() calls chooseDefaultTarget() which may silently change state.

The selectedEnemyId() getter has a side effect of potentially updating selectedEnemyId via chooseDefaultTarget(). This could be unexpected behavior for a method that appears to be a simple accessor. Consider documenting this behavior or separating the validation/fallback logic.

🤖 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/view/ArenaSceneModel.java` around lines
102 - 111, The getter selectedEnemyId() currently performs validation and may
mutate state by calling chooseDefaultTarget(), which is surprising for an
accessor; change this by separating concerns: make selectedEnemyId() a pure
accessor that just returns the field, and move the validation/fallback logic
into a new explicit method (e.g. validateOrChooseDefaultTarget() or keep
chooseDefaultTarget() but call it from places that perform selection state
updates such as the UI controller or when starting a turn). Update callers that
relied on side effects to invoke the new validation method explicitly; reference
selectedEnemyId (field), selectedEnemyId() (method), chooseDefaultTarget(),
sprites, and FighterSpriteDto to locate affected code. Ensure any retained
public API is documented to reflect whether it mutates state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneLoop.java`:
- Around line 19-20: Wrap the call to tickCallback.run() inside the Swing Timer
action handler in a try-catch inside ArenaSceneLoop so any Throwable is caught;
on exception, log the error (include the exception) and stop the tickTimer (call
tickTimer.stop() or your existing loop-stop method) to prevent EDT shutdown and
cascading failures. Ensure you reference the tickCallback.run() invocation and
the tickTimer variable when making the change.

In `@src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneModel.java`:
- Around line 302-303: The code in ArenaSceneModel currently treats sprite.x and
sprite.y == 0 as "unset", which wrongly resets valid coordinates at 0; modify
the logic to use Double.isNaN for unset detection (e.g., replace checks like
sprite.x == 0 and sprite.y == 0 with Double.isNaN(sprite.x) and
Double.isNaN(sprite.y) in ArenaSceneModel where oldX/oldY are computed and the
corresponding later block), and initialize the position fields to Double.NaN in
FighterSpriteDto so NaN is the sentinel for "uninitialized" rather than 0.0.

---

Nitpick comments:
In `@src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneModel.java`:
- Around line 102-111: The getter selectedEnemyId() currently performs
validation and may mutate state by calling chooseDefaultTarget(), which is
surprising for an accessor; change this by separating concerns: make
selectedEnemyId() a pure accessor that just returns the field, and move the
validation/fallback logic into a new explicit method (e.g.
validateOrChooseDefaultTarget() or keep chooseDefaultTarget() but call it from
places that perform selection state updates such as the UI controller or when
starting a turn). Update callers that relied on side effects to invoke the new
validation method explicitly; reference selectedEnemyId (field),
selectedEnemyId() (method), chooseDefaultTarget(), sprites, and FighterSpriteDto
to locate affected code. Ensure any retained public API is documented to reflect
whether it mutates state.

In `@src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java`:
- Around line 119-130: Restore the Graphics2D state after drawFloatingTexts by
saving the original Font and Composite at the start of
ArenaSceneRenderer.drawFloatingTexts and restoring them before returning (so the
font set at line 120 does not leak); also change the render flow to accept a
consistent timestamp by passing the same now value into render() and through to
drawFloatingTexts instead of calling System.nanoTime() in different places to
avoid time skew (update the render(...) signature and callers to forward the now
parameter to drawFloatingTexts).
- Around line 88-99: In drawRuins(Graphics2D g, int width, int floorTop) the
loop temporarily switches color for the middle stripe (using new Color(120, 58,
58, 150)) then resets, which can be confusing because the color change could
persist between iterations; update the loop to either explicitly set the desired
color before every fillRect call (so colors are deterministic) or add a concise
inline comment explaining the intentional alternating color pattern (reference:
drawRuins, loop variable x and the Color(...) used for the middle stripe) to
make the intent clear.

In `@src/test/java/sc2002/turnbased/ui/gui/view/ArenaSceneModelTest.java`:
- Around line 99-113: Add unit tests in ArenaSceneModelTest to cover the
suggested edge cases: create a scenario (use setupWithTwoEnemies and spriteById
helpers) where all enemy combatants are marked dead and assert selectNextEnemy
returns the expected “none” behavior (e.g., empty optional or sentinel) and does
not throw; add tests for selectEnemyAt feeding out-of-bounds/invalid Points and
assert it returns no selection and is safe; and add tests for showSetupPreview
that toggle preview on then off and verify model state (selected sprite,
highlighted indices, or preview flag) resets to the initial state. Use the
existing helper methods (setupWithTwoEnemies, spriteById, ArenaSceneModel) to
locate where to wire these tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d86b589e-4596-4441-b679-09868c93a57d

📥 Commits

Reviewing files that changed from the base of the PR and between 6537c01 and a84bdb1.

📒 Files selected for processing (5)
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneLoop.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneModel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java
  • src/test/java/sc2002/turnbased/ui/gui/view/ArenaSceneModelTest.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java

Comment thread src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneLoop.java Outdated
Comment thread src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneModel.java Outdated
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.

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/main/java/sc2002/turnbased/ui/gui/controller/BattleController.java (2)

146-150: Include the command type in the warning log for better debuggability.

The warning log message includes the action name but not the original command that was rejected. For debugging purposes, including the command would help trace issues.

♻️ Suggested improvement
         } else {
-            view.appendLog("Warning: could not queue command "
+            view.appendLog("Warning: could not queue command (" + command + ") "
                 + playerCommand.actionName()
                 + playerCommand.targetLabel());
         }
🤖 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/controller/BattleController.java`
around lines 146 - 150, The warning in BattleController currently logs only
actionName() and targetLabel(); update the view.appendLog call to include the
original command type/representation (e.g.,
playerCommand.getClass().getSimpleName() or playerCommand.toString()) so the
rejected command is clear for debugging; change the message built in the else
branch that calls view.appendLog to concatenate a clear command identifier (type
or toString) along with actionName() and targetLabel() and include separators
for readability.

179-192: Exception handling could lose original stack trace.

In runBattle(), the catch block calls showBattleError(ex) which only logs ex.getMessage() to the view. Consider logging the full exception to a logger or console for debugging, as the message alone may not provide enough context for troubleshooting.

♻️ Suggested improvement for debugging
     private void runBattle(BattleLaunchRequest request) {
         try {
             BattleSetup setup = request.createSetup(setupFactory);
             SwingThread.runAndWait(() -> view.showBattleLoaded(setup));

             BattleEngine engine = new BattleEngine(setup, new SpeedTurnOrderStrategy());
             GuiPlayerDecisionProvider decisions = new GuiPlayerDecisionProvider(this);
             BattleEventListener listener = event -> SwingUtilities.invokeLater(() -> playbackController.enqueue(event));
             engine.runUntilBattleEnds(decisions, listener);
             SwingUtilities.invokeLater(() -> queuePostGame(request.replayConfiguration()));
         } catch (Exception ex) {
+            ex.printStackTrace(); // Or use a proper logger
             SwingUtilities.invokeLater(() -> showBattleError(ex));
         }
     }
🤖 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/controller/BattleController.java`
around lines 179 - 192, The catch block in runBattle currently only forwards ex
to showBattleError which uses ex.getMessage(), losing stack trace; update the
catch to log the full exception (e.g., using a logger or ex.printStackTrace())
before calling SwingUtilities.invokeLater(() -> showBattleError(ex)), and also
consider modifying showBattleError to accept and display or record the full
Throwable; reference: runBattle, showBattleError, SwingUtilities.invokeLater,
BattleController.
src/main/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequest.java (1)

48-61: Consider using a switch expression for exhaustive sealed-type handling.

The replay() method uses sequential instanceof checks, but since PostGameConfig is a sealed interface, a switch expression would be cleaner, more idiomatic, and would allow the compiler to verify exhaustiveness (eliminating the need for the fallback throw at line 60).

Also note the asymmetric behavior: Preset replay uses a generic "Replaying same settings" intro, while Custom replay regenerates the full configuration description. If this is intentional, consider adding a brief comment.

♻️ Suggested refactor using switch expression
     public static BattleLaunchRequest replay(PostGameConfig configuration) {
         Objects.requireNonNull(configuration, "configuration");
-        if (configuration instanceof PostGameConfig.Preset preset) {
-            return new BattleLaunchRequest(
+        return switch (configuration) {
+            case PostGameConfig.Preset preset -> new BattleLaunchRequest(
                 factory -> factory.create(preset.configuration()),
                 configuration,
                 "=== Replaying same settings ==="
             );
-        }
-        if (configuration instanceof PostGameConfig.Custom custom) {
-            return custom(custom.configuration());
-        }
-        throw new IllegalArgumentException("Unsupported replay configuration: " + configuration);
+            case PostGameConfig.Custom custom -> custom(custom.configuration());
+        };
     }
🤖 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/setup/BattleLaunchRequest.java` around
lines 48 - 61, Refactor the replay(PostGameConfig) method to use a switch
expression on the sealed type PostGameConfig (handling PostGameConfig.Preset and
PostGameConfig.Custom) instead of sequential instanceof checks: replace the
instanceof branches with a switch that returns the appropriate
BattleLaunchRequest for Preset (using factory ->
factory.create(preset.configuration()), configuration, "=== Replaying same
settings ===") and for Custom call custom(custom.configuration()); remove the
trailing IllegalArgumentException because the switch will be exhaustive enforced
by the compiler; if the different intro text for Preset vs Custom is
intentional, add a brief comment above the Preset arm explaining the asymmetry.
src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteHudRendererTest.java (1)

15-18: Consider using delta-based assertions for floating-point comparisons

Lines 15–18 use exact equality for double values. While these specific calculations (integer division and clamping) produce exactly representable results, using tolerance-based assertions is safer and more idiomatic for floating-point comparisons in JUnit.

Proposed change
-        assertEquals(1.0, FighterSpriteHudRenderer.healthRatio(sprite(130, 100)));
-        assertEquals(0.0, FighterSpriteHudRenderer.healthRatio(sprite(-10, 100)));
-        assertEquals(0.0, FighterSpriteHudRenderer.healthRatio(sprite(20, 0)));
-        assertEquals(0.4, FighterSpriteHudRenderer.healthRatio(sprite(40, 100)));
+        assertEquals(1.0, FighterSpriteHudRenderer.healthRatio(sprite(130, 100)), 1e-9);
+        assertEquals(0.0, FighterSpriteHudRenderer.healthRatio(sprite(-10, 100)), 1e-9);
+        assertEquals(0.0, FighterSpriteHudRenderer.healthRatio(sprite(20, 0)), 1e-9);
+        assertEquals(0.4, FighterSpriteHudRenderer.healthRatio(sprite(40, 100)), 1e-9);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteHudRendererTest.java`
around lines 15 - 18, The tests in FighterSpriteHudRendererTest use exact
equality for floating-point results from FighterSpriteHudRenderer.healthRatio;
change the assertions to use a delta-based assertion (e.g.,
assertEquals(expected, actual, delta)) with a small tolerance like 1e-6 to make
comparisons robust for floating-point arithmetic while keeping the same expected
values; update each assertEquals that checks healthRatio(...) accordingly.
src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteRenderer.java (1)

54-54: Use null-safe ID comparison in the selection check.

Line 54 can throw if sprite.id is unexpectedly null; Objects.equals makes this path safer.

♻️ Proposed fix
-        if (sprite.id.equals(selectedEnemyId) && sprite.alive) {
+        if (sprite.alive && Objects.equals(sprite.id, selectedEnemyId)) {
🤖 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/view/FighterSpriteRenderer.java` at
line 54, The selection check in FighterSpriteRenderer uses
sprite.id.equals(selectedEnemyId) which can NPE if sprite.id is null; update the
condition to a null-safe comparison using Objects.equals(sprite.id,
selectedEnemyId) and add the necessary import (java.util.Objects) so the
selected-enemy check remains safe and behaves identically when either ID is
null.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteRenderer.java`:
- Around line 41-66: The Graphics2D copy created in FighterSpriteRenderer is not
protected against exceptions from bodyRenderer.renderBody(copy, sprite) or
effectRenderer.renderPulse(copy, sprite), so move all operations that use "copy"
(translate, setComposite/rotate/translate, fill/draw calls,
bodyRenderer.renderBody(copy, sprite), effectRenderer.renderPulse(copy, sprite))
into a try block and call copy.dispose() in a finally block to guarantee
disposal; leave hudRenderer.render(g, sprite, (int)x, (int)y) outside the
try/finally using the original Graphics "g". Ensure you retain the existing
logic and transforms when restructuring.

---

Nitpick comments:
In `@src/main/java/sc2002/turnbased/ui/gui/controller/BattleController.java`:
- Around line 146-150: The warning in BattleController currently logs only
actionName() and targetLabel(); update the view.appendLog call to include the
original command type/representation (e.g.,
playerCommand.getClass().getSimpleName() or playerCommand.toString()) so the
rejected command is clear for debugging; change the message built in the else
branch that calls view.appendLog to concatenate a clear command identifier (type
or toString) along with actionName() and targetLabel() and include separators
for readability.
- Around line 179-192: The catch block in runBattle currently only forwards ex
to showBattleError which uses ex.getMessage(), losing stack trace; update the
catch to log the full exception (e.g., using a logger or ex.printStackTrace())
before calling SwingUtilities.invokeLater(() -> showBattleError(ex)), and also
consider modifying showBattleError to accept and display or record the full
Throwable; reference: runBattle, showBattleError, SwingUtilities.invokeLater,
BattleController.

In `@src/main/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequest.java`:
- Around line 48-61: Refactor the replay(PostGameConfig) method to use a switch
expression on the sealed type PostGameConfig (handling PostGameConfig.Preset and
PostGameConfig.Custom) instead of sequential instanceof checks: replace the
instanceof branches with a switch that returns the appropriate
BattleLaunchRequest for Preset (using factory ->
factory.create(preset.configuration()), configuration, "=== Replaying same
settings ===") and for Custom call custom(custom.configuration()); remove the
trailing IllegalArgumentException because the switch will be exhaustive enforced
by the compiler; if the different intro text for Preset vs Custom is
intentional, add a brief comment above the Preset arm explaining the asymmetry.

In `@src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteRenderer.java`:
- Line 54: The selection check in FighterSpriteRenderer uses
sprite.id.equals(selectedEnemyId) which can NPE if sprite.id is null; update the
condition to a null-safe comparison using Objects.equals(sprite.id,
selectedEnemyId) and add the necessary import (java.util.Objects) so the
selected-enemy check remains safe and behaves identically when either ID is
null.

In
`@src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteHudRendererTest.java`:
- Around line 15-18: The tests in FighterSpriteHudRendererTest use exact
equality for floating-point results from FighterSpriteHudRenderer.healthRatio;
change the assertions to use a delta-based assertion (e.g.,
assertEquals(expected, actual, delta)) with a small tolerance like 1e-6 to make
comparisons robust for floating-point arithmetic while keeping the same expected
values; update each assertEquals that checks healthRatio(...) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f611b0ea-1b3f-4ba4-bea5-36deef76ac1e

📥 Commits

Reviewing files that changed from the base of the PR and between a84bdb1 and da37336.

📒 Files selected for processing (10)
  • src/main/java/sc2002/turnbased/ui/gui/controller/BattleController.java
  • src/main/java/sc2002/turnbased/ui/gui/model/BattleSessionModel.java
  • src/main/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequest.java
  • src/main/java/sc2002/turnbased/ui/gui/setup/PostGameConfig.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteHudRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteRenderer.java
  • src/test/java/sc2002/turnbased/ui/gui/model/BattleSessionModelTest.java
  • src/test/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequestTest.java
  • src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteHudRendererTest.java
  • src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteRendererTest.java
✅ Files skipped from review due to trivial changes (1)
  • src/test/java/sc2002/turnbased/ui/gui/setup/BattleLaunchRequestTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteHudRenderer.java
  • src/test/java/sc2002/turnbased/ui/gui/view/FighterSpriteRendererTest.java
  • src/test/java/sc2002/turnbased/ui/gui/model/BattleSessionModelTest.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 (5)
src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java (2)

94-98: Remove redundant setColor call.

Line 96 sets stoneColor immediately after line 94 already set it, with no intervening draw operation.

Proposed fix
         for (int x = 44; x < width; x += 245) {
             g.setColor(stoneColor);
             g.fillRect(x, base - 94, 26, 94);
-            g.setColor(stoneColor);
             g.fillRect(x + 76, base - 118, 28, 118);
             g.setColor(stoneColor);
             g.fillRect(x - 10, base - 120, 128, 18);
🤖 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/view/ArenaSceneRenderer.java` around
lines 94 - 98, In ArenaSceneRenderer (the rendering method that draws stones)
remove the redundant g.setColor(stoneColor) call that immediately follows the
previous g.setColor(stoneColor) with no intervening drawing; simply delete the
second duplicate setColor invocation so the two fillRect calls use the single
prior setColor call.

166-176: Consider binary search or early exit for text fitting.

The fitText method performs O(n) stringWidth calls in the worst case. For typical UI text lengths this is fine, but if banner messages can be very long, consider:

  • Binary search on the end index
  • Caching font metrics

This is a minor optimization opportunity—current implementation is correct.

src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneModel.java (3)

162-167: Fragile victory/defeat detection via string prefix.

Checking banner.startsWith("Victory") or banner.startsWith("Defeat") couples battle-end logic to specific narration text. If the text changes (e.g., "You are victorious!", localization, or formatting), the battle won't properly deactivate.

Consider having NarrationEvent carry a semantic flag (e.g., isVictory(), isDefeat(), or a NarrationType enum) rather than parsing the display text.

Sketch of a cleaner approach
// In NarrationEvent, add:
public enum Type { NORMAL, VICTORY, DEFEAT }
private final Type type;

// Then in applyBattleEvent:
} else if (event instanceof NarrationEvent narrationEvent) {
    banner = narrationEvent.getText();
    if (narrationEvent.getType() == NarrationEvent.Type.VICTORY 
            || narrationEvent.getType() == NarrationEvent.Type.DEFEAT) {
        acceptingPlayerTurn = false;
        battleActive = false;
    }
}
🤖 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/view/ArenaSceneModel.java` around lines
162 - 167, Replace fragile string-prefix checks in the ArenaSceneModel by adding
a semantic type to NarrationEvent and using it to decide battle end: add a
NarrationEvent.Type enum (e.g., NORMAL, VICTORY, DEFEAT) and a getter
(getType()) on NarrationEvent, then change the logic in the block that currently
tests banner.startsWith(...) (the branch handling "NarrationEvent
narrationEvent" where banner, acceptingPlayerTurn and battleActive are set) to
check narrationEvent.getType() == Type.VICTORY || narrationEvent.getType() ==
Type.DEFEAT; update any NarrationEvent constructors/call sites to supply the
correct Type instead of relying on text content.

324-326: enemyOrder.contains() is O(n) on ArrayList.

For typical enemy counts (≤10), this is fine. If enemy counts grow significantly, consider maintaining a parallel Set<CombatantId> for O(1) membership checks.

Also applies to: 336-338

🤖 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/view/ArenaSceneModel.java` around lines
324 - 326, The code uses enemyOrder.contains(sprite.id) which is O(n) on an
ArrayList; change to maintain a parallel Set<CombatantId> (e.g., enemyOrderSet)
inside ArenaSceneModel and use enemyOrderSet.contains(sprite.id) for membership
checks, and when adding/removing IDs ensure you update both enemyOrder (preserve
list order) and enemyOrderSet (for O(1) checks); apply the same change to the
other occurrence(s) that currently call enemyOrder.contains(...) so all
membership checks use the Set and all list mutations keep the Set in sync.

200-204: Minor: spritesByDrawOrder() allocates a new sorted list per call.

This is invoked every render frame. For the typical small sprite count (~5-10), this is acceptable. If performance becomes a concern, consider caching the sorted list and invalidating on sprite add/remove/position change.

🤖 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/view/ArenaSceneModel.java` around lines
200 - 204, The spritesByDrawOrder() method currently allocates and sorts a new
list each call; change it to cache the sorted list and only recompute when
sprite state changes by adding a cached List<FighterSpriteDto> (e.g.,
cachedOrderedSprites) and a dirty flag (e.g., orderedSpritesDirty) in
ArenaSceneModel, mark orderedSpritesDirty = true whenever sprites map is mutated
or any FighterSpriteDto position/ drawY changes (update points where sprites are
added/removed or their position setters are called), and have
spritesByDrawOrder() return the cached list after sorting/rebuilding it only
when orderedSpritesDirty is true.
🤖 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/view/ArenaSceneModel.java`:
- Around line 162-167: Replace fragile string-prefix checks in the
ArenaSceneModel by adding a semantic type to NarrationEvent and using it to
decide battle end: add a NarrationEvent.Type enum (e.g., NORMAL, VICTORY,
DEFEAT) and a getter (getType()) on NarrationEvent, then change the logic in the
block that currently tests banner.startsWith(...) (the branch handling
"NarrationEvent narrationEvent" where banner, acceptingPlayerTurn and
battleActive are set) to check narrationEvent.getType() == Type.VICTORY ||
narrationEvent.getType() == Type.DEFEAT; update any NarrationEvent
constructors/call sites to supply the correct Type instead of relying on text
content.
- Around line 324-326: The code uses enemyOrder.contains(sprite.id) which is
O(n) on an ArrayList; change to maintain a parallel Set<CombatantId> (e.g.,
enemyOrderSet) inside ArenaSceneModel and use enemyOrderSet.contains(sprite.id)
for membership checks, and when adding/removing IDs ensure you update both
enemyOrder (preserve list order) and enemyOrderSet (for O(1) checks); apply the
same change to the other occurrence(s) that currently call
enemyOrder.contains(...) so all membership checks use the Set and all list
mutations keep the Set in sync.
- Around line 200-204: The spritesByDrawOrder() method currently allocates and
sorts a new list each call; change it to cache the sorted list and only
recompute when sprite state changes by adding a cached List<FighterSpriteDto>
(e.g., cachedOrderedSprites) and a dirty flag (e.g., orderedSpritesDirty) in
ArenaSceneModel, mark orderedSpritesDirty = true whenever sprites map is mutated
or any FighterSpriteDto position/ drawY changes (update points where sprites are
added/removed or their position setters are called), and have
spritesByDrawOrder() return the cached list after sorting/rebuilding it only
when orderedSpritesDirty is true.

In `@src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java`:
- Around line 94-98: In ArenaSceneRenderer (the rendering method that draws
stones) remove the redundant g.setColor(stoneColor) call that immediately
follows the previous g.setColor(stoneColor) with no intervening drawing; simply
delete the second duplicate setColor invocation so the two fillRect calls use
the single prior setColor call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bd669e14-af66-4f61-99e7-8be693eb286e

📥 Commits

Reviewing files that changed from the base of the PR and between da37336 and 635f4b6.

📒 Files selected for processing (7)
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneLoop.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneModel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneRenderer.java
  • src/main/java/sc2002/turnbased/ui/gui/view/BattleView.java
  • src/main/java/sc2002/turnbased/ui/gui/view/FighterSpriteDto.java
  • src/test/java/sc2002/turnbased/ui/gui/view/ArenaSceneModelTest.java
✅ Files skipped from review due to trivial changes (2)
  • src/test/java/sc2002/turnbased/ui/gui/view/ArenaSceneModelTest.java
  • src/main/java/sc2002/turnbased/ui/gui/view/BattleView.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaSceneLoop.java
  • src/main/java/sc2002/turnbased/ui/gui/view/ArenaScenePanel.java

@liang799 liang799 merged commit 5a6ab02 into main Apr 15, 2026
3 checks passed
@liang799 liang799 deleted the tianpok/2d-gameplay-upgrade branch April 15, 2026 07:25
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.

1 participant