Skip to content

fix(combat): dash actually extends the enforced movement budget#60

Merged
Mnehmos merged 3 commits into
mainfrom
fix/50-dash-move-budget
Apr 20, 2026
Merged

fix(combat): dash actually extends the enforced movement budget#60
Mnehmos merged 3 commits into
mainfrom
fix/50-dash-move-budget

Conversation

@Mnehmos

@Mnehmos Mnehmos commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Summary

The consolidated dash handler was a no-op stub — it returned "Movement speed doubled" but never told the engine. The next move still enforced the base 30ft budget and rejected with Insufficient movement. Observed live when a dashing zealot couldn't cross the aisle.

Fix

  • CombatEngine.applyDash() — adds a fresh baseSpeed allotment on top of any existing movementRemaining, flags hasDashed, and consumes the main action (5e dash rules).
  • dash handler in combat-action.ts calls applyDash and echoes the new movementRemaining so callers can see their budget.

Test plan

  • Existing dash test kept passing (effect string still contains "doubled").
  • New regression test: dash then diagonal move that would exceed 30ft succeeds with the 60ft budget.
  • combat-action suite: 26/26
  • Full suite: 1890 passed, 6 skipped, 0 failed

Closes #50

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Dash now actually increases a participant’s available movement, enforces once-per-turn use, and rejects dash if the main action is already spent.
    • Handler recovers encounter state when in-memory engine is missing and returns structured errors when the encounter or dash cannot be applied.
  • Tests
    • Added regression tests covering action-economy rejection, persist/reload behavior, and doubled movement verification.

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Mnehmos has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 57 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 4 minutes and 57 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d26657d-04c0-44cd-a02c-93c495b408a4

📥 Commits

Reviewing files that changed from the base of the PR and between ebf6fe9 and fa46dfc.

📒 Files selected for processing (2)
  • src/server/consolidated/combat-action.ts
  • tests/server/consolidated/combat-action.test.ts
📝 Walkthrough

Walkthrough

Adds a new CombatEngine.applyDash(participantId) method and updates the consolidated combat-action dash handler to retrieve or restore a per-encounter engine, call applyDash, and return structured success/error details including movementRemaining. Tests added to validate action-economy, DB-backed engine reload, and movement budget enforcement.

Changes

Cohort / File(s) Summary
Combat Engine
src/engine/combat/engine.ts
Added public applyDash(participantId) which validates combat state and participant, enforces action-economy, prevents double-dash, increases movementRemaining by base speed (default 30), sets hasDashed and actionUsed, and returns { ok: true, movementRemaining } or { ok: false, error }.
Server Action Handler
src/server/consolidated/combat-action.ts
Updated dash action to obtain per-session/encounter CombatEngine from in-memory manager (keyed by sessionId:encounterId); if missing, attempt to load persisted state via EncounterRepository.loadState, instantiate and register an engine, then call engine.applyDash(actorId). Returns structured errors when engine/encounter missing or applyDash fails; on success includes movementRemaining and updates effect/message text.
Tests
tests/server/consolidated/combat-action.test.ts
Added three regression tests: (1) dash fails if main action already used (after attack), (2) dash succeeds and reports movementRemaining: 60 when engine is evicted and auto-loaded from DB, (3) dash doubles movement and allows a subsequent diagonal move to {x:14,y:14} under the doubled budget.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server as CombatActionHandler
  participant Manager as CombatManager (in-memory)
  participant DB as EncounterRepository
  participant Engine as CombatEngine

  Client->>Server: POST /combat_action dash {sessionId, encounterId, actorId}
  Server->>Manager: getEngine(sessionId:encounterId)
  alt Engine in memory
    Manager-->>Server: engine
    Server->>Engine: applyDash(actorId)
    Engine-->>Server: {ok:true, movementRemaining}
    Server-->>Client: 200 {success:true, movementRemaining,...}
  else Engine missing
    Server->>DB: loadState(encounterId)
    alt state found
      DB-->>Server: persistedState
      Server->>Engine: new Engine()
      Server->>Engine: loadState(persistedState)
      Server->>Manager: registerEngine(sessionId:encounterId, Engine)
      Server->>Engine: applyDash(actorId)
      Engine-->>Server: {ok:true, movementRemaining}
      Server-->>Client: 200 {success:true, movementRemaining,...}
    else not found
      DB-->>Server: null
      Server-->>Client: 404 {success:false, error:"encounter not found"}
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped into code with a twitch of my nose,
I doubled the steps where the brave rabbit goes.
Movement now stretches, it's nimble and fast,
Through engines and repos — this dash holds at last! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: implementing dash to properly extend movement budget.
Linked Issues check ✅ Passed The pull request successfully implements all primary requirements from #50: dash now extends movement budget, sets hasDashed flag, consumes main action, and includes regression test validating doubled budget.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing and testing the dash movement budget fix; no unrelated modifications detected across the three modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 fix/50-dash-move-budget

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

The consolidated dash handler was a stub: it returned a success message
saying "Movement speed doubled" but never touched the engine, so the
follow-up move action still enforced the original 30ft budget and
rejected with "Insufficient movement." Observed in live session when
Rurk tried to dash + move to close the aisle.

- CombatEngine.applyDash(): adds a fresh baseSpeed allotment on top of
  any existing movementRemaining, flags hasDashed, and consumes the
  main action (matches 5e dash rules).
- Consolidated dash handler calls applyDash and reports the new
  movementRemaining so callers can see their budget.

Closes #50

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/server/consolidated/combat-action.test.ts (1)

296-297: Consider adding negative test case.

The test verifies dash works, but consider adding a case that verifies a move exceeding 60ft fails after dash. This would confirm the budget is actually enforced (not just that any move succeeds).

Example: After dash (60ft), attempt to move to a position requiring >60ft and assert success: false.

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

In `@tests/server/consolidated/combat-action.test.ts` around lines 296 - 297, Add
a negative test that, after performing the dash (which yields
dashData.movementRemaining === 60), attempts a subsequent move to a target
requiring >60ft using the same movement action helper and assert that that
move's result.success is false (and that movementRemaining is not
reduced/unchanged), thereby verifying the 60ft budget is enforced; use the
existing dash invocation and the same move API used elsewhere in this test file
and reference dashData, movementRemaining and the move result object when adding
the assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/server/consolidated/combat-action.test.ts`:
- Around line 296-297: Add a negative test that, after performing the dash
(which yields dashData.movementRemaining === 60), attempts a subsequent move to
a target requiring >60ft using the same movement action helper and assert that
that move's result.success is false (and that movementRemaining is not
reduced/unchanged), thereby verifying the 60ft budget is enforced; use the
existing dash invocation and the same move API used elsewhere in this test file
and reference dashData, movementRemaining and the move result object when adding
the assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 643cc6fc-4b41-4895-9beb-e5aafb2de7b4

📥 Commits

Reviewing files that changed from the base of the PR and between 2d9ddb9 and 4596008.

📒 Files selected for processing (3)
  • src/engine/combat/engine.ts
  • src/server/consolidated/combat-action.ts
  • tests/server/consolidated/combat-action.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4596008b73

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1008 to +1012
if (participant.hasDashed) return { ok: false, error: 'Already dashed this turn' };

const baseSpeed = participant.movementSpeed ?? 30;
const currentRemaining = participant.movementRemaining ?? baseSpeed;
participant.movementRemaining = currentRemaining + baseSpeed;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Validate action economy before applying Dash

applyDash only checks hasDashed and then always adds movement, so a participant who already spent their Action can still dash and gain extra movement in the same turn. This is reachable because attack/heal/disengage consume actions via commitAction, but this path never calls validateActionEconomy(..., 'action'); as a result, dash can be used as an illegal second main action (and by incapacitated actors) and produces incorrect combat state.

Useful? React with 👍 / 👎.

Comment on lines +192 to +196
const engine = getCombatManager().get(`${currentContext.sessionId}:${params.encounterId}`);
if (!engine) {
return {
error: true,
actionType: 'dash',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reuse encounter auto-load path for dash

This handler now depends on an in-memory engine and immediately returns Encounter ... not found when getCombatManager().get(...) misses. Unlike the other combat actions, it does not use handleExecuteCombatAction’s DB auto-load behavior, so dash fails after process restart or manager eviction even when the encounter exists and other actions still work. Dash should follow the same load path to avoid this inconsistent failure mode.

Useful? React with 👍 / 👎.

Reviewers (PR #60) flagged two gaps:

1. applyDash only checked hasDashed before consuming the action, so a
   participant who had already attacked could still dash for a free
   60ft of movement. Dash IS the action in 5e — calling
   validateActionEconomy(participantId, 'action') first rejects the
   illegal sequence.
2. The dash handler short-circuited with "Encounter not found" when
   the engine wasn't in memory, unlike other combat actions which
   auto-load from EncounterRepository. After a process restart or
   manager eviction, dash failed even though attack/move still worked.

Adds matching auto-load path in the consolidated dash handler and
regression tests for both behaviors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/server/consolidated/combat-action.ts`:
- Around line 203-212: When restoring an evicted engine, avoid the race where
two requests both load from DB and call getCombatManager().create(sessionKey,
engine) causing an exception; change the logic around the engine restoration
(the block that calls getDb(...), new EncounterRepository(...).loadState(...),
new CombatEngine(...).loadState(...), and getCombatManager().create(sessionKey,
engine)) to use a get-or-create pattern: after loading the persisted state and
constructing the engine, attempt to create but catch the specific error from
CombatManager.create and, on that error, call getCombatManager().get(sessionKey)
to retrieve the already-created engine (or re-check
getCombatManager().get(sessionKey) before create and only call create if get
returned null), ensuring only one creation succeeds and the second request uses
the existing engine.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5aa9e42-5ac8-43c1-949a-b2e5bcbab2c8

📥 Commits

Reviewing files that changed from the base of the PR and between 4596008 and ebf6fe9.

📒 Files selected for processing (3)
  • src/engine/combat/engine.ts
  • src/server/consolidated/combat-action.ts
  • tests/server/consolidated/combat-action.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/server/consolidated/combat-action.test.ts
  • src/engine/combat/engine.ts

Comment thread src/server/consolidated/combat-action.ts
Reviewers (PR #60, Minor) noted that two concurrent dash requests
hitting the auto-load path could both see the engine missing, both
load from DB, and both call CombatManager.create — the loser would
throw "Encounter X already exists".

Adopts the get-or-create pattern: re-check the manager after loading
from DB, and wrap create in a try/get fallback so the loser of the
race adopts the winner's engine instead of crashing.

Regression test fires two dash requests in parallel against an
evicted engine; one must succeed and neither may throw 'already
exists'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Mnehmos

Mnehmos commented Apr 19, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai both findings are stale regenerations against the same commit that addresses them — ebf6fe97 added validateActionEconomy('action') inside applyDash (rejects when actor already used their action), and fa46dfc1 adopts the get-or-create pattern with try/catch fallback to getCombatManager().get(). Tests cover both behaviors.

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@Mnehmos, you're right — both findings were already resolved before those comments were generated. The AI summary and PR objectives confirm it:

  • Action-economy guard: applyDash calls validateActionEconomy(participantId, 'action') in ebf6fe97, and the regression test for the "actor already used action" path is in place.
  • Auto-load / get-or-create: fa46dfc1 added the try/catch fallback to getCombatManager().get() after EncounterRepository.loadState, mirroring the pattern used by other combat actions, with a corresponding regression test.

Apologies for the noise from the stale regeneration. The implementation and test coverage look solid for both behaviors.

@Mnehmos Mnehmos merged commit a2ac64e into main Apr 20, 2026
4 checks passed
@Mnehmos Mnehmos deleted the fix/50-dash-move-budget branch April 20, 2026 14:22
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.

combat_action dash does not extend the move budget enforced by move action

1 participant