Skip to content

Feature/player piloted summons#1375

Open
ifroshty wants to merge 20 commits into
mainfrom
feature/player-piloted-summons
Open

Feature/player piloted summons#1375
ifroshty wants to merge 20 commits into
mainfrom
feature/player-piloted-summons

Conversation

@ifroshty

@ifroshty ifroshty commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Summary

Adds player-piloted summons: when a human player summons a creature in an interactive battle, they now drive that summon themselves on its turn (its own jutsu, items, action points and targeting) instead of handing it to the rule-based AI. Along the way the summon system was rewritten to fix long-standing issues (re-cast, one-per-controller, masterless summons), two regressions found during review were fixed, and the summon lifecycle logic was consolidated into a single tested module so it stops breaking on each change. Summons are now also correctly disabled in auto-resolved battle types (KAGE_AI / CLAN_CHALLENGE).

What was implemented

Player-piloted summons

  • New transient isPiloted flag on the battle user state. A summon is piloted when its controller is a live human in a non-auto battle type (shouldPilotSummon). isAi deliberately stays true, so reward attribution, drop tables and the AI accounting path are unchanged — isPiloted only affects turn routing and UI.
  • Server turn routing via getTurnControl / wantsHumanActionSet: on a piloted summon's turn the action is taken by the human controller, not the AI. The AI dispatch loop never auto-drives a piloted summon, and a defense-in-depth guard prevents the AI exhaustion path from self-killing a piloted summon.
  • Per-user masking exposes isPiloted to all viewers (added to the public allow-list) while the summon's private stats remain visible only to its controller (unchanged controllerId-based masking).
  • Client: a shared resolveControlledActorId helper makes the combat UI follow the controlled actor — normally the player, transparently the summon on its turn — so the action menu, timer, AP, targeting, camera and keyboard end-turn all act for the summon without any extra UI. AI auto-play is suppressed for the player's own piloted summon.

Summon system rewrite / hardening

  • One-summon-per-controller cap (hasLiveSummon) with a clear "already has a summon!" message; clears once the prior summon is dead/fled/left so re-summon works.
  • Orphan cleanup (isOrphanedSummon / spliceOrphanedSummons): removes a live masterless summon when its controller dies/flees/leaves, before actor selection, so the turn order never lands on or names a controllerless summon.
  • AI self-kill guard for piloted summons.

Bug fixes (found in review)

  • Summoning was completely broken on the branch — the summon-template lookup had been changed to match on userId, but battle-loaded AI templates carry their DB id in controllerId (their userId is a fresh nanoid). Restored the controllerId match so summon() finds the template again. Fixes "Failed to create summon!" for all summons.
  • Re-summon failed after the first — orphan cleanup was splicing out the hidden clone-source template (parked at curHealth: 0), so subsequent casts had no template. isOrphanedSummon now only treats live summons as orphans, leaving templates (and dead summons) in place. Both first-cast and re-cast now work.

Hardening — single source of truth

  • Consolidated the scattered summon helpers into a new app/src/libs/combat/summon.ts and replaced the hand-re-derived "template vs active summon" checks with one tested predicate set (isSummonTemplate / isActiveSummon / isLiveSummon), routed through the cap, orphan cleanup and AI gate. The summon-template lookup and teardown stay raw (controllerId-based) by design — routing them through the predicate would regress legacy lookups and dead-summon removal.
  • Added an explicit, server-internal isSummonTemplate marker (set at load, cleared on spawn/clone), and enforced the template curHealth:0 invariant in all battle types by guarding the AutoBattle pool reset with !user.isSummon — so a hidden off-grid template can never be selected as the active actor.
  • spliceOrphanedSummons now classifies against a pre-mutation snapshot (order-independent).
  • No summons in KAGE_AI / CLAN_CHALLENGEsummon() now refuses to spawn in auto-resolved battle types, where there are no human turns. Previously an AI participant there could spawn an AI-driven summon.
  • Comprehensive tests pin every category (template / spawned / clone), the legacy fallback, masking-absence, splice determinism, turn-order exclusion and the auto-battle block, so future edits can't silently reintroduce the old bugs.

Why

The summon feature previously ran entirely under AI, giving players no control over creatures they summoned. Piloting makes summons a real tactical tool. The rewrite also resolves re-cast and masterless-summon edge cases that were awkward in the old implementation.

Screenshots

TODO: add a combat screenshot showing the action panel switching to the summon's jutsu/AP on the summon's turn.

Breaking changes

No schema/API breaks; one intentional behavior change.

  • No database schema or migration changes. isPiloted (public) and isSummonTemplate (server-internal) are transient fields inside the battle-state JSON, not new columns.
  • Behavior change: summons no longer occur in auto-resolved battle types (KAGE_AI, CLAN_CHALLENGE). Previously an AI participant there could spawn an AI-driven summon; summon() now refuses to spawn in those types, matching the design that those battles have no human turns. (Interactive battle types are unaffected.)

Testing

  • bun test tests/libs/combat/140 passed / 0 failed (14 files). Coverage spans the piloting suites (turn control, action-set selection, controlled-actor resolution, AI piloted-guard, masking, one-per-controller cap, orphan cleanup), the summon() template-lookup regression tests (first-cast, re-cast-through-cleanup, template-kept, live-orphan-removed, turn-order placement, missing-AI message, auto-battle block), and the new summon.ts predicate module (full template/spawned/clone matrix, legacy fallback, masking-absence, splice determinism, summonsAllowedInBattle).
  • bun run typecheck → exit 0; import cycle (summon ↔ actions ↔ tags) verified runtime-safe (combat route serves 200).
  • New test files added; no schema/migration changes.

Notes / follow-ups

  • Summon jutsus reference an AI creature by aiId, which must exist as a UserData row to be loaded as a template. Only jutsu and item summon effects are scanned at battle init (not bloodline/skill effects). If a referenced AI is missing from the database, the summon now fails with a clearer message ("…'s summon creature could not be found.") rather than a generic error — this is a content/data concern, not a code issue (e.g. most summon AIs are unseeded in local dev DBs; production has them).

License

By making this pull request, I confirm that I have the right to waive copyright and related rights to my contribution, and agree that all copyright and related rights in my contributions are waived, and I acknowledge that the Studie-Tech ApS organization has the copyright to use and modify my contribution for perpetuity.

Summary by CodeRabbit

  • New Features

    • Combat now recognizes a controlled actor, including piloted summons, for turn order, actions, targeting, and highlighting.
    • Summons can be marked as player-controlled, with battle state preserving this status.
    • Added summon lifecycle handling for orphaned summons and improved summon placement in turn order.
  • Bug Fixes

    • Fixed turn handling so piloted summons use their own action set and aren’t treated like the session user.
    • Improved AI and wait-action behavior to respect piloted summons and avoid clearing selections unnecessarily.
    • Added safeguards so summon templates, clones, and orphaned summons are handled more consistently.

@ifroshty ifroshty requested a review from MathiasGruber as a code owner June 21, 2026 05:42
@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
the-ninja-ai Ready Ready Preview, Comment Jun 29, 2026 7:31pm
tnr Ready Ready Preview, Comment Jun 29, 2026 7:31pm

Request Review

@github-actions github-actions Bot added the size:L 100-499 effective changed lines (test files excluded in mixed PRs). label Jun 21, 2026
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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
  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

Adds controlled-actor routing for piloted summons, updates summon lifecycle and turn control handling, wires the combat UI to the active actor, and adds tests covering piloting, masking, AI behavior, and summon spawning/orphan cleanup.

Changes

Piloted Summon Combat System

Layer / File(s) Summary
State flags and summon helpers
app/src/libs/combat/types.ts, app/src/libs/combat/constants.ts, app/src/libs/combat/summon.ts, app/src/validators/combat.ts
Adds piloting/template fields, summon classification/live/orphan helpers, piloting policy helpers, and the summon tag field for player-controlled casting.
Controlled actor and action control
app/src/libs/combat/actions.ts, app/src/libs/combat/util.ts, app/src/libs/combat/ai_v2.ts, app/src/server/api/routers/combat.ts
Adds controlled-actor resolution, turn-control helpers, human-action selection, orphan cleanup during rounds, AI summon checks, and server turn/action gating updates.
Summon cast, spawn, and placement
app/src/libs/combat/tags.ts
Reworks summon and clone behavior to resolve templates by controller, enforce live-summon limits, set piloting state, and change summon insertion/removal semantics.
Combat UI controlled-actor wiring
app/src/app/combat/page.tsx, app/src/layout/Combat.tsx, app/src/libs/threejs/combat.ts
Routes controlled actor IDs through the page and layout, updates action/timer/input/highlight handling, and refreshes related comments.
Combat behavior and masking tests
app/tests/libs/combat/*
Adds tests for controlled-actor selection, turn control, piloting policy, masking, summon lifecycle, AI guard behavior, and summon spawn/template flow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

Completed

Suggested reviewers

  • theeneon
  • Phrosfire
  • MathiasGruber

Poem

🐇 A piloted hop through combat tonight,
With summon and turn logic tucked in just right.
Orphans get swept as the battle winds spin,
And carrots of testing keep everything in.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: player-piloted summons.
Description check ✅ Passed The description covers what changed, why, screenshots, breaking changes, testing, and the required license section.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feature/player-piloted-summons

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/layout/Combat.tsx`:
- Around line 154-166: The precomputedActions variable computed via useMemo is
render-scoped but gets consumed by long-lived callbacks (interval handlers and
renderer click/highlight paths at lines 501-503 and 734-747) that run outside
the React render cycle, causing them to access stale action data after battle
updates. Follow the same pattern already established with userIdRef: create a
new useRef to store precomputedActions, add a useEffect hook that updates this
ref whenever the precomputedActions value changes (with dependencies on
battleRef.current?.version and controlledActorId), and then update all the
long-lived callback code at those referenced line ranges to read from the ref
instead of the closure variable. This ensures the interval and click handlers
always access the current precomputedActions data rather than stale cached
values.
🪄 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: 584f4ea3-f9ac-4caf-beea-9b91cd1670ad

📥 Commits

Reviewing files that changed from the base of the PR and between 89e2016 and cb0822d.

📒 Files selected for processing (19)
  • app/src/app/combat/page.tsx
  • app/src/layout/Combat.tsx
  • app/src/libs/combat/actions.ts
  • app/src/libs/combat/ai_v2.ts
  • app/src/libs/combat/constants.ts
  • app/src/libs/combat/tags.ts
  • app/src/libs/combat/types.ts
  • app/src/libs/combat/util.ts
  • app/src/libs/threejs/combat.ts
  • app/src/server/api/routers/combat.ts
  • app/tests/libs/combat/actions.test.ts
  • app/tests/libs/combat/ai_v2_piloted_guard.test.ts
  • app/tests/libs/combat/orphan_summon.test.ts
  • app/tests/libs/combat/piloted_actionset.test.ts
  • app/tests/libs/combat/piloted_masking.test.ts
  • app/tests/libs/combat/piloted_summon.test.ts
  • app/tests/libs/combat/resolve_controlled_actor.test.ts
  • app/tests/libs/combat/summon_template_lookup.test.ts
  • app/tests/libs/combat/turn_control.test.ts

Comment thread app/src/layout/Combat.tsx
@greptile-apps

greptile-apps Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 4/5

Safe to merge for interactive battle types; all previous review-thread concerns are addressed and the piloted-summon turn routing is correctly gated on both server and client.

The implementation is thorough and the 140-test suite covers every predicate, splice edge case, masking, and turn-control scenario. The previous reviewer concern about the AI interval auto-driving another player's piloted summon is fully fixed: getTurnControl gates on actor.isAi && !actor.isPiloted, which is false for any piloted summon regardless of which client is watching. The complexity of coordinating React refs, render-cycle effects, and 1-second intervals still warrants a close look at Combat.tsx — the brief window between a battle-state update and the interval's re-registration only causes a spurious changedActor refetch rather than incorrect action submission.

app/src/layout/Combat.tsx — the interaction between the reactive controlledActorId, precomputedActionsRef, and the 1-second AI-interval closure is subtle and worth a manual smoke-test of the turn handoff between a player and their piloted summon in a live multiplayer battle.

Important Files Changed

Filename Overview
app/src/libs/combat/summon.ts New module consolidating all summon lifecycle predicates; well-documented, snapshot-safe splice, comprehensive test coverage.
app/src/libs/combat/tags.ts summon() rewritten: template lookup moved to controllerId, one-per-controller cap, aiId no longer rebound on spawn, teardown excludes clones, summon inserted after summoner in turn order.
app/src/layout/Combat.tsx Client-side piloting: controlledActorId derived from reactive battleState.battle; AI interval gates on getTurnControl(actor, suid).isAITurn preventing auto-drive of any piloted summon.
app/src/server/api/routers/combat.ts performAction uses getTurnControl for authorization; action list built from actor.userId; mid-round orphan splice; inner AI loop guards !newActor.isPiloted; AutoBattle pool reset guarded with !user.isSummon.
app/src/libs/combat/actions.ts Adds resolveControlledActorId helper shared between page.tsx and Combat.tsx.
app/src/libs/combat/ai_v2.ts does_not_have_summon updated to use isLiveSummon/isClone; AI self-kill guard protected with !user.isPiloted.
app/src/libs/combat/types.ts Adds isPiloted (public) and isSummonTemplate (server-internal) optional fields with clear documentation.
app/src/app/combat/page.tsx Uses resolveControlledActorId from reactive battle state; passes controlledActorId to availableUserActions and ActionTimer.
app/src/validators/combat.ts SummonTag gains playerControlled: z.boolean().prefault(false); plain boolean correctly avoids coercion edge cases.
app/src/libs/combat/util.ts Adds wantsHumanActionSet and getTurnControl helpers; alignBattle calls spliceOrphanedSummons on round progress.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as Client (Player)
    participant S as Server (performAction)
    participant AI as AI Loop

    Note over C,AI: Player's own turn
    C->>S: "performAction(userId=suid, actionId)"
    S->>S: "getTurnControl(actor, suid) isUserTurn=true isAITurn=false"
    S-->>C: battleUpdate (summon now active)

    Note over C,AI: Piloted summon's turn
    C->>C: resolveControlledActorId() → summon.userId
    C->>C: availableUserActions(summon.userId) → summon jutsu
    C->>S: "performAction(userId=summon.userId, jutsu)"
    S->>S: "getTurnControl(pilotedSummon, suid) isUserTurn=true isAITurn=false"
    S->>S: "availableUserActions(actor.userId=summon)"
    S-->>C: battleUpdate

    Note over AI: AI loop on any client — piloted summon turn
    AI->>AI: "getTurnControl(actor, suid) isAITurn = isAi && !isPiloted = false"
    AI->>AI: No auto-drive fired
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as Client (Player)
    participant S as Server (performAction)
    participant AI as AI Loop

    Note over C,AI: Player's own turn
    C->>S: "performAction(userId=suid, actionId)"
    S->>S: "getTurnControl(actor, suid) isUserTurn=true isAITurn=false"
    S-->>C: battleUpdate (summon now active)

    Note over C,AI: Piloted summon's turn
    C->>C: resolveControlledActorId() → summon.userId
    C->>C: availableUserActions(summon.userId) → summon jutsu
    C->>S: "performAction(userId=summon.userId, jutsu)"
    S->>S: "getTurnControl(pilotedSummon, suid) isUserTurn=true isAITurn=false"
    S->>S: "availableUserActions(actor.userId=summon)"
    S-->>C: battleUpdate

    Note over AI: AI loop on any client — piloted summon turn
    AI->>AI: "getTurnControl(actor, suid) isAITurn = isAi && !isPiloted = false"
    AI->>AI: No auto-drive fired
Loading

Reviews (11): Last reviewed commit: "fix: typescript ci" | Re-trigger Greptile

Comment thread app/src/libs/combat/tags.ts
Comment thread app/src/libs/combat/util.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
app/tests/libs/combat/summon_turn_order.test.ts (1)

22-27: ⚡ Quick win

Replace process-tracking prose with purpose-focused test commentary

The comment references task/review history instead of the test’s behavior contract. Please rewrite it to only describe why this fixture proves calcActiveUser excludes non-live templates.

Suggested edit
-    // curHealth:0 is what Task 1 Step 8 (the AutoBattle !user.isSummon guard)
-    // GUARANTEES at load for templates in every battle type. This test locks the
-    // CONSUMER behavior — that calcActiveUser excludes a curHealth:0 template —
-    // so it exercises the stillInBattle exclusion path, not the loader itself
-    // (which needs a DB-backed processUsersForBattle; covered by dev-boot smoke).
+    // This template is intentionally non-live (curHealth: 0), so calcActiveUser
+    // must exclude it through stillInBattle-based actor selection.

As per coding guidelines, comments should describe code purpose and should not include review/task tracking markers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/tests/libs/combat/summon_turn_order.test.ts` around lines 22 - 27, The
multi-line comment block preceding the template creation references
process-tracking details like task history and implementation concerns rather
than the test's purpose. Replace this comment with a clear explanation focused
solely on describing why this fixture with curHealth:0 proves that
calcActiveUser excludes non-live templates from active user calculations. Remove
all references to Task identifiers, loader implementation details, and smoke
test coverage, keeping only the behavioral contract this test verifies.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/tests/libs/combat/summon_turn_order.test.ts`:
- Around line 22-27: The multi-line comment block preceding the template
creation references process-tracking details like task history and
implementation concerns rather than the test's purpose. Replace this comment
with a clear explanation focused solely on describing why this fixture with
curHealth:0 proves that calcActiveUser excludes non-live templates from active
user calculations. Remove all references to Task identifiers, loader
implementation details, and smoke test coverage, keeping only the behavioral
contract this test verifies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb13694b-1ef2-404d-826f-d1a63ab2e673

📥 Commits

Reviewing files that changed from the base of the PR and between cb0822d and d16b8aa.

📒 Files selected for processing (11)
  • app/src/libs/combat/ai_v2.ts
  • app/src/libs/combat/summon.ts
  • app/src/libs/combat/tags.ts
  • app/src/libs/combat/types.ts
  • app/src/libs/combat/util.ts
  • app/src/server/api/routers/combat.ts
  • app/tests/libs/combat/orphan_summon.test.ts
  • app/tests/libs/combat/piloted_summon.test.ts
  • app/tests/libs/combat/summon.test.ts
  • app/tests/libs/combat/summon_template_lookup.test.ts
  • app/tests/libs/combat/summon_turn_order.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/tests/libs/combat/piloted_summon.test.ts
  • app/tests/libs/combat/orphan_summon.test.ts
  • app/src/libs/combat/types.ts
  • app/src/server/api/routers/combat.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/validators/combat.ts`:
- Around line 740-742: The playerControlled validator currently uses
z.coerce.boolean(), which relies on JavaScript truthiness and can turn the
string "false" into true. Update the playerControlled field in the combat
validator to use a proper string-to-boolean parser such as z.stringbool() or an
explicit boolean parsing schema, while keeping the same default behavior and
surrounding validation structure intact.

In `@app/tests/libs/combat/summon_template_lookup.test.ts`:
- Around line 308-310: The summon tests are too loose because `toBeFalsy()`
allows `undefined` and won’t catch regressions in `summon()` initialization.
Update the non-piloted assertions in `summon_template_lookup.test.ts` to use
exact boolean checks on `findSpawn(...)?.isPiloted`, especially in the
`summon()` paths that rely on `shouldPilotSummon(...)`, so the tests fail if
`isPiloted` is no longer explicitly set to `false`.
- Around line 365-369: The clone assertion in summon_template_lookup.test should
verify that clone() removes the isPiloted flag entirely, not just that it is
falsy. Update the expectation on the cloned summon found via usersState.find to
use toBeUndefined() for cloned?.isPiloted so this test catches regressions where
the property remains set to false instead of being cleared.
🪄 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: 06822444-690c-4e80-845e-83cc0074c021

📥 Commits

Reviewing files that changed from the base of the PR and between 73dc667 and 08e2ff3.

📒 Files selected for processing (5)
  • app/src/libs/combat/summon.ts
  • app/src/libs/combat/tags.ts
  • app/src/validators/combat.ts
  • app/tests/libs/combat/piloted_summon.test.ts
  • app/tests/libs/combat/summon_template_lookup.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/tests/libs/combat/piloted_summon.test.ts
  • app/src/libs/combat/summon.ts
  • app/src/libs/combat/tags.ts

Comment thread app/src/validators/combat.ts Outdated
Comment thread app/tests/libs/combat/summon_template_lookup.test.ts Outdated
Comment thread app/tests/libs/combat/summon_template_lookup.test.ts Outdated
Comment thread app/src/layout/Combat.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/layout/Combat.tsx (1)

429-466: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid stale suid/timeDiff in the once-registered key handler.

The listener is registered with [], but still closes over render-scope suid and timeDiff. If user data or clock sync arrives after mount, the w shortcut can compare against a stale session id or compute the active actor with stale timing.

Proposed fix
   const suid = userData?.userId;
+  const suidRef = useRef<string | undefined>(suid);
+  const timeDiffRef = useRef(timeDiff);
+
+  useEffect(() => {
+    suidRef.current = suid;
+    timeDiffRef.current = timeDiff;
+  }, [suid, timeDiff]);
+
@@
   const onDocumentKeyDown = (event: KeyboardEvent) => {
     if (battleRef.current) {
@@
-      const { actor } = calcActiveUser(battleRef.current, suid, timeDiff);
+      const currentSuid = suidRef.current;
+      const { actor } = calcActiveUser(
+        battleRef.current,
+        currentSuid,
+        timeDiffRef.current,
+      );
@@
-          if (actor.controllerId === suid) {
+          if (actor.controllerId === currentSuid) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/layout/Combat.tsx` around lines 429 - 466, The once-registered
keyboard handler in onDocumentKeyDown is closing over stale suid and timeDiff,
so the “w” action can use outdated session/clock state after mount. Update the
handler to read the latest values at event time, either by moving suid and
timeDiff into refs or by wiring the effect so the listener is refreshed when
those values change, while keeping calcActiveUser and the battleRef.current
lookup aligned with the live state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/src/layout/Combat.tsx`:
- Around line 429-466: The once-registered keyboard handler in onDocumentKeyDown
is closing over stale suid and timeDiff, so the “w” action can use outdated
session/clock state after mount. Update the handler to read the latest values at
event time, either by moving suid and timeDiff into refs or by wiring the effect
so the listener is refreshed when those values change, while keeping
calcActiveUser and the battleRef.current lookup aligned with the live state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49007cbb-351a-4951-9dd7-fc80f44e2d20

📥 Commits

Reviewing files that changed from the base of the PR and between 08e2ff3 and d98a751.

📒 Files selected for processing (3)
  • app/src/layout/Combat.tsx
  • app/src/validators/combat.ts
  • app/tests/libs/combat/summon_template_lookup.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/validators/combat.ts
  • app/tests/libs/combat/summon_template_lookup.test.ts

@MathiasGruber

Copy link
Copy Markdown
Collaborator

Rebased this branch onto the latest main to clear the merge conflicts.

Conflict resolved:

  • app/tests/libs/combat/actions.test.ts — the @/libs/hexgrid mock block conflicted. main had already landed an equivalent fix for leaked hexgrid test mocks (commit Fix leaked hexgrid test mocks) that serves the same purpose as this branch's "bun-test hardening" change. Kept main's version: getPossibleActionTiles returns the passed grid and PathCalculator exposes getShortestPath. Both mocked functions are import-time guards only (not referenced in any test body), so the two variants are functionally equivalent for this file; the branch's now-redundant explanatory comment was dropped.

No migration conflicts were involved. The 16 branch commits replayed cleanly otherwise, and the PR is now mergeable.

@MathiasGruber

Copy link
Copy Markdown
Collaborator

/tnr-review-now

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Scope covered

  • Reviewed the combat/summon PR surface in code to derive player-facing scenarios.
  • Provisioned preview users, authenticated into the protected deployment, and exercised the changed flow in a live Arena battle.
  • Covered the core new behavior: human-cast summon creation, summon turn handoff to the human controller, summon turn not being auto-driven by AI, and keyboard end-turn routing while controlling the summon.

Test steps executed

  1. Logged into the preview as a brokered player and captured a baseline profile checkpoint.
  2. Learned and auto-equipped Summoning: Reptile King for the test player through authenticated browser-side tRPC calls.
  3. Logged into the preview as a CONTENT user and temporarily marked that summon jutsu’s summon effect as playerControlled: true so the PR path could be exercised.
  4. Started an Arena fight against Sad Puppy.
  5. Cast the summon and verified the summon spawned successfully in battle.
  6. Ended the player turn and verified the spawned summon became the active actor with isPiloted: true instead of being auto-played by AI.
  7. Verified the player still controlled the combat UI on the summon’s turn and that hotkey W ended the summon’s turn correctly.
  8. Attempted to isolate the live-summon re-cast guard in-browser, but the summon jutsu was already on cooldown in this scenario, so the exact already has a summon message path was not reached.

Findings (pass/fail, bugs, risks)

  • Pass: Summoning worked in the preview battle. The summon spawned correctly; I did not hit the old Failed to create summon! regression.
  • Pass: Player-piloted summon control worked. After ending the player turn, the active actor switched to the summon, the battle state exposed isPiloted: true, and the summon was not auto-driven by AI.
  • Pass: Player input routing worked on the summon turn. The combat UI stayed under player control and hotkey W ended the summon’s turn successfully, after which the battle advanced normally.
  • Risk / coverage gap: I did not fully isolate the browser-visible one-summon-per-controller rejection message because the summon jutsu cooldown invalidated immediate re-cast attempts first. The attempted re-cast path returned Action not valid anymore. Try something else, so that specific user-facing branch still has some unverified UI coverage.

Screenshot index

  • 01-profile-after-login.png - Player authenticated in preview before combat setup.
  • 02-summon-cast.png - Arena battle after casting Summoning: Reptile King.
  • 03-piloted-summon-turn.png - Summon turn under player control after handoff.

Recommendation

Approve.

Core PR behavior worked in live preview testing. The only remaining gap is narrower UI coverage around the duplicate-cast rejection message while a live summon already exists.


Screenshots

01 profile after login
01 profile after login

02 summon cast
02 summon cast

03 piloted summon turn
03 piloted summon turn


Run: View workflow run

@MathiasGruber MathiasGruber left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Logic / readability / DRY review.

Comment thread app/src/layout/Combat.tsx Outdated
Comment thread app/src/libs/combat/tags.ts
Comment thread app/src/libs/combat/util.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

"Completed" label removed

This PR has 3 unresolved review thread(s). The "Completed" label has been automatically removed.

Please resolve all review threads before re-applying the label.

Unresolved threads:

  • @MathiasGruber: setCombatActionId(undefined) runs on every successful performAction mutation...
  • @MathiasGruber: Behavioral regression in the summon cap: hasLiveSummon matches any live `isSum...
  • @MathiasGruber: DRY: isUserTurn re-derives the human-action-set predicate (`!actor.isAi || !!a...

Comment thread app/src/libs/combat/tags.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/src/libs/combat/tags.ts (1)

3120-3129: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude clones when selecting the summon to unsummon.

clone() creates clones with isSummon = true, controllerId = user.userId, and isOriginal = false, so this lookup can remove a dead/expired clone before the actual summon effect owner. Keep summon cleanup scoped to original summons.

Proposed fix
-    let idx = usersState.findIndex(
-      (u) =>
-        u.isSummon &&
-        u.controllerId === effect.creatorId &&
-        !isLiveSummon(u, usersState, userEffects),
-    );
+    const isOriginalSummonForEffect = (u: BattleUserState) =>
+      u.isSummon && u.isOriginal !== false && u.controllerId === effect.creatorId;
+
+    let idx = usersState.findIndex(
+      (u) => isOriginalSummonForEffect(u) && !isLiveSummon(u, usersState, userEffects),
+    );
     if (idx === -1) {
-      idx = usersState.findIndex(
-        (u) => u.isSummon && u.controllerId === effect.creatorId,
-      );
+      idx = usersState.findIndex(isOriginalSummonForEffect);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/libs/combat/tags.ts` around lines 3120 - 3129, The summon cleanup in
tags.ts is matching any isSummon unit for effect.creatorId, which can pick a
clone instead of the real summon. Update the lookup around the unsummon
selection logic to exclude non-original summons by checking isOriginal (and/or
equivalent clone markers) in both the primary findIndex and the fallback path,
so only the actual summon owner is removed. Use the existing isLiveSummon
selection block as the reference point for the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/libs/combat/tags.ts`:
- Around line 11-16: The summon cleanup fallback in tags.ts can mistakenly pick
a clone because clone() entities also satisfy the summon-like checks. Update the
target selection logic to explicitly exclude isClone(u) before choosing the
entity, or switch to the shared summon helper used by hasLiveSummon/isLiveSummon
so the cleanup only matches real summons. Use the existing summon-related
symbols in this module to keep the filter aligned with the rest of the combat
logic.

---

Duplicate comments:
In `@app/src/libs/combat/tags.ts`:
- Around line 3120-3129: The summon cleanup in tags.ts is matching any isSummon
unit for effect.creatorId, which can pick a clone instead of the real summon.
Update the lookup around the unsummon selection logic to exclude non-original
summons by checking isOriginal (and/or equivalent clone markers) in both the
primary findIndex and the fallback path, so only the actual summon owner is
removed. Use the existing isLiveSummon selection block as the reference point
for the fix.
🪄 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: 90680522-c61d-48eb-9d5a-f9179dee3a62

📥 Commits

Reviewing files that changed from the base of the PR and between d98a751 and ef6c8e6.

📒 Files selected for processing (22)
  • app/src/app/combat/page.tsx
  • app/src/layout/Combat.tsx
  • app/src/libs/combat/actions.ts
  • app/src/libs/combat/ai_v2.ts
  • app/src/libs/combat/constants.ts
  • app/src/libs/combat/summon.ts
  • app/src/libs/combat/tags.ts
  • app/src/libs/combat/types.ts
  • app/src/libs/combat/util.ts
  • app/src/libs/threejs/combat.ts
  • app/src/server/api/routers/combat.ts
  • app/src/validators/combat.ts
  • app/tests/libs/combat/ai_v2_piloted_guard.test.ts
  • app/tests/libs/combat/orphan_summon.test.ts
  • app/tests/libs/combat/piloted_actionset.test.ts
  • app/tests/libs/combat/piloted_masking.test.ts
  • app/tests/libs/combat/piloted_summon.test.ts
  • app/tests/libs/combat/resolve_controlled_actor.test.ts
  • app/tests/libs/combat/summon.test.ts
  • app/tests/libs/combat/summon_template_lookup.test.ts
  • app/tests/libs/combat/summon_turn_order.test.ts
  • app/tests/libs/combat/turn_control.test.ts
💤 Files with no reviewable changes (9)
  • app/tests/libs/combat/piloted_actionset.test.ts
  • app/tests/libs/combat/resolve_controlled_actor.test.ts
  • app/tests/libs/combat/piloted_masking.test.ts
  • app/tests/libs/combat/piloted_summon.test.ts
  • app/tests/libs/combat/turn_control.test.ts
  • app/tests/libs/combat/summon_turn_order.test.ts
  • app/tests/libs/combat/summon.test.ts
  • app/tests/libs/combat/orphan_summon.test.ts
  • app/tests/libs/combat/summon_template_lookup.test.ts
✅ Files skipped from review due to trivial changes (3)
  • app/src/libs/combat/constants.ts
  • app/src/libs/threejs/combat.ts
  • app/src/validators/combat.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • app/src/libs/combat/types.ts
  • app/src/libs/combat/ai_v2.ts
  • app/tests/libs/combat/ai_v2_piloted_guard.test.ts
  • app/src/app/combat/page.tsx
  • app/src/libs/combat/summon.ts
  • app/src/libs/combat/actions.ts
  • app/src/libs/combat/util.ts
  • app/src/server/api/routers/combat.ts
  • app/src/layout/Combat.tsx

Comment thread app/src/libs/combat/tags.ts
@github-actions github-actions Bot added size:XL 500-999 effective changed lines (test files excluded in mixed PRs). and removed size:L 100-499 effective changed lines (test files excluded in mixed PRs). labels Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

40 SS Completed size:XL 500-999 effective changed lines (test files excluded in mixed PRs).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants