Commit Graph
10748 Commits
Author SHA1 Message Date
adminandClaude Opus 4.6 228101b937 Post Discord bug reports as plain text with log attachment
Discord-bound bug reports now put the description and metadata in the
message content and attach the full recent logs as recent_logs.txt,
instead of shipping everything inside a webhook embed. Webhook-embed
bodies aren't surfaced by the Claude Code Discord plugin's
fetch_messages/push path, so reports couldn't be auto-processed;
plain-text content and a file attachment both flow through cleanly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:36:30 -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