Skip to content

Comments

Teaming callout#3197

Open
Caleb-Cohen wants to merge 13 commits intoopenfrontio:mainfrom
Caleb-Cohen:teaming-callout
Open

Teaming callout#3197
Caleb-Cohen wants to merge 13 commits intoopenfrontio:mainfrom
Caleb-Cohen:teaming-callout

Conversation

@Caleb-Cohen
Copy link

@Caleb-Cohen Caleb-Cohen commented Feb 13, 2026

If this PR fixes an issue, link it below. If not, delete these two lines.
Resolves #3189

Description:

Adds a new "teaming with" quick chat phrase that allows players to call out two players who are teaming, using a two-player selection flow in the chat modal.

Screenshots

Modal before countries are selected

Screenshot 2026-02-12 at 9 32 27 PM

Modal after countries are selected

Screenshot 2026-02-12 at 9 33 06 PM

Sent quick chat message

Screenshot 2026-02-12 at 9 33 19 PM

Describe the PR.

  • Added teaming_with entry to QuickChat.json with requiresPlayer and requiresPlayer2 flags
  • Added Teaming_with: "[P1] is teaming with [P2]!", player_1, and player_2 to en.json
  • Added optional target2 field to QuickChatIntent schema, SendQuickChatEvent, QuickChatExecution, displayChatMessage, and DisplayChatMessageUpdate
  • Updated ChatModal to support a two-step player selection. After picking P1, the modal transitions to P2 mode with P1 disabled on the list. Selection then loops back to P1 so players can correct their selection or submit.
  • Updated EventsDisplay to resolve the [P2] placeholder using target2

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

CalebCohen

@CLAassistant
Copy link

CLAassistant commented Feb 13, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 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

Adds support for quick chat messages that reference two players. Adds a new "teaming_with" warning and translations, threads an optional second target (target2) through client UI, transport, schemas, execution, game API, and event rendering to produce messages like "[P1] is teaming with [P2]!".

Changes

Cohort / File(s) Summary
Configuration & Translations
resources/QuickChat.json, resources/lang/en.json
Added warnings.teaming_with entry requiring two players; added chat.player_1 and chat.player_2 translation keys and chat.warnings.teaming_with.
Client Transport
src/client/Transport.ts
SendQuickChatEvent constructor now accepts optional target2; onSendQuickChatIntent includes target2 in emitted payload.
Client UI Layer
src/client/graphics/layers/ChatModal.ts
Added requiresPlayer2 flag, selectedPlayer2/selectingPlayer2 state and two-step selection flow; preview and send now support [P2]; resets second-player state on close/reset; exports quickChatPhrases.
Client Event Rendering
src/client/graphics/layers/EventsDisplay.ts
Resolves event.target2 and substitutes [P2] in translated messages; logs and aborts if resolution fails.
Core Schema & Execution
src/core/Schemas.ts, src/core/execution/ExecutionManager.ts, src/core/execution/QuickChatExecution.ts
Added optional target2 to QuickChatIntentSchema; ExecutionManager passes intent.target2 into QuickChatExecution, which stores target2 and includes it in displayChat calls.
Game API & Updates
src/core/game/Game.ts, src/core/game/GameImpl.ts, src/core/game/GameUpdates.ts
Added target2 parameter to displayChat, included target2 in emitted DisplayChatEvent, and added target2 to DisplayChatMessageUpdate.
Tests
tests/QuickChatExecutions.test.ts
New tests covering two-target, single-target, no-target quick chats, invalid recipient behavior, lifecycle, and key parsing for the new teaming_with phrase.

Sequence Diagram

sequenceDiagram
    autonumber
    participant User as User
    participant ChatModal as ChatModal
    participant Transport as Transport
    participant ExecMgr as ExecutionManager
    participant QuickExec as QuickChatExecution
    participant Game as GameImpl
    participant EventsDisplay as EventsDisplay

    User->>ChatModal: select "teaming_with" phrase
    ChatModal->>ChatModal: requiresPlayer2Selection = true
    User->>ChatModal: pick Player 1
    ChatModal->>ChatModal: set selectedPlayer
    User->>ChatModal: pick Player 2
    ChatModal->>ChatModal: set selectedPlayer2
    User->>ChatModal: click Send
    ChatModal->>Transport: SendQuickChatEvent(recipient, key, target, target2)
    Transport->>ExecMgr: emit quick_chat intent (target, target2)
    ExecMgr->>QuickExec: new QuickChatExecution(..., target, target2)
    QuickExec->>Game: displayChat(message, category, target, target2, ...)
    Game->>EventsDisplay: DisplayChatEvent (key, category, target, target2, ...)
    EventsDisplay->>EventsDisplay: resolve [P1] and [P2]
    EventsDisplay->>User: show "[Player1] is teaming with [Player2]!"
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

Two names appear, a short duet,
P1 and P2 in one vignette.
From phrase to game, the message sings,
Small change, two players, joyful things. 🎭

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Teaming callout' directly summarizes the main change: adding a new quick chat feature to call out players who are teaming together.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the feature addition, implementation details, UI changes, and including relevant screenshots.
Linked Issues check ✅ Passed The PR fully implements the requirement from issue #3189 to add a chat option for 'Person A is teaming with Person B' with proper UI selection flow and message rendering.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the teaming callout feature; no unrelated modifications to other systems or features are present.
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.


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 and usage tips.

Copy link
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
src/client/graphics/layers/ChatModal.ts (1)

54-63: 🛠️ Refactor suggestion | 🟠 Major

This instance property quickChatPhrases is dead code — shadows the module-level import.

getPhrasesForCategory at line 75 uses the module-level quickChatPhrases (line 21), not this.quickChatPhrases. This instance property is never referenced and can be removed.

Suggested fix
-  quickChatPhrases: Record<
-    string,
-    Array<{ text: string; requiresPlayer: boolean }>
-  > = {
-    help: [{ text: "Please give me troops!", requiresPlayer: false }],
-    attack: [{ text: "Attack [P1]!", requiresPlayer: true }],
-    defend: [{ text: "Defend [P1]!", requiresPlayer: true }],
-    greet: [{ text: "Hello!", requiresPlayer: false }],
-    misc: [{ text: "Let's go!", requiresPlayer: false }],
-  };
🤖 Fix all issues with AI agents
In `@src/client/graphics/layers/ChatModal.ts`:
- Around line 232-251: selectPlayer currently mixes translation keys and
rendered strings via previewText, which is fragile; change the flow so
previewText always holds the rendered display string (or maintain two explicit
fields like previewKey and previewText). Concretely: in selectPhrase (and
anywhere a phrase key is chosen) call translateText(key) once to get the base
translated string and store that result (or store key in previewKey and the
translated string in previewText), then in selectPlayer perform token
replacement on that translated string (replace "[P1]" / "[P2]") and set
previewText to the final rendered text; finally update render() to use
previewText directly (remove or skip translateText on previewText). Update the
selectPlayer/selectPhrase logic and any places referencing previewText to use
the new pattern so translation happens only once.

In `@src/client/graphics/layers/EventsDisplay.ts`:
- Around line 421-434: The fallback for resolving the second player is
incorrect: in the block handling event.target2 you call
this.game.player(event.target2) and compute target2Name =
target2Player?.displayName() ?? event.target but it should fall back to
event.target2 (the raw second-player ID). Update the fallback used when
target2Player?.displayName() is nullish so translatedMessage.replace("[P2]",
target2Name) uses event.target2 instead of event.target; keep the same try/catch
and warning behavior around the target2Player resolution.
🧹 Nitpick comments (5)
src/core/game/Game.ts (1)

813-821: target2 parameter added to displayChat interface — works but the signature is getting long.

Seven positional parameters with similar types (string | undefined, PlayerID | null, boolean, string) make call sites hard to read. Consider grouping into an options object in a future refactor.

src/client/graphics/layers/ChatModal.ts (4)

180-182: Send button is not disabled when requiresPlayer2Selection is true but P2 selection hasn't started yet.

Consider the scenario: a phrase that requires both players is selected, but only P1 has been chosen and the user somehow avoids triggering selectingPlayer2 (e.g., a race condition or future refactor changes the flow). The current check only guards on selectingPlayer2 && !selectedPlayer2, not on requiresPlayer2Selection && !selectedPlayer2.

A safer guard would be:

Suggested fix
            ?disabled=${!this.previewText ||
-            (this.requiresPlayerSelection && !this.selectedPlayer) ||
-            (this.selectingPlayer2 && !this.selectedPlayer2)}
+            (this.requiresPlayerSelection && !this.selectedPlayer) ||
+            (this.requiresPlayer2Selection && !this.selectedPlayer2)}

253-257: Remove debug console.log calls before merging.

These log statements (lines 254-257) look like development artifacts. Consider removing or gating them behind a debug flag.


235-242: The ?? null on line 237 is unnecessary.

String.prototype.replace() always returns a string, so the nullish coalescing is a no-op here. Same applies to line 246.

Suggested fix
-      this.previewText =
-        this.previewText.replace("[P2]", player.name()) ?? null;
+      this.previewText = this.previewText.replace("[P2]", player.name());

304-317: open() does not reset selectedPlayer or selectedPlayer2, risking stale state.

If a user opens the modal, selects players, closes without sending, and re-opens, selectedPlayer and selectedPlayer2 retain their previous values while the UI resets categories/phrases (via close()). The stale player references could point to players no longer alive or relevant. Consider resetting selectedPlayer in close() as well.

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Feb 13, 2026
Copy link
Contributor

@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

🤖 Fix all issues with AI agents
In `@src/client/graphics/layers/ChatModal.ts`:
- Around line 221-238: When re-selecting Player 1 in selectPlayer, clear any
stale Player 2 state so the UI and send logic don't use an old value: inside
selectPlayer (the branch that resets previewText from selectedPhraseTemplate and
sets selectedPlayer, selectingPlayer2, and requiresPlayerSelection), also set
selectedPlayer2 to null/undefined (and if needed clear any related flags that
gate the send button like requiresPlayer2Selection) so the previewText shows the
unresolved "[P2]" and the send button correctly disables until a new P2 is
chosen.
🧹 Nitpick comments (1)
src/client/graphics/layers/ChatModal.ts (1)

198-214: Three identical translateText calls — store the result once.

selectedPhraseTemplate, selectedPhraseText, and previewText all receive the same translated string. Call translateText once and reuse it.

Proposed simplification
  private selectPhrase(phrase: QuickChatPhrase) {
    this.selectedQuickChatKey = this.getFullQuickChatKey(
      this.selectedCategory!,
      phrase.key,
    );
-   this.selectedPhraseTemplate = translateText(
-     `chat.${this.selectedCategory}.${phrase.key}`,
-   );
-   this.selectedPhraseText = translateText(
-     `chat.${this.selectedCategory}.${phrase.key}`,
-   );
-   this.previewText = translateText(
-     `chat.${this.selectedCategory}.${phrase.key}`,
-   );
+   const translated = translateText(
+     `chat.${this.selectedCategory}.${phrase.key}`,
+   );
+   this.selectedPhraseTemplate = translated;
+   this.selectedPhraseText = translated;
+   this.previewText = translated;
    this.requiresPlayerSelection = phrase.requiresPlayer;
    this.requiresPlayer2Selection = phrase.requiresPlayer2 ?? false;
    this.requestUpdate();
  }

Copy link
Contributor

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/client/graphics/layers/ChatModal.ts (1)

198-215: ⚠️ Potential issue | 🟠 Major

Bug: selectPhrase does not reset second-player state.

If a user is mid-way through the P2 selection flow (so selectingPlayer2 === true) and then clicks a different phrase, selectingPlayer2 stays true. This causes the player column header to still display "Player [P2]" for a phrase that may not need two players. Also, selectedPlayer2 is never cleared here, so switching from a two-player phrase to a one-player phrase and sending will still include the stale selectedPlayer2 id in the event payload.

selectCategory already resets all P2 fields — selectPhrase should do the same.

Proposed fix — reset P2 state in selectPhrase
   private selectPhrase(phrase: QuickChatPhrase) {
     this.selectedQuickChatKey = this.getFullQuickChatKey(
       this.selectedCategory!,
       phrase.key,
     );
     this.selectedPhraseTemplate = translateText(
       `chat.${this.selectedCategory}.${phrase.key}`,
     );
     this.selectedPhraseText = translateText(
       `chat.${this.selectedCategory}.${phrase.key}`,
     );
     this.previewText = translateText(
       `chat.${this.selectedCategory}.${phrase.key}`,
     );
     this.requiresPlayerSelection = phrase.requiresPlayer;
     this.requiresPlayer2Selection = phrase.requiresPlayer2 ?? false;
+    this.selectedPlayer = null;
+    this.selectedPlayer2 = null;
+    this.selectingPlayer2 = false;
     this.requestUpdate();
   }
🧹 Nitpick comments (1)
src/client/graphics/layers/ChatModal.ts (1)

243-246: Remove debug console.log calls before merging.

These four log statements ("Sent message", "Sender", "Recipient", "Key") look like development leftovers. Same for the logs in open() (lines 295-296). They leak player info to the browser console and add noise.

Proposed cleanup
   private sendChatMessage() {
-    console.log("Sent message:", this.previewText);
-    console.log("Sender:", this.sender);
-    console.log("Recipient:", this.recipient);
-    console.log("Key:", this.selectedQuickChatKey);
-
     if (this.sender && this.recipient && this.selectedQuickChatKey) {
   public open(sender?: PlayerView, recipient?: PlayerView) {
     if (sender && recipient) {
-      console.log("Sent message:", recipient);
-      console.log("Sent message:", sender);
       this.players = this.g

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 13, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 13, 2026
Copy link
Contributor

@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 (3)
src/client/graphics/layers/ChatModal.ts (2)

245-250: Remove debug console.log calls from the send path.

These four log statements expose previewText, sender, recipient, and selectedQuickChatKey on every sent message. They are debug artifacts and should be removed before merging.

♻️ Proposed clean-up
  private sendChatMessage() {
-   console.log("Sent message:", this.previewText);
-   console.log("Sender:", this.sender);
-   console.log("Recipient:", this.recipient);
-   console.log("Key:", this.selectedQuickChatKey);
-
    if (this.sender && this.recipient && this.selectedQuickChatKey) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/ChatModal.ts` around lines 245 - 250, In the
ChatModal class remove the debug console.log statements inside the
sendChatMessage method (the lines logging this.previewText, this.sender,
this.recipient, and this.selectedQuickChatKey); simply delete those console.log
calls from sendChatMessage (or replace them with a proper, non-sensitive logger
call if required) so sensitive message/send-path details are not logged.

198-218: Three identical translateText calls can be collapsed to one.

selectedPhraseTemplate, selectedPhraseText, and previewText are all set to the same translateText(...) result. Each call performs a DOM query and format lookup.

♻️ Proposed clean-up
  private selectPhrase(phrase: QuickChatPhrase) {
    this.selectedQuickChatKey = this.getFullQuickChatKey(
      this.selectedCategory!,
      phrase.key,
    );
-   this.selectedPhraseTemplate = translateText(
-     `chat.${this.selectedCategory}.${phrase.key}`,
-   );
-   this.selectedPhraseText = translateText(
-     `chat.${this.selectedCategory}.${phrase.key}`,
-   );
-   this.previewText = translateText(
-     `chat.${this.selectedCategory}.${phrase.key}`,
-   );
+   const translatedPhrase = translateText(
+     `chat.${this.selectedCategory}.${phrase.key}`,
+   );
+   this.selectedPhraseTemplate = translatedPhrase;
+   this.selectedPhraseText = translatedPhrase;
+   this.previewText = translatedPhrase;
    this.requiresPlayerSelection = phrase.requiresPlayer;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/ChatModal.ts` around lines 198 - 218, In
selectPhrase, avoid calling translateText three times; compute const translated
= translateText(`chat.${this.selectedCategory}.${phrase.key}`) once and assign
translated to this.selectedPhraseTemplate, this.selectedPhraseText, and
this.previewText; keep the rest of the method (requiresPlayer*, selectedPlayer*,
selectUpdate call) unchanged and call this.requestUpdate() at the end.
tests/QuickChatExecutions.test.ts (1)

52-52: ?? [] guards on DisplayChatEvent arrays are unnecessary.

createGameUpdatesMap() pre-initialises every GameUpdateType key to an empty array, so updates[GameUpdateType.DisplayChatEvent] is always an array. The same pattern applies on lines 89, 111, 132, 135, 174, and 196.

♻️ Example clean-up (apply similarly to all occurrences)
-  const chatEvents = updates[GameUpdateType.DisplayChatEvent] ?? [];
+  const chatEvents = updates[GameUpdateType.DisplayChatEvent];

Based on learnings: game.executeNextTick() returns the createGameUpdatesMap() result, which initialises every key to []; optional-chaining and nullish-coalescing are not needed.

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

In `@tests/QuickChatExecutions.test.ts` at line 52, The guard "?? []" and any
optional-chaining on accesses like updates[GameUpdateType.DisplayChatEvent] are
unnecessary because createGameUpdatesMap() (the structure returned by
game.executeNextTick()) pre-initialises every GameUpdateType key to an array;
remove the nullish-coalescing and optional-chaining so lines referencing
updates[GameUpdateType.DisplayChatEvent] (and the other occurrences on lines
with GameUpdateType keys) directly use the array returned from
game.executeNextTick() (locate the references in
tests/QuickChatExecutions.test.ts and update the relevant expressions).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/QuickChatExecutions.test.ts`:
- Around line 39-74: The test is passing the i18n path
"chat.warnings.teaming_with" to QuickChatExecution but
ChatModal.getFullQuickChatKey() expects the compact chat key
"warnings.teaming_with"; update the QuickChatExecution instantiation in the test
to use "warnings.teaming_with" (replace the three-segment string) so
category/key parsing matches production behavior and the assertions for category
and key (via QuickChatExecution handling) will be correct.

---

Nitpick comments:
In `@src/client/graphics/layers/ChatModal.ts`:
- Around line 245-250: In the ChatModal class remove the debug console.log
statements inside the sendChatMessage method (the lines logging
this.previewText, this.sender, this.recipient, and this.selectedQuickChatKey);
simply delete those console.log calls from sendChatMessage (or replace them with
a proper, non-sensitive logger call if required) so sensitive message/send-path
details are not logged.
- Around line 198-218: In selectPhrase, avoid calling translateText three times;
compute const translated =
translateText(`chat.${this.selectedCategory}.${phrase.key}`) once and assign
translated to this.selectedPhraseTemplate, this.selectedPhraseText, and
this.previewText; keep the rest of the method (requiresPlayer*, selectedPlayer*,
selectUpdate call) unchanged and call this.requestUpdate() at the end.

In `@tests/QuickChatExecutions.test.ts`:
- Line 52: The guard "?? []" and any optional-chaining on accesses like
updates[GameUpdateType.DisplayChatEvent] are unnecessary because
createGameUpdatesMap() (the structure returned by game.executeNextTick())
pre-initialises every GameUpdateType key to an array; remove the
nullish-coalescing and optional-chaining so lines referencing
updates[GameUpdateType.DisplayChatEvent] (and the other occurrences on lines
with GameUpdateType keys) directly use the array returned from
game.executeNextTick() (locate the references in
tests/QuickChatExecutions.test.ts and update the relevant expressions).

@Caleb-Cohen Caleb-Cohen force-pushed the teaming-callout branch 3 times, most recently from d6c8056 to 21c5077 Compare February 21, 2026 11:39
…ated instead of calling translateText three times within selectPhrase function. Remove console.log debuggin within sendChatMessage().
…i18n translation path. Deleted redundant tests.
@iiamlewis iiamlewis added this to the v30 milestone Feb 23, 2026
@iiamlewis iiamlewis added Gameplay Features that affect gameplay Feature labels Feb 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature Gameplay Features that affect gameplay

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

Additional chat options

3 participants