Commit Graph
100 Commits
Author SHA1 Message Date
adminandGitHub e1686b6ee5 Revert "Default Shardok server status to WAITING_FOR_AI when no faction is qu…"
This reverts commit ebc29a75d4.
2026-05-08 07:13:59 -07:00
ebc29a75d4 Default Shardok server status to WAITING_FOR_AI when no faction is queued (#6689)
In Eagle battles, computeShardokServerStatus checks each faction's
currentCommand to decide between YOUR_TURN, WAITING_FOR_AI,
WAITING_FOR_HUMAN_PLAYER, and PROCESSING. The AI's currentCommand,
however, isn't populated during the window when the AI is computing
its move — so the typical post-player-turn state had no faction with
a current command and fell through to PROCESSING. Clients never
observed WAITING_FOR_AI in practice; they bounced between YOUR_TURN
and "Processing...".

When no faction has a current command, infer the most useful status
from which other factions are present in the battle: WAITING_FOR_AI
if any AI faction remains, WAITING_FOR_HUMAN_PLAYER if any other
human remains, and PROCESSING only when no other factions are present
(an edge case at battle boundaries).

The custom-battle path (CustomBattleManager.customBattleStatus) was
already binary YOUR_TURN/WAITING_FOR_AI and not affected.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 07:05:47 -07:00
f0d7067cc9 Wire ShardokServerStatus into the connection-status indicator (#6686)
* Wire ShardokServerStatus into the connection-status indicator

Client half of the Shardok-side connection-status work. The server
already populates ShardokServerStatus on SingleShardokGameResultResponse
(see #6685); this hooks it up to the persistent ConnectionStatusUI so
players can see whether they're up, the AI is thinking, or another human
is about to act while a battle is on screen.

- ShardokGameModel implements IShardokGameStateProvider and caches the
  latest ShardokServerStatus, updated as new responses arrive in
  EagleGameModel.
- ConnectionStatusUI gains a Shardok provider slot; when set, it renders
  Your Turn / Waiting for AI / Waiting for Other Player / Processing
  instead of the strategic-layer status.
- EagleGameController binds the UI to the Shardok model when entering a
  battle; ShardokGameController clears the binding on exit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix PersistentUIManager namespace in ShardokGameController

CI build failed with:
  error CS0234: The type or namespace name 'PersistentUIManager' does
  not exist in the namespace 'eagle'

PersistentUIManager lives in the `common` namespace, and Shardok already
has `using common;`, so the `eagle.` qualifier was both wrong and
unnecessary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Wire ShardokServerStatus through CustomBattleHandler

Custom battles use CustomBattleHandler as the gRPC subscriber instead
of GameModelUpdater, so the existing wiring on EagleGameController +
EagleGameModel didn't reach them — the indicator stayed on "Connected"
in custom battles even after the server began populating
ShardokServerStatus correctly.

- Set the ShardokGameModel as the connection-status UI's Shardok game
  state provider when entering a custom battle. Cleanup is already
  handled by ShardokGameController.EndTurn for both battle paths.
- Forward resp.ShardokServerStatus to the model from
  CustomBattleHandler.ReceiveGameUpdate, mirroring the per-response
  handling that GameModelUpdater already does for Eagle battles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 06:58:32 -07:00
e05ea92c46 Populate ShardokServerStatus in remaining response paths (#6687)
* Add doc describing server-side work for Shardok status indicator

Hand-off note for the Scala worktree: client wiring on
shardok-status-client is complete, but the indicator only ever shows
"Connected" in battle because the server attaches a ShardokServerStatus
proto with status left at the default UNKNOWN value.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Populate ShardokServerStatus in remaining response paths

PR #6685 added the ShardokServerStatus proto and populated it in
GameController.humanClientsAfterPostingResults, but three other
SingleShardokGameResultResponse construction sites left the field
unset, defaulting to UNKNOWN on the wire and producing the symptom
flagged in docs/SHARDOK_SERVER_STATUS_SERVER_WORK.md.

Fix:

- HumanPlayerClientConnectionState.streamUpdates / apply factory now
  take a shardokServerStatusFor: ShardokGameId => ShardokServerStatus
  callback and set the field on each per-battle response. Avoids a
  Bazel dep cycle into game_controller by accepting the precomputed
  function rather than calling computeShardokServerStatus directly.
- GameController.streamUpdates wires
  GameController.computeShardokServerStatus into that callback.
- CustomBattleManager populates the field at both construction sites
  via a small helper (YOUR_TURN if the human has a current command,
  WAITING_FOR_AI otherwise) — custom battles have only one human
  faction and the AI on -1, so the full computeShardokServerStatus
  machinery is overkill.

Existing test sites in HumanPlayerClientConnectionStateTest pass a
stub shardokServerStatusFor since they don't exercise active battles.

Removes the now-stale TODO doc — its diagnosis (enum never set) no
longer applies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 15:49:01 -07:00
b0639dc8d0 Add ShardokServerStatus proto and server-side population (#6685)
Introduces a new ShardokServerStatus message on
SingleShardokGameResultResponse so the client can render a tactical-side
connection-status indicator (Your Turn / Waiting for AI / Waiting for
Other Player / Processing).

ShardokServerStatus is intentionally separate from ServerGameStatus so
the strategic and tactical layers don't have to evolve in lockstep.

GameController.computeShardokServerStatus picks the status from the
receiving player's perspective by checking shardokPlayerAvailableCommands
across factions and classifying them via aiClientFactionIds. The two
humanClientsAfterPostingResults overloads now thread an aiFactionIds set
through and populate the new field on every per-battle response.

This is the server half of a two-PR split. The client wiring lands
separately; until then the new field is emitted but unread, which is
safe.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 06:07:54 -07:00
9eb8f7f086 Highlight March button in post-taxes tutorial dialogue (#6683)
Matches the existing pattern used for the Improve Province and Give Alms
buttons earlier in the strategic tutorial.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 21:49:53 -07:00
9f18322013 Increase Handle Captured Heroes panel font size (#6682)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 21:36:31 -07:00
7306c9cb06 Don't block dropGame on archive + battle cancellation (#6681)
When the last human drops a running game, the previous order was:
cancel-battles → archiveGame → mutate state → save() → return. Both
cancelBattlesForGame (gRPC roundtrip) and archiveGame (per-S3-file
copy + delete, all serialized) ran inside this.synchronized AND inside
the lock that gates the lobby push, so users could wait several
seconds to see the dropped game disappear from the lobby list.

Now we mutate state and save() first so the lobby update reflects the
removal immediately, then run cancelBattles + archive on a background
Future. Failures are logged.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 21:28:12 -07:00
3ff012806f Reliably highlight reinforcement heroes in battle tutorial (#6679)
* Highlight Elena (and other reinforcements) reliably in tutorial dialogues

Elena Fyar's hex wasn't pulsing during her arrival dialogue even though
the trigger fired. The most likely causes are: she's matched in
ReserveUnitsById rather than UnitsById at the moment the dialogue plays,
the SelfHostility check rejects an allied-rendered reinforcement, or her
ProfessionInfo isn't populated on whichever diff happens to be the
latest UnitView in UnitsById when ApplyUnitHighlight runs.

Three changes, defense in depth:

- DialogueManager.ApplyUnitHighlight now scans UnitsById ∪ ReserveUnitsById
  (still requires a placed Location), and treats any non-enemy unit as
  highlightable rather than requiring SelfHostility.
- Elena's and Ranil's reinforcement dialogues, plus engineer_bombard
  (which is Ranil speaking about himself), now match by heroName instead
  of profession. Each dialogue is about a specific hero by name, and
  heroName goes through unit.Name (text-id-based) which doesn't depend
  on ProfessionInfo being present on the latest diff.
- duel keeps profession matching since the dialogue is about Champions
  in general.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Re-apply tutorial unit highlight every frame

The Shardok controller's ModelUpdated() calls hexGrid.ClearOverlays() on
every model update, which wiped the tutorial unit highlight applied
during step 1 of a multi-step dialogue: by the time the player was
reading Marek's lead-in line about Elena (or Ranil), a follow-up server
update had cleared the overlay. Step 2 happened to survive because no
further model updates arrived between the player advancing the dialogue
and the render.

DialogueManager now stores the current step's highlight criteria and
re-applies it every frame from Update() so it survives any number of
ClearOverlays calls. ApplyUnitHighlight is now idempotent (clears its
tracked cells before re-scanning).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 21:15:39 -07:00
904dadf170 Bump Luke the Prank-tricker tutorial bias to 300 to outpace decay (#6680)
The previous 130 setting netted +30 effective bias after
FactionBiasFromImprisonment (-100), which then decayed to 0 within
3 rounds via NewRoundAction's per-round bias decay (additive -10/round
below |100|, multiplicative *0.9 above). By the time prison-round odds
had accumulated, the bias contribution was already gone.

Setting the config to 300 yields +200 effective bias, putting it in the
multiplicative-decay regime: ~+131 by round 4 (Nov) and ~+96 by round 7
(Feb). That keeps Luke comfortably over MinOddsForRecruitment (80)
through the intended early-tutorial recruitment window.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 21:05:36 -07:00
579d04eafd Rename knights in tutorial opening dialogue to Doomriders (#6678)
Old Marek's first tutorial-battle line previously named Shardok's Guard
as "the finest soldiers in the realm" and referred to "knights besides"
as a separate unit. Since the heavy-cavalry battalion in the attacking
army is named Doomriders, attribute the knights line to them directly
and drop the implicit second unit reference.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:46:48 -07:00
acdea65632 Highlight referenced units during battle tutorial dialogues (#6676)
Several battle tutorial dialogues call out specific units by role
(longbows for archery, the champion for dueling, Marek for charging,
the engineer/paladin reinforcements, Hedrick for hiding) or by an
available action (melee, start fire). The dialogue had no way to point
at the unit on the battlefield, so the player had to find it themselves.

Adds an optional `highlightUnit` block to dialogue steps with criteria
fields (battalionType, profession, heroName, canStartFire, canMelee)
that DialogueManager resolves against the current Shardok model. Each
matching friendly unit gets a strobing border via HexGrid.OverlayCell
— the same primitive already used for archery range and movement
overlays — so the visual language matches existing combat highlights.
The overlay is cleared on step change and dialogue end.

Wires up the criteria for all relevant battle dialogues:
- archery_available → Longbowmen
- ability_charge_available → Old Marek the Learned
- melee_available → friendly unit with a MeleeCommand available
- start_fire_available → friendly unit with CanStartFire
- duel_available → Champion
- tutorial_reinforcement_paladin → Paladin (Elena)
- tutorial_reinforcement_engineer → Engineer (Ranil)
- engineer_near_enemy → Engineer
- hide_available → Hedrick the Hedge-Merchant

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:38:18 -07:00
b4bb2f6d7f Bump Luke the Prank-tricker tutorial recruitment bias to 130 (#6677)
The tutorial configures +110 faction-3 bias on Luke so that he becomes
recruitable shortly after capture, but EndBattleAftermathPhaseAction
applies FactionBiasFromImprisonment (-100) to the imprisoning faction
and merges in initialFactionBiases on top of it. The effective bias was
only +10, which left him needing several extra rounds of prison-time
odds adjustment before reaching MinOddsForRecruitment (~Feb/Mar 372 in
practice). Bumping to 130 raises effective bias to +30 (~4 rounds
earlier), pulling first recruitability to ~Nov 371 as intended.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:30:58 -07:00
44546ac3e0 Mutually distrust the three tutorial NPC factions (#6675)
Bregos, Bridget, and Hedrick now all start at -500 trust toward each
other. With trust drift at +1/round, that's effectively permanent for
tutorial timescales. The invitation gate (TrustForDiplomacy.scala) only
checks the inviter's trust toward the target, so previously Bregos's
trust toward the smaller factions was an unset 0 and he could try to
absorb them after his earliestRoundForInvitation passed.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:27:26 -07:00
ab44d53b60 Point tutorial highlight at the Fight button on the battle progress panel (#6674)
The opening tutorial's pre-battle step instructed the player to click
"Battle!" and highlighted "GoToBattleButton", but that UI was replaced
by per-battle Fight buttons on the BattleProgressPanel. The dialogue now
references "Fight!" and BattleProgressRowController dynamically registers
its action button as "FightButton" with the tutorial target registry while
the row is in the fightable state, unregistering otherwise (and on
destroy) to avoid stale highlight targets.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 20:03:09 -07:00
02cffbdccb Check off completed tutorial items (#6673)
The Shardok tutorial, narrative hook, and early-victory items are
already delivered by the dialogue system: tutorial_battle.json covers
combat mechanics start to finish, tutorial_opening provides Sadar's
backstory in the first scene, and surviving Tarn's attack is the
early-victory beat.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 19:52:07 -07:00
152b72d691 Load Eagle/Tarn province setup from game_parameters.json (#6671)
EagleAppearsAction had Province32Stats / Province6Stats hardcoded
alongside hardcoded battalion definitions and random hero counts —
all duplicating the corresponding entries in game_parameters.json.
Any tuning of the normal game's Ingia/Soria starting state would
silently diverge in the tutorial.

Replace the hardcoded constants with a lazy load from
GameParametersUtils.defaultExpandedGameParameters: find the
SetFaction whose head is The Eagle, derive ProvinceConfig (stats,
random hero count, battalion templates) from each occupied province,
and look up which province belongs to the Eagle vs. Tarn by primary
ruling hero name. Battalions come through BattalionConverter.fromProto
and have IDs assigned at result time.

Behavior is preserved (verified by EagleAppearsActionTest).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 19:23:03 -07:00
ea7677e351 Add dialogue for Ranil and Elena forming their own factions (#6672)
* Add dialogue for Ranil and Elena forming their own factions

The tutorial_hero_faction_appears trigger fires when John Ranil and
Elena Fyar break off from Sadar's service to form their own factions
(Builders of a Better World, Champions of Justice) seven rounds after
The Eagle appears. The trigger had no dialogue script, so a major
narrative beat — losing two of the player's signature allies — fired
silently.

Add a 4-step dialogue: Marek frames the departures as the same
mysterious force that has been claiming the King's heroes; Ranil and
Elena each speak their reason for leaving; Marek closes by noting they
are not enemies and an alliance may be possible later.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Sharpen Marek's closing line to convey ambition

Old Marek now frames Ranil and Elena as obstacles the kingdom may need
to go through, not just former allies on different paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Don't blame Bregos Fyar as the realm's arsonist

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 19:15:45 -07:00
d57003e144 Add tests for EagleAppearsAction (#6670)
The TutorialStrategicEventsTest comment claimed EagleAppearsAction's
result-creation was tested in TutorialGameCreationTest, but no such
tests existed — leaving the Eagle/Tarn faction setup uncovered.

Add EagleAppearsActionTest covering: faction creation and hostility,
The Eagle hero, Tarn's faction switch and loyalty, province 32 and 6
ownership / hero placement / stats, and battalion creation. Run
against a real bootstrapped tutorial game state via
TutorialGameCreation.createTutorialGame.

Also fix the stale comment in TutorialStrategicEventsTest.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 19:10:04 -07:00
4c720983b7 Fix stale tutorial-battle origin province reference (#6669)
Doc said Tarn's attacking army originates from province 32; the
config in tutorial_parameters.json:102 has originProvinceId: 31.
Province 32 (Ingia) is unoccupied at game start and only becomes
relevant later when The Eagle appears.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 17:58:05 -07:00
5831dc6e7a Tutorial system doc: add missing triggers and full expected flow (#6668)
Catalog four triggers missing from the original doc:
tutorial_faction_appears, tutorial_hero_faction_appears,
tutorial_hero_departed, and tutorial_hero_departed_again (all from
OnStrategicActionResult), plus shardok_battle_reset (fired directly
from ShardokGameController, bypassing the registry).

Replace the brief expected-flow stub with the full strategic-map and
tactical-battle dialogue flow tables, plus the underlying narrative
arc, so future tutorial work has a concrete map of what fires and
when.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 17:12:42 -07:00
79ee1e4e28 Docs: update Small Eagle TODO and add tutorial system architecture doc (#6667)
* Update Small Eagle TODO: lobby fixes done, merge tutorial + onboarding

Mark lobby fixes complete and consolidate the tutorial and
first-session onboarding bullets into a single section, since they
overlap heavily (a guided first scenario is essentially a tutorial).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add tutorial system architecture doc

Document the Unity tutorial system's class map, step lifecycle,
trigger catalog (with file:line citations), highlight targets,
persistence, and expected first-session flow. Calls out that the
step-based UI is currently dormant (RegisterAll early-returns) and
the dialogue system is the live path today.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 17:01:59 -07:00
26100acbe5 Show a proper page with a back link when a game URL is not found (#6666)
Previously /games/<id> returned a plain-text 404 with no way back to
the games list. Now renders an HTML error page through the shared
layout with a link back to /games.

Adds a generic error.html template and a renderError helper so other
handlers can use the same pattern.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 18:11:09 -07:00
0af9a11bc0 Commit missing .meta files for tracked scripts (#6665)
AddressableLoader.cs and AssetUsageAuditor.cs were committed without
their .meta files, so each dev's Unity was generating a different
local GUID. Commit the metas to lock the GUIDs so any future
prefab/ScriptableObject reference will be stable across clones.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 13:45:17 -07:00
a9ebe48a76 Align battle thermometer rows regardless of troop-count visibility (#6664)
Rows where the viewer sees troop counts (allied) render non-empty
AttackerLabel / DefenderLabel, while other rows render them empty.
The labels' LayoutElement had MinWidth=-1, so Unity fell through to
TMP's content-dependent minWidth, making the bar area's inner edges
land at different X positions across rows whenever available width
was tight.

Pin MinWidth=100 (== PreferredWidth) on both labels so their width is
content-agnostic and the bar areas line up.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 21:19:48 -07:00
a582af560d Update Gameplay scene layout (#6663)
Rebakes many UI RectTransforms in the Gameplay scene: several panels
switch to layout-driven sizing (anchors/position/size zeroed to let a
parent LayoutGroup/ContentSizeFitter drive them) and a few panels get
explicit resizes.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 20:15:15 -07:00
adminandGitHub 28f0081a86 Fix turn-mismatch reconnect during chronicle text generation (#6662)
* Report WAITING_FOR_PLAYERS while an AI is taking its turn

computeServerGameStatus only considered other human factions when
checking whether to report WAITING_FOR_PLAYERS, so when a human player
finished their turn and the server transitioned to AI play it returned
None. The client keeps its previous YOUR_TURN status when the server
sends a None update (ServerStatus is only overwritten when non-null)
while HandleAvailableCommands clears commands on the same update, so
CheckForTurnMismatch fired a spurious reconnect after its 3s grace.

Pass all other faction IDs (human + AI) into computeServerGameStatus
so WAITING_FOR_PLAYERS is reported whenever any other player has
commands to act on.

* Count unrequested (queued) texts in hasIncompleteTextsAccessibleTo

Chronicle texts begin their life in unrequestedTexts and only move to
incompleteTexts once withMarkedRequested fires as the LLM request
actually starts. If computeServerGameStatus ran during that window -
after the player's commands had already cleared but before the LLM
request had been kicked off - hasIncompleteTextsAccessibleTo returned
false, the status fell through to None, and the client kept its stale
YOUR_TURN while its commands were cleared on the same update. That is
the actual trigger for the 3s turn-mismatch reconnect we kept chasing:
waiting for the chronicle text to start streaming, not waiting for an
AI turn.

Include 'unrequested' alongside 'incomplete' in both the in-memory
default implementation and the SQLite EXISTS query so we report
GENERATING_TEXT for the full queued + generating window.
2026-04-22 19:08:41 -07:00
adminandGitHub fe6b50b055 Unify ServerGameStatus reported on re-subscribe with post-result path (#6661)
The re-subscribe path in HumanPlayerClientConnectionState.streamUpdates
was computing ServerGameStatus using simpler logic than
humanClientsAfterPostingResults: it only reported YOUR_TURN (when the
player had commands) or None otherwise. When a client re-subscribed
while another player was taking their turn, a battle was in progress, or
chronicle text was generating, the server sent serverGameStatus=None and
empty availableCommands. The client cleared its command state but kept
the stale YOUR_TURN status from the previous update (ServerStatus is
only overwritten when non-null), which tripped CheckForTurnMismatch
after its 3s grace and triggered a spurious reconnect.

Extract GameController.computeServerGameStatus as a single source of
truth, and route it through HumanPlayerClientConnectionState.apply and
through rewindTo so all three paths agree on
BATTLE_IN_PROGRESS / YOUR_TURN / GENERATING_TEXT / WAITING_FOR_PLAYERS.
2026-04-22 07:26:10 -07:00
4ee0230b18 Add failing test for fleeing army at hostile province (#6660)
* Add failing test: fleeing army at hostile province should not shatter

A defender that fled a battle lands at its pre-selected flee province
with fleeProvinceId cleared. If that province has since been conquered
by the enemy, shatterStrandedArmies destroys the army outright — but the
army still has combat power and should get a chance at battle via the
normal HostileArmySetup → AttackDecision flow.

This commit flips the prior "should shatter" test to the behavior we
actually want. Fix to follow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Remove shatterStrandedArmies so fleeing armies can trigger battle

Fleeing armies arrive at their pre-selected flee province with
fleeProvinceId cleared (by ResolveBattleAction) so they can't flee
again. If the province has been conquered by the enemy in the meantime,
shatterStrandedArmies was destroying the retreating force outright
during ProvinceMoveResolution — even though the army still had combat
power and should get a chance at battle.

Removing the block lets these incoming armies fall through to
HostileArmySetup later in the same round, where they become hostile
army groups and trigger the normal AttackDecision → battle flow.

Also drops the now-unused ShatteredArmyUtils.shatteredStrandedArmyResult
helper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 08:20:27 -07:00
af75f7af74 Add UpgradeBattalionQuest AI quest handler (#6659)
Adds a deterministic chooser that walks a priority tree per turn toward
fulfilling an UpgradeBattalionQuest: arm-completes (optionally preceded by
travel and/or a safe food sale to cover the gold cost) -> train-completes
-> march in a pre-qualified neighbor battalion -> organize/top-off of the
quest battalion type -> march in an unqualified neighbor battalion ->
develop Economy/Agriculture -> train progress -> develop Infrastructure
-> arm progress. Travel is only toggled when it directly enables an
arm-completes finisher, to avoid fragile multi-turn plans.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-16 22:12:37 -07:00
614534ac54 Add BetrayAllyQuest AI quest handler (#6658)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 22:02:24 -07:00
4696d56ec6 Add FightBeastsAloneQuest AI quest handler (#6656)
* Add FightBeastsAloneQuest AI quest handler

When an unaffiliated hero offers a FightBeastsAloneQuest, the AI now
sends the weakest expendable non-leader hero to fight beasts without a
battalion. Only heroes weaker than the quest-giving hero are considered,
since the hero will almost certainly die.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add MaxExpendableHeroPowerRatio setting for FightBeastsAloneQuest

Hero must have power at most 75% of the quest-giving hero's power to
be considered expendable, ensuring the trade is clearly worthwhile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add MaxExpendableHeroPowerRatio to settings.tsv and regenerate loader BUILD

The new setting was added to the settings BUILD.bazel but was missing from the
TSV source file, causing the auto-generated settings_loader to lack the dep.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 21:47:13 -07:00
4af198fe1a Add BattalionDiversityQuest AI quest handler (#6657)
* Add BattalionDiversityQuest AI quest handler

When an unaffiliated hero offers a BattalionDiversityQuest, the AI now
hires one battalion of a type it doesn't already have. Only types where
the province meets development requirements are considered, and the
province must have sufficient gold and food surplus.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Only hire battalion when it would reach 3+ distinct types

The quest requires 3+ different types at near-full capacity. Skip
hiring if the province has fewer than 2 existing types, since adding
one more wouldn't reach the required 3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 21:42:40 -07:00
e74e26f191 Update AI quest completion plan with prioritized backlog (#6655)
Reorganize the "Does Not Attempt" section into a prioritized plan for
future implementation and a list of quests not planned for active
pursuit.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 12:55:25 -07:00
dadb374143 Prefer CrackDown for riots when SuppressRiotByForceQuest exists (#6654)
When an unaffiliated hero has a SuppressRiotByForceQuest and the faction
has battalions available, the AI now prefers CrackDown over Give when
handling riots. This modifies the existing riot handler priority rather
than adding a quest command chooser, since riot handling runs before
FulfillQuestsCommandSelector.

The AI estimates survival odds against a 90th-percentile riot before
committing to CrackDown, and prefers non-leader heroes to avoid risking
the faction head.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 11:22:07 -07:00
5bccde82ac Add ApprehendOutlawQuest AI quest handler (#6653) (#6653)
Matches the quest's target outlawHeroId against available outlaws in the
province and issues an ApprehendOutlaw command when found.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 09:46:39 -07:00
b0c7c57402 Add ReconSpecificProvincesQuest AI quest handler (#6652) (#6652)
Higher priority than the count-based ReconProvincesQuest since reconning
a specific province also counts toward generic recon counts. Prefers
targets not yet reconned by the faction.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 09:26:59 -07:00
f6850fc838 Add ReconProvincesQuest AI quest handler (#6651)
* Add ReconProvincesQuest AI quest handler

When an unaffiliated hero has a ReconProvincesQuest, the AI now issues
a Recon command to reconnoiter a province, helping fulfill the quest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Prioritize recon target provinces in ReconProvincesQuestCommandChooser

Sort candidates by (isNeighbor, isHostile, notRecentlyReconned) to
prefer hostile neighboring provinces that haven't been scouted recently,
matching the prioritization pattern from MidGameAIClient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Deprioritize unowned provinces in recon target selection

Unowned provinces (no ruling faction) are now sorted last, even if
they're neighbors, since reconning owned hostile provinces is more
strategically valuable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 09:15:28 -07:00
2f783f7ab0 Add GrandArmyQuest AI quest handler (#6650)
* Add GrandArmyQuest AI quest handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review: extract helper, use flatMap/inside, verify quest fulfillable

- Extract grandArmyTarget into a private method with pattern matching
- Replace organize match with flatMap chain
- Only return command if total troops after organizing meet the quest
  target (prevents partial fulfillment)
- Replace asInstanceOf with inside() in tests
- Add test for insufficient resources to meet target

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 08:05:45 -07:00
7bdbd7ec8f Add AI quest handlers for blizzard, drought, and epidemic filtering (#6649)
* Add AI quest handlers for blizzard, drought, and epidemic own-province filtering

Create ControlWeatherQuestCommandChooser to handle StartBlizzardQuest and
StartDroughtQuest via the ControlWeather command. Update StartEpidemicQuest
CommandChooser to skip quests targeting the acting faction's own provinces.
Both handlers verify the appropriate command is available before selecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review: use inside() pattern and extract questTargets method

- Replace asInstanceOf with inside() pattern in ControlWeather and
  StartEpidemic test assertions
- Extract questTargets into a private method in
  ControlWeatherQuestCommandChooser and match directly on its result

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 07:35:23 -07:00
334a431f14 Add DevelopProvinces and MobilizeProvinces AI quest handlers (#6648)
* Add DevelopProvinces and MobilizeProvinces AI quest handlers

Implement IssueOrders-based quest command choosers so the AI can
fulfill DevelopProvincesQuest and MobilizeProvincesQuest by switching
province orders. Develop prefers non-hostile-neighbor provinces;
Mobilize prefers hostile-neighbor provinces. Mobilize is skipped when
a DevelopProvincesQuest also exists to avoid conflicting orders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract ProvinceOrderQuestHelper to deduplicate order-switching logic

The Develop and Mobilize choosers shared nearly identical logic for
finding the IssueOrders command, counting current orders, partitioning
by hostile neighbors, and building the selection. Extract this into
ProvinceOrderQuestHelper.chooseOrders parameterized by target order
type, hostile-neighbor preference, and reason string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove early returns from province order quest choosers

Use pattern matching, flatMap, and Option.when instead of early
returns for idiomatic Scala style.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 21:27:30 -07:00
4ba9a475da Filter out Fear from AI search when attacker is alone (#6647)
Without a teammate to capitalize on the stun, a solo attacker casting Fear
just ends its turn no closer to killing units or capturing castles. The
state scorer currently rewards the resulting morale drop on the victim,
so minimax/MCTS can pick Fear over useful actions. Prune it in
AICommandFilter with one exception: if the victim is already on a fire
tile, the stun locks them there and the burn damage supplies the
follow-up, so Fear is still worthwhile in that case.

The rule only applies to attackers — defenders legitimately use Fear to
stall, which is their win condition.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 19:52:30 -07:00
acb409bf3f Update AI quest completion docs to match code (#6646)
The doc was missing 11 quest handlers that were added since it was
written: all prisoner quests, TotalDevelopment, SpendOnFeasts,
SendSupplies, RestProvince, StartEpidemic, and SwearBrotherhood.
Also fixes the priority order (Alliance is last, not first).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 19:46:47 -07:00
0389357366 Hide [CNTL] modifier hint on iOS and Android (#6645)
There is no Ctrl key on mobile platforms, so the [CNTL] helper text
under the profession attack button is meaningless. Skip it when
running on IPhonePlayer or Android.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 19:06:58 -07:00
a470bb16da Hide [CNTL] helper for untargeted profession commands like Fortify (#6644)
The profession attack button group (group 5) always showed "[CNTL]"
underneath, but that hint only makes sense for targeted commands like
Reduce where the player can ctrl-click a hex. Fortify has no target,
so showing the modifier key shortcut is misleading. Now the helper
text only appears when the group has a targeted command.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 19:02:07 -07:00
37ac215b85 Dedupe stale ActionResultViews in EagleGameModel.HandleUpdates (#6635)
* Dedupe stale ActionResultViews by unfiltered result count on main thread

Eagle's HandleUpdates previously applied every ActionResultView it was
handed, regardless of whether those results had already been applied.
The only count-based safety net was the heartbeat SyncMismatch check,
which triggers a reconnect but does not stop an in-flight MainQueue job
from applying stale deltas first. Applying a stale ActionResultView
(e.g. re-executing VASSAL_EXILED) can corrupt client state and throw
"Duplicate unaffiliated hero".

Track a main-thread-only high-water mark _lastAppliedUnfilteredCount and
gate HandleUpdates on it: if a delivery's UnfilteredResultCountAfter is
not ahead of the high-water mark but carries results, drop them with a
[DEDUP] log line. Reset the high-water mark in HandleStartingState so
state resyncs/rewinds rebuild the baseline cleanly.

This is defense-in-depth alongside the mid-stream SUBSCRIBE fix: it
protects the main thread against any source of stale/duplicate
ActionResultView delivery, not just the specific TryPendingCommands race.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Handle mid-batch exceptions in HandleUpdates by forcing a state resync

The previous commit added a dedupe gate but assumed ActionResultView
batches were applied atomically. If HandleNewHistoryEntry throws partway
through a batch, _currentModel is left partially mutated:
_lastAppliedUnfilteredCount never advances (so future batches would
re-apply the 1..k results that did land), while _lastUnfilteredResultCount
on the gRPC thread has already advanced (so the heartbeat sync check
reports "in sync" and never schedules a reconnect). The client
invisibly corrupts and the dedupe gate itself makes the next delivery
worse, not better.

Wrap the apply loop in try/catch. On exception:
- mark the model _modelCorrupted and refuse to apply any further
  GameUpdates in ReceiveGameUpdate until a fresh StartingState arrives
- reset both _lastUnfilteredResultCount (under its lock) and
  _lastAppliedUnfilteredCount to 0 so the upcoming subscribe asks the
  server for a fresh snapshot
- report the exception via ErrorHandler so we still get a stack trace
- trigger PersistentConnection.ForceReconnect() to drive the resync

HandleStartingState clears the _modelCorrupted flag when the fresh
snapshot lands, at which point normal update processing resumes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 08:57:26 -07:00
830985e01f Run S3 archive cleanup on self-hosted runner with aws CLI (#6643)
Switches from ubuntu-latest to [self-hosted, bazel] and replaces
s3cmd with aws CLI. Installs aws via brew if not already present.
Uses DO_SPACES credentials (same key pair the Eagle server uses to
write archived games) and macOS date syntax.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 08:47:30 -07:00
a50d64ee94 Disable scheduled Bazel cache cleanup (#6642)
bazel clean races with other runners sharing the output_base on
/Volumes/remote_cache, causing "Directory not empty" failures
(4 of last 5 runs). Manual trigger preserved via workflow_dispatch.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 08:13:26 -07:00
b8f6b99db2 Color unit labels via TMP_Text.color instead of rich-text tags (#6640)
* Color unit labels via TMP_Text.color instead of rich-text tags

SetOneUnitLabels was building <color=#...>Name</color> / <b> wrapped
strings on every update. Since each label is rendered in a single color,
setting TMP_Text.color directly is cheaper (no per-update string
concatenation, no rich-text tag parsing) and safer (type-checked Color
instead of hex string formatting). Secondary-label bold is now a font
style set once at cell creation.

SetUnitInfoLabels now takes an explicit Color parameter; all four
callers updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Disable richText on hex-grid labels

None of the hex-grid TMP labels (hero names, battalion sizes, odds
percentages, action points, coord debug strings) use rich-text tags
after the previous commit. Disabling richText in the shared label
factory skips TMP's per-update tag-scanning pass and makes the labels
robust against accidental '<' characters in localized text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 21:25:38 -07:00
c340f2b79d Color retargetable mages purple in battle UI (#6639)
Adds a third unit-label color in Shardok battles: purple (#8000FF) for a
player's mage that has already picked a meteor target but is still in the
Target state and could retarget on the same turn. Blue/black remain for
"has commands" / "no commands".

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 13:37:29 -07:00
c94422f1cf Clear focus province when faction loses ruler (#6638)
* Clear focus province when faction loses ruler to keep state valid

RuntimeValidator requires that every faction's focusProvinceId points
to a province it rules. ProvinceConqueredAction and
EndPlayerCommandsPhaseAction already emit ChangedFactionC with
clearFocusProvinceId for their respective pathways, but other pathways
(hero departures, battle casualties leaving a province unruled, etc.)
reach the applier's fixRulerIfNeeded hook without any faction-side
cleanup. The next validation pass then crashes with "Unowned focus
province for faction ...".

Clear the stale focus alongside fixRulerIfNeeded in the applier so any
ChangedProvinceC that removes or transfers a ruling faction also drops
the now-invalid focus pointers. The negative case (same ruler, just
losing some heroes) is preserved so active focuses aren't dropped
unnecessarily.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Switch to end-of-phase focus cleanup instead of applier side-effect

Reverts the applier-level focus-clearing added in the previous commit.
Modifying factions as a side-effect of applyChangedProvinceC violates
the event-sourcing model: every game state change should be described
by an ActionResult, not implicit in the applier.

New approach, matching the pattern used by other
EndPlayerCommandsPhaseAction-style cleanups:

- RuntimeValidator.validateFactions now only enforces the "focus must
  be an owned province" invariant during PlayerCommands. Other phases
  may legitimately leave a stale focusProvinceId in transit (e.g.
  fixRulerIfNeeded silently clears rulingFactionId after hero
  departures or battle casualties).

- EndVassalCommandsPhaseAction is the sole entry point into
  PlayerCommands. Its final ActionResultC now carries
  ChangedFactionC(clearFocusProvinceId = true) for every faction
  whose focus province is no longer ruled by it, so the invariant
  holds when the validator re-engages.

- Removes the three applier-level tests that exercised the reverted
  side-effect, and adds EndVassalCommandsPhaseActionTest covering the
  new cleanup behavior (unruled focus, focus taken by another
  faction, and focus still held).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 10:10:08 -07:00
4b4a4e7dc6 Don't shatter stranded armies arriving at unoccupied provinces (#6637)
shatterStrandedArmies only checked whether an incoming army's destination
was not its own faction's territory. Since isFriendlyMove does
`province.rulingFactionId.contains(...)`, unoccupied provinces (None)
also returned false, so any already-retreated army (fleeProvinceId = None)
arriving at an unoccupied province was spuriously shattered with a
"nowhere to withdraw to" notification.

Add an explicit `rulingFactionId.isDefined` guard so arrivals at
unoccupied provinces fall through to the normal uncontested conquest
flow.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 19:54:55 -07:00
aba582bd10 Stop mid-stream SUBSCRIBE on stale command token to prevent duplicate updates (#6634)
TryPendingCommands' stale-token branches called StreamOneGameAsync to
"refresh state" when a pending command's token was behind the current
token. This sent a SUBSCRIBE(unfilteredCount=N) over the active stream,
which caused the server to replay history.since(N) as a new
ActionResultResponse while normal updates were still in flight.

Those re-delivered ActionResultViews were then applied a second time on
the main thread, which could surface as (e.g.) "Duplicate unaffiliated
hero N in province M" when a VASSAL_EXILED action ran twice.

The refresh is unnecessary: we only reach the stale-token branch because
an ActionResultResponse already delivered the newer token and its
updates were applied. Drop the stale command without re-subscribing.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 18:40:38 -07:00
a7d3b2b421 Fix nonsensical "over X months" in supply/feast quest descriptions (#6636)
The divination and quest-ended prompts for SendSuppliesQuest and
SpendOnFeastsQuest both said "over $componentCount months", but
componentCount on these quests equals the total food/gold (since the
counter is incremented by units delivered, not months elapsed). This
produced confusing output like "send 1337 food over 1337 months".

Drop the time clause — these quests are purely cumulative totals.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 16:46:59 -07:00
f6ff303ba9 Catch exceptions in ReceiveGameUpdate to avoid silent state corruption (#6633)
When an exception propagates out of an async-void lambda enqueued to
MainQueue, it goes through AsyncMethodBuilderCore.ThrowAsync to Unity's
SynchronizationContext, which logs it but cannot undo the partial state
changes already applied by the failing update. Subsequent updates then
apply diffs on top of corrupt state, cascading further errors until the
client is effectively unusable.

Wrap ReceiveGameUpdate at all four call sites in HandleGameUpdate so
exceptions are caught at the source, logged via Debug.LogException
(triggering the in-game ErrorHandler panel) and the file logger, and the
enclosing lambda completes normally. TryPendingCommands and timing logs
still run, keeping the MainQueue pump healthy.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 07:14:40 -07:00
5813bc9500 Restore missing VelociraptorColor1/2 prefabs (#6632)
VelociraptorEffect.prefab's animalPrefabs array references two
prefab GUIDs (8fa2338697777704fafc6374e9750de9 and
df9fda81573442940a2a92a3628b3cbc) from the Dino Pack Low Poly V1
asset bundle that were never committed to the repo in PR #6525.
The pack's Velociraptor.fbx, materials, and textures were added,
but the standalone .prefab files that wrap the fbx with an
Animation component and a MeshRenderer were missing.

The dangling references didn't fail until something in the live
game actually triggered ProvinceBeastsController.SpawnBeastsEffect
for a "velociraptor" beast, at which point AnimalEffect.SpawnAnimals
threw MissingReferenceException on the null array element.

Re-import the two referenced prefabs from Dino Pack Low Poly V1
(PrefabPack/PrefabSingleTexture/VelociraptorP/). The re-imported
prefab GUIDs match exactly, and the root GameObject fileIDs
(7255238702826593904 and 8644578109599837090) match the fileIDs
VelociraptorEffect.prefab already references. Internal references
(VelociraptorC1.mat material, Velociraptor.fbx mesh and animations,
VelociraptorMapAnims.controller) are all already in the repo.

The atlas-texture variant prefabs from the same pack were not
committed since nothing references them.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 22:02:49 -07:00
a912347dbd Serialize TryPendingCommands to prevent BAD_TOKEN bursts (#6630)
Multiple MainQueue actions (one per ActionResultResponse and
ShardokActionResultResponse) each call TryPendingCommands as
async void lambdas. When one yields at an await, the MainQueue
Update loop immediately invokes the next action, which re-enters
TryPendingCommands. Because PostRequest synchronously re-adds
the command to _pendingCommands before yielding for WriteAsync,
each parallel invocation re-drains the same buffered command and
writes it to the gRPC stream again. This produces bursts of 10+
identical writes in a single frame, all of which the server
rejects with BAD_TOKEN.

Wrap TryPendingCommands in a non-blocking SemaphoreSlim acquire.
If another invocation is already running, skip and return — any
items added to _pendingCommands while it runs will be picked up
by the next ActionResultResponse handler that fires.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 21:34:12 -07:00
e2221c4e45 Suppress "Reconnecting..." UX during the first retry (#6627)
Occasional disconnects are normal and usually recover on the first
retry, so the old behavior of immediately flashing "Reconnecting..."
(and a 2s countdown) on every blip was too noisy.

Now the first retry after a fresh disconnect is "silent": we don't
touch _currentState, so the status UI keeps showing Connected while
the retry attempt runs in the background. Only after the first retry
has itself failed does the ScheduleReconnect path flip state to
Reconnecting, at which point the user sees the countdown (starting
from the 4s second-attempt backoff).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 20:37:37 -07:00
5af91f6e8b Multiply focus province storage caps by a settable multiplier (#6628)
Introduces the FocusProvinceStorageMultiplier setting (default 1.50) and
applies it to food and gold caps when a province is its ruling faction's
focus province, so focus provinces can stockpile extra reserves.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 20:27:12 -07:00
bc442ba230 Add delta gradient flash to battle thermometer (#6624)
* Add delta gradient flash to battle thermometer

When the thermometer ratio changes, overlay a gradient on the delta
region showing the gaining side's color fading toward white-tinted,
making the territory shift visible. Fades out 2.5x faster than the
existing boundary flash line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix invisible delta gradient by using FullRect sprite mesh

The gradient sprite was using the default SpriteMeshType.Tight which
trimmed out the fully-transparent pixel, producing either an empty or
half-sized mesh. Use SpriteMeshType.FullRect with a 256x1 texture for a
cleaner gradient, and SetAsLastSibling so the overlay renders on top of
the attacker/defender bars. The boundary flash line also reasserts
last-sibling so it stays visible above the gradient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add diagnostic logging for delta gradient

Temporary logging to diagnose why the gradient overlay isn't visible
in-game. Will be removed after diagnosis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* DEBUG: render delta gradient as opaque magenta to test positioning

Temporarily replace the gradient sprite with plain opaque magenta and
add canvas renderer state logging to isolate whether the problem is
with the sprite/shader or with positioning/z-order/masking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* DEBUG: use attackerBar sprite, bright green, no fade

Set the delta gradient to use the attacker bar's known-working sprite,
force opaque bright green, and disable the fade coroutine so the image
stays permanently visible. Also log the full transform path to confirm
which thermometer instance is being modified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* DEBUG: test gradient sprite with permanent green tint

Swap back to the procedural 256x1 gradient sprite while keeping the
permanent no-fade green tint. This isolates whether the gradient sprite
itself renders correctly (gradient visible) or has a mesh/UV issue
(solid green or invisible).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use losing side's color for delta gradient, remove debug scaffolding

The previous implementation tinted the delta region with the *gaining*
side's color, but the bar underneath the delta region is already that
same color — so the gradient was nearly invisible (faction color fading
to a slightly lighter faction color).

Flip it around: use the *losing* side's color. The old owner's
territory briefly lingers in the lost region, then fades to reveal the
new owner's bar underneath. This is a more intuitive visualization of a
territory change.

Also flip the gradient direction so the opaque (losing-color) edge sits
at the old boundary (where the losing side's territory previously
ended), fading toward the new boundary.

Remove the debug logging, green color override, and disabled fade that
were added while diagnosing the earlier rendering issue, and re-enable
the DeltaGradientRoutine fade coroutine.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Flip delta gradient direction

The losing color should be on the side of the delta closer to the
losing side's remaining bar, so it visually points "home" to its own
side. The transparent edge reveals the new owner's bar underneath.

- Attacker gained → losing=defender → opaque on right (near defender).
- Defender gained → losing=attacker → opaque on left (near attacker).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Double the delta gradient fade duration

Previously 0.16s (flashDuration/2.5); now 0.32s (flashDuration/1.25)
so the gradient is easier to see.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 20:06:25 -07:00
3d169136a4 Fix orphaned backstory text ID for rubber-band spawned heroes (#6626)
RandomHeroGenerator.createLowPowerHero pre-populates backstoryVersions
with hero_${id}_backstory_0, but UnaffiliatedHeroAppearedAction only
emits a HeroInitialBackstoryRequest when backstoryVersions is empty. The
rubber-band spawn path in PerformUnaffiliatedHeroesAction was relying on
that action to emit the request, so the initial backstory text ID was
orphaned from the start — never registered with ClientTextStore, always
resolving to TextGenerationDependencyUnknown.

Clear backstoryVersions before handing the hero to
UnaffiliatedHeroAppearedAction so it creates a proper
hero_${id}_initial_backstory_$roundId text ID with a matching LLM
request. This restores effectiveBackstory's ability to fall back to the
initial backstory when later updates are legitimately skipped (e.g. for
heroes only visible to AI factions).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 19:41:49 -07:00
fae53369bf Switch default Gemini model to 3.1 Flash-Lite preview (#6625)
3.1 Flash-Lite is faster (throughput + TTFT), meaningfully smarter, and
still among the cheapest Gemini options, making it a strict upgrade
over 2.5 Flash-Lite for narrative text generation.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 19:00:51 -07:00
ae65205d34 Add temporary debug logging for free hero backstory visibility (#6623)
Logs a warning in GameController.withHandledEngineAndResults when a
free hero's most recent backstory is not visible to the province's
ruling faction. Includes game ID, history count, round, and phase
to help trace back to the action that caused the visibility gap.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 13:15:43 -07:00
d0c87c838e Add retry logic to all Addressable asset loading (#6622)
* Retry beast effect Addressable loads on failure

Beast effect prefabs are loaded from a remote CDN via Addressables.
If the download failed (network hiccup, CDN timeout), the effects
were permanently lost for the session with no retry. Now retries up
to 3 times with backoff delays (2s, 5s, 10s). Also releases failed
handles to avoid leaking resources.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Extract AddressableLoader utility with retry logic for all Addressable loads

All Addressable assets load from a remote CDN and a single transient
network failure permanently broke the affected feature for the session.
Extracts retry logic (3 retries with 2s/5s/10s backoff) into a shared
AddressableLoader.LoadWithRetry<T>() utility and updates all 6 callers:
ProvinceBeastsController, TacticalAssetLoader, SoundManager,
ImageForTerrainTracker, BattleInProgressController, ProvinceActionAnimator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 12:09:33 -07:00
6c01a81bbf Remove unused Modern UI Pack scripts, animations, and prefabs (#6621)
Removed ButtonManagerBasic and UIGradient components from the lobby
AvailableGame prefab (they were no-ops — the button already had a
normal onClick callback). Then deleted all non-texture content from
Modern UI Pack: Scripts, Animations, Editor, Fonts, Scenes, Unused
prefabs, and Documentation. Only Textures/ is retained.

Saves ~34 MB of unused assets.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 08:21:16 -07:00
c25ecadd60 Add editor tool to audit third-party asset usage (#6619)
* Add editor tool to audit third-party asset pack usage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Include Addressable group entries in asset usage audit

The previous version only traced dependencies from build scenes, completely
missing assets loaded via Addressables. This caused ~8.5 GB of assets to
be falsely reported as unused. Now traces dependencies from all Addressable
group entries (Beast Effects, Hex Tiles, Tactical Assets, Sound Effects)
in addition to build scenes.

Also logs all USED files per pack for easier analysis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove Pixel Fonts Megapack from audit pack list

Pack was deleted in PR #6620.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 08:02:12 -07:00
251aeb5794 Remove unused Pixel Fonts Megapack asset pack (#6620)
Asset usage audit (scenes + Addressables) confirmed 0 files from this
pack are referenced anywhere in the build. Saves 2 MB.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 08:00:43 -07:00
9223b0452e Clear fleeProvinceId after fleeing to prevent stale flee destination (#6618)
When an army flees after losing a battle, the flee province is consumed
but was not being cleared. This caused armies arriving at a now-hostile
flee province to get attack decision options instead of being shattered.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 07:23:19 -07:00
05299dbe77 Remove debug logs from battle progress UI (#6617)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 20:01:45 -07:00
f06ba27a6f Fix reversed thermometer bar widths in Shardok tactical view (#6616)
The Shardok Container prefab had defender on the left and attacker on
the right, but BattleThermometer code forces the defender bar to the
right and attacker bar to the left. This caused the wide bar to appear
next to the smaller number. Swap the field references so bars and labels
are on matching sides.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 19:54:19 -07:00
bf90c98add Update Unity prefabs for battle progress row and editor re-serialization (#6615)
Wire up flashDuration on BattleThermometer in the Battle Progress Row
prefab and right-align the attacker troop count label. Includes Unity
editor re-serialization of Emu and peasant prefabs.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 19:34:06 -07:00
95afb025e1 Replace inappropriate Excellent Glory headshot (#6614)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 19:31:26 -07:00
93e3804eef Only grant Allied hostility for actual alliances in battle views, not truces (#6613)
FactionUtils.hostilityStatus treats both truce and alliance as non-hostile,
which caused battle player infos to show Allied hostility for truce holders.
The client then displayed an "Observe" button that threw an exception on click
because the server correctly rejects observation for truce-only relationships.

Fix shardokBattlePlayerInfos to use hasAlliance directly instead of the
general hostilityStatus, so only actual alliances produce Allied hostility.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 19:27:32 -07:00
6c13ade07e Flash thermometer boundary on every day update (#6611)
Previously the flash only triggered when the ratio changed by more
than 0.001. Now it flashes on every update after the first, so each
day tick produces a visible flash even if the ratio barely moved.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 19:15:55 -07:00
dee2574a59 Fix losing armies being shattered instead of retreating to flee province (#6612)
ResolveBattleAction was discarding the original fleeProvinceId for
armies that lost a battle, causing them to shatter instead of retreating
to the province the player designated when dispatching the army.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 19:06:41 -07:00
8bb8a8f791 Only send BattleProgressResponse when gated round advances (#6610)
postBattleUpdate sent a BattleProgressResponse for every Shardok
action, even when the gated round hadn't changed. In AI-only battles
this floods clients with dozens of identical-day updates per round.
Now only sends battle progress when the gated round actually advances,
reducing redundant updates while still delivering Shardok action
results to participants.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 18:45:50 -07:00
51b95d0b62 Forward battle progress during strategic turn updates (#6609)
withHandledEngineAndResults used the 5-parameter overload of
humanClientsAfterPostingResults which did not send battle progress.
This meant non-combatant observers only received battle progress
updates when Shardok sent a GameUpdateResponse, not during Eagle
strategic turn resolution. Switch to the 7-parameter overload to
forward existing battle progress on every client update.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 18:45:30 -07:00
d06f26bffe Reset lastRevealedRound when all battles end (#6608)
The gated round (lastRevealedRound) used max(previous, current) to
prevent information leakage, but never reset when all battles ended.
This caused new battles to start at the previous battle's final day
(e.g. "Day 29") instead of "Day 0".

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 18:28:19 -07:00
7375d9f8ee Reset BattleDay to 0 when a new battle starts (#6607)
BattleDay was only set from BattleProgressResponse, so it retained
stale values from previous battles until the first progress update
arrived for the new battle.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 18:22:27 -07:00
0a650dec2c Add flash effect at thermometer boundary when balance shifts (#6606)
A thin white line briefly flashes at the attacker/defender boundary
whenever the troop ratio changes, giving visual feedback during battle.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 16:46:20 -07:00
22cdf99d4d Propagate isDefender flag to Shardok thermometer (#6604)
* Use isDefender flag in Shardok thermometer and game model

Use the new is_defender field from ShardokBattlePlayerInfo instead of
assuming player 0 is always the defender. Updates ShardokGameController
thermometer, ShardokGameModel.IsDefender, EagleGameModel.MakeGameModel,
and CustomBattleHandler player setup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update Gameplay scene layout and soldier prefabs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 15:41:38 -07:00
86e9489f21 Add is_defender to ShardokBattlePlayerInfo proto (#6605)
Add is_defender field (field 5) to ShardokBattlePlayerInfo so the
client can identify which players are defenders without assuming
player 0 is always the defender. Populate it from
ShardokPlayer.isDefender in BattleFilter and serialize it in
ShardokBattleViewConverter.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 15:20:47 -07:00
2339050154 Fix battle progress bar: swap sides and forward updates to client (#6601)
Two fixes for the battle progress thermometer:

1. Swap attacker/defender sides so attacker fills from left and defender
   from right, matching the expected layout.

2. Add missing BattleProgressResponse case in PersistentClientConnection's
   HandleGameUpdate switch. The server was sending progress updates but
   the client silently dropped them, causing the bar to stay at 50%.

Also extract UpdateBattleProgressDisplay helper in EagleGameController
and call it from SwapModel's early-return path so progress updates aren't
skipped when the player has active strategic commands.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 14:51:13 -07:00
f7fefe078e Fix crash in AttackDecisionCommandSelector when configuring toggles (#6602)
ConfigureToggle used toggle.isOn = false which fires the ToggleChanged
callback. That callback accesses SelectedDecision, which throws when no
toggle is on yet during SetUpUI. Use SetIsOnWithoutNotify instead.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 14:50:49 -07:00
ad3e7f19b9 Fix reversed troop counts in battle progress tracker (#6603)
The updatedBattleProgress method assumed player 0 was always the
defender, but RequestBattlesAction builds the players vector with
attackers first and defender last. Use the isDefender flag from the
battle's player list to correctly classify troop counts.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 14:45:25 -07:00
022bdab53f Fix ToggleGroup preventing disabled toggles from turning off (#6600)
When ConfigureToggle sets isOn=false on a toggle that is the last active
member of a ToggleGroup with allowSwitchOff=false, Unity forces it back
to true. The toggle is then removed from the group but retains the stale
isOn=true state. This causes the wrong option to be sent to the server
(e.g. Recruit when only Imprison/Return/Execute were available).

Fix by removing the toggle from its group before setting isOn=false.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 10:27:41 -07:00
c01e64265d Seed initial troop counts in BattleFilter from army battalion data (#6598)
When a battle is first created, participants now see troop counts and an
accurate balance bar immediately instead of a blank 50/50 bar while
waiting for the first Shardok GameUpdateResponse.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 07:25:17 -07:00
a7572a5a9a Use faction-specific colors in battle thermometer (#6597)
- BattleThermometer: accept IList<Color> for attacker colors, dynamically
  create extra bar segments for multi-attacker battles
- BattleProgressRowController: look up defender faction from province
  RulingFactionId and attacker factions from PlayerInfos, using
  LightPlayerColor for faction-correct colors; use DarkColoredProvinceName
  for more readable province name text
- ShardokGameController: wrap single attacker color in list for new API

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 07:03:45 -07:00
44edd8cc5e Clear hex overlays on every model update and turn end (#6596)
ModelUpdated() never unconditionally cleared overlays, so stale highlights
persisted after phase transitions (setup -> running) and after issuing a
command that ends the turn. Add ClearOverlays() at the top of ModelUpdated()
so overlays are always wiped before being selectively re-drawn, and in
EndTurn() for immediate visual feedback when clicking Commit/End Turn.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 22:03:13 -07:00
3a1f1c2bbe Add battle progress display for non-combatant observers (#6581)
* Add battle progress tracking and real-time progress updates

During BATTLE_RESOLUTION phase, track Shardok battle progress (troop counts
and defender/attacker ratios) and push synchronized updates to clients via
a new BattleProgressResponse message. All battles advance together using
gated round progression to prevent information leakage. Allied observers
see troop counts; non-allied players see only the ratio bar.

Key changes:
- Add defender_ratio, troop counts, and winning_faction_id to ShardokBattleView proto
- Add updated_battles to GameStateViewDiff proto
- Add BattleProgressResponse to GameUpdate oneof
- Track battle progress in GameController with synchronized round gating
- Send enriched battle views to clients via HumanPlayerClientConnectionState
- Update GameStateViewDiffer to detect battle updates
- Add GameStateViewDifferTest with 6 test cases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Move BattleProgressResponse to views layer to fix proto layering

BattleProgressResponse only contains ShardokBattleView objects, which
belong to the views layer. Move it from eagle.proto (api layer) to
game_state_view.proto (views layer) and remove the direct
shardok_battle_view.proto import from eagle.proto.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add battle progress display for non-combatant observers

During BATTLE_RESOLUTION, non-combatant players now see real-time
progress bars instead of a single "Observe Battle" button. Each bar
shows defender/attacker balance with green/red coloring. Allied battles
show troop counts and an Observe button; non-allied battles show only
the bar. When a battle ends, the bar is replaced with Victory/Defeat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix TextAlignmentOptions.MidlineCenter compile error and meta GUID conflict

MidlineCenter does not exist in Unity 6 TMP; use Center instead.
Also include reassigned .meta GUID to resolve conflict with duel.asset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add BattleProgressPanel GameObject to Gameplay scene

Wire the panel to EagleGameController.battleProgressPanel with
VerticalLayoutGroup and ContentSizeFitter components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove BattleInProgressController from BattleProgressPanel GameObject

BattleInProgressController belongs on the map, not the progress panel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused ShardokGameStateView import from GameController

The import triggered -Werror and broke the battle_progress_test build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Show BattleProgressPanel for all battles, not just observed ones

The panel was only activated for non-combatant observers. Now it shows
whenever there are battles, alongside the "Battle!" button for combatants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use ShardokBattles instead of RunningShardokGameModels for panel visibility

RunningShardokGameModels requires the client to have connected to the
Shardok game and received a GameRunning status, which may not have
happened yet. ShardokBattles comes from the server game state view and
is available immediately when battles exist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Refactor BattleProgressPanel to use prefab rows and display battle day

Replace programmatic CreateRow()/PopulateRow() with a prefab-based
approach using BattleProgressRowController. The panel now instantiates
rows from a prefab into a scroll view, displays "Day X" from the
server's gated round, and uses colored province names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Use BattleThermometer in battle progress rows

Replace manual anchor/color logic with the existing BattleThermometer
component, which already handles both troop-count and ratio-only modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix row container cleanup, observe button visibility, and prefab transforms

Clear stale children from rowContainer on first update. Only show
Observe button for allied battles (not the player's own). Fix bar
rotation/scale in row prefab. Add debug logging for battle progress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update BattleProgressPanel layout in Gameplay scene

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Replace goToBattleButton with per-row Fight!/Waiting.../Observe buttons

Each battle row now has its own action button: "Fight!" for the
fightable battle, "Waiting..." (grayed) for other own battles,
and "Observe" for allied battles. The global goToBattleButton is removed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove goToBattleButton from scene and adjust battle panel layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Adjust battle progress panel layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 21:31:16 -07:00
0f3ffc24c2 Send gated round as current_day in BattleProgressResponse (#6595)
The server already computes lastRevealedRound (the minimum round across
all active battles) for fair reveal timing, but never sent it to the
client. Add current_day field to the proto and pass gatedRound through
sendBattleProgress so clients can display the battle day.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:56:53 -07:00
741efecc66 Fix AnimalEffect spamming "State could not be found" errors (#6594)
Guard idle and walk Play() calls with HasState(), matching the existing
pattern already used for eat/attack transitions.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:10:55 -07:00
4c7ac587e7 Add Scala battle progress tracking for non-combatant observers (#6585)
* Add battle progress tracking with proto-based troop counts

Adds real-time battle progress tracking so non-combatant observers can
see defender/attacker troop ratios during BATTLE_RESOLUTION phase.

C++ Shardok extracts current_round and per-player troop counts from the
FlatBuffer game state and sends them as new proto fields on
GameUpdateResponse, avoiding the previous approach of parsing opaque
FlatBuffer bytes as Protobuf on the Scala side.

Scala Eagle tracks progress via BattleProgressTracker, gates round
reveals across concurrent battles, and enriches ShardokBattleView with
defender_ratio and troop counts for allied observers.

Includes tests for both C++ troop count extraction and Scala progress
tracking logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove default values from ShardokBattleView fields

Make all new fields (defenderRatio, defenderTroopCount,
attackerTroopCount, winningFactionId) required at construction sites
instead of relying on defaults.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Preserve battleProgress across GameController operations

Convert withHumanClient, withAiClient, stopStreaming, aiClientCommands,
and withHandledEngineAndResults from explicit GameController construction
to copy(), so battleProgress and lastRevealedRound are preserved when
clients connect/disconnect or commands are processed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 20:10:13 -07:00
3c18918634 Extract battle progress data from FlatBuffer game state in Shardok (#6593)
Populates the new proto fields (current_round, player_troop_counts) on
GameUpdateResponse by extracting data from the FlatBuffer game state in
ShardokGameController::GetUpdates(). Counts troops for each player
across NORMAL, RESERVE, and NEVER_ENTERED units.

Updates the OnUpdates callback signature to pass the new fields through
EagleInterfaceGrpcServer to Eagle.

Includes tests verifying troop count extraction for both normal games
and games with reserved slots.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:52:23 -07:00
b930ca626f Add proto fields for battle progress tracking (#6592)
Adds new proto fields to support real-time battle progress updates for
non-combatant observers:

- shardok_internal_interface: PlayerTroopCount message, current_round
  and player_troop_counts on GameUpdateResponse
- shardok_battle_view: defender_ratio, troop counts for allied viewers,
  winning_faction_id
- game_state_view: updated_battles on GameStateViewDiff,
  BattleProgressResponse message
- eagle.proto: battle_progress_response in GameUpdate oneof
- BUILD.bazel: game_state_view_scala_proto target

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:45:11 -07:00
d1219787a8 Add weekly S3 archive cleanup workflow (#6591)
Deletes game folders from eagle/archived/ where directory.e0i is over
1 month old. Runs weekly on Sundays at 05:00 UTC with manual trigger support.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:39:59 -07:00
8019986ea7 Populate "Your Army" table in Battle Aftermath panel (#6590)
The surviving units data was being sent by the server but never displayed.
Wire the EventBasedTable to show heroes and battalions using the same
CombatUnitRowController pattern as AttackDecisionCommandSelector.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:30:06 -07:00
62b7e3abe1 Fix "What's New" showing same entries on every sign-in (#6588)
MarkAllAsSeen() assumed entries[0] had the latest date, but entries
in whats-new.json aren't necessarily in chronological order. When a
backdated entry was added at position 0, the saved last-seen date was
older than other entries, causing them to reappear on every sign-in.

Now scans all entries to find the maximum date.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:09:21 -07:00
88b049334b Archive S3 game files on game removal (#6589)
When a game is archived (last player exits) or deleted (admin console),
S3 files under eagle/save/{gameId}/ are now moved to eagle/archived/{gameId}/
to prevent the lazy-loader from resurrecting stale data and to keep S3 clean.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:07:21 -07:00
0aaa726abb Preserve schemaVersion when saving loaded games (#6586)
* Preserve schemaVersion when saving loaded games to games.e0es

GamesManager.save() was building RunningGame entries for loaded games
without setting schemaVersion, causing it to default to 0. This made
every game appear as needing migration on every server restart after
being loaded into memory, even when the schema version hadn't changed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Also stamp currentSchemaVersion on newly created games

Pass GameMigrator.currentVersion into GamesManager so new games get the
correct schema version on their first save, avoiding a needless no-op
migration on the next server restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 18:57:33 -07:00