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>
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>
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>
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>
* 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>
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>
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>
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>
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>
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>
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>
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>
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>
* 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>
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>
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>
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>
* 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>
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>
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>
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>
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>
* 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.
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.
* 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>
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>
* 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>
* 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>
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>
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>
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>
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>
* 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>
* 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>
* 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>
* 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>
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>
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>
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>
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>
* 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>
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>
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>
* 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>
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>
* 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>
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>
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>
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>
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>
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>
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>
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>
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>
* 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>
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>
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>
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>
* 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>
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>
* 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
* 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>
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>
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>
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>
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>
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>
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>
- 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>
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>
* 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>
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>
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>
* 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>
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>
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>
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>
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>
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>
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>
* 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>
delete() and deleteAll() only removed from the first (local) persister,
leaving stale files in S3. This caused rewound shardok battles to be
resurrected via lazy-loading from S3.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix battle progress parsing FlatBuffer bytes as Protobuf
The updatedBattleProgress method was calling ShardokGameStateView.parseFrom()
on opaque FlatBuffer bytes, causing InvalidProtocolBufferException in production.
Fix: Add current_round and player_troop_counts fields to GameUpdateResponse
proto so Shardok serializes the data explicitly. Eagle reads from these new
proto fields instead of trying to parse the opaque game state bytes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add tests for battle progress proto fields
C++ test verifies troop count extraction from FlatBuffer game state
matches GetUpdates() logic. Scala test covers updatedBattleProgress()
for tracker creation, same-round updates, round boundary snapshots,
multi-attacker summation, and missing defender defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The Sparkle-2.6.4.tar.xz extracts to ./bin/sign_update, not
./Sparkle-2.6.4/bin/sign_update. This broke mac builds when the
runner had a fresh /tmp/sparkle-cache.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
new List<ProvinceId>(heroInfo.provinceId) calls the capacity constructor,
creating an empty list with capacity = provinceId. Changed to collection
initializer syntax { heroInfo.provinceId } so the province ID is actually
added as an element. The PopupPanelController already feeds this list into
mapController.OverrideTargetedProvinces for highlighting, but the list was
always empty.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Include game ID, command type, and province/player info in the error
log line so failed commands are diagnosable without reproducing.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
SoldierEffect.prefab referenced 3 of 4 character models as raw .fbx files
(PT_Male_Soldier_01.fbx, PT_Female_Soldier_01.fbx, PT_Female_Soldier_02.fbx).
Raw FBX instantiation doesn't carry material assignments, causing models to
render with the default white material. Updated references to use the proper
.prefab files from Polytope Studio which have materials correctly configured.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
All prefab override fields are null at runtime on Windows builds only.
Log the missing fields and gracefully bail out of ApplyPersistedSettings,
Start, and HandleEscape so the game can still launch and we can
investigate whether other prefab overrides are also broken.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GitHub Actions retention-days: 3 is unreliable — 1,846 expired artifacts
(282 MB) accumulated over 5+ weeks. Add a cleanup-expired job that deletes
artifacts older than 3 days before the storage check runs.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Suppress harmless stream-closed IllegalStateException in SyncResponseObserver
The most common Sentry exception was "Stream is already completed" from
CustomBattleManager. This happens when Shardok sends battle results to a
client that already disconnected — the isCancelled check passes because
"cancelled" and "completed" are different stream states, so the underlying
onNext throws IllegalStateException. Catch it silently in all three stream
methods, matching the existing pattern in GameController.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Narrow catch to only suppress "already completed" stream exceptions
Only catch IllegalStateException when the message contains "already completed",
so unexpected IllegalStateExceptions still propagate to Sentry.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The wind-assisted range-3 check was allowing tiles in adjacent hex
directions (±60° from wind), producing a 120° cone. This let longbows
shoot northwest with a northeast wind. Tighten the check so only tiles
whose direction matches the wind direction (or sits on the boundary
between two sectors, one of which is the wind direction) qualify.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The control command's CommandDescriptor has target_unit but no target
coords, so the client treated it as fully untargeted and animated toward
the nearest enemy instead of the controlled undead unit. Now
PlayUntargetedCommandAnimation uses the command's target_unit when
available. Also set target_unit on the CONTROLLED action result so
server-side history replays animate correctly.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The ProvinceBeastsController could attempt to spawn beast effects before
centroids arrived from CDN, causing "No centroid found" warnings. The
effect prefabs (loaded from local Addressables) often finished loading
before the centroids (fetched over the network), creating a window where
UpdateBeastsEffects would proceed without centroid data.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Guard AnimalEffect animator state plays with HasState check
Some animal prefab animator controllers don't have states named idle,
walk, eat, or attack, causing spammy "State could not be found" and
"Invalid Layer Index '-1'" errors every frame. Added SafePlay helper
that checks HasState before calling Play.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix AnimalEffect animator state errors for kangaroo, leopard, black panther
Three animator controllers (KangarooMapAnims, LeopardMapAnims,
BlackPantherMapAnims) used "idle1" instead of "idle", causing
"State could not be found" errors every frame. Renamed to "idle".
Also skip eat/attack actions for animals whose controllers don't have
those states (e.g. scorpion has no eat) — falls back to idle instead
of playing a missing state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
On reconnect, HandleStartingState clears all ShardokGameModels and
re-creates them, then the server replays the full battle history. Each
ChangedReserveUnit in the replayed history was triggering auto-placement
to starting positions, causing visible UI thrashing — especially with
repeated reconnect attempts during idle periods. The user's manual
placements (local-only until submitted) were lost each time.
Added SkipAutoPlacement flag to ShardokGameModel, set during resync
model creation and cleared after the first successful HandleUpdates.
This prevents auto-placement during history replay while preserving
it for genuinely new battles.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The 4 decision toggles (Advance, Demand Tribute, Withdraw, Safe Passage)
were not wired to ToggleChanged in the prefab, so selecting Demand Tribute
never activated the tribute container with gold/food sliders. Also changed
ConfigureToggle to use SetIsOnWithoutNotify when disabling unavailable
toggles, preventing a "No decision selected" exception during setup.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Documents phased approach for migrating from Built-In Render Pipeline
to Universal Render Pipeline before Unity drops BiRP support after 6.7.
Covers shader inventory, long-lived branch strategy, and effort estimates.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Unaffiliated hero backstory updates were only visible to the current
province owner, so when another faction conquered and tried to recruit,
the backstory was missing. Now unaffiliated hero backstory updates use
empty recipientFactionIds (meaning all factions), matching the initial
backstory behavior.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Upgrade Unity from 6000.3.8f1 to 6000.4.0f1
Also clear the CI runner's Library/ cache when the Unity version
changes to prevent infinite import loops on version upgrades.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix em dash characters in workflow YAML breaking GitHub Actions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix gray textures after Unity 6.4 upgrade by switching to embedded materials
materialLocation: 0 (External) is deprecated in Unity 6.4 and causes FBX
models to render without textures. Changed all 749 .fbx.meta files to use
materialLocation: 1 (Embedded) which is the new default.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Trigger CI checks
* Add trailing newline to workflow file to re-trigger CI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix YAML syntax error on line 125 of unity_build.yml
The run: value contained | and > characters that confused the YAML parser.
Switching to block scalar syntax (run: |) avoids the ambiguity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remap bridge materials to fix gray textures after materialLocation change
With materialLocation: 1 (Embedded), Unity ignores external .mat files.
Added externalObjects remapping so the bridge FBX models still use the
correctly-textured bridges_d material from TileableBridgePack/Materials/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add material remapping to all remaining bridge FBX meta files
The old-format meta files (serializedVersion: 18) had no materialLocation
or externalObjects fields, causing Unity 6.4 to default to the deprecated
External material path and render bridges gray. Added externalObjects
remapping and materialLocation: 1 to all 47 remaining bridge models.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Use UISprite with Mask component on the Bar Area container to clip
the colored bars with smooth rounded corners.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add battle thermometer to Shardok tactical view
Replace the always-visible armies table with a horizontal thermometer bar
showing defender vs attacker troop balance at a glance. The bar uses
Fantasy RPG boss gauge sprites with faction-matched colors. Hovering the
thermometer reveals the full armies table as a popup.
Also adds icons to Army Info Row and removes the totals row from the
armies table since troop counts are now shown on the thermometer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix thermometer bar sizing and null model crash
Use anchor-based horizontal sizing instead of fillAmount, preserving
vertical inspector settings. Replace BarMask with plain Bar Area
container. Guard UpdateReserves against null Model when Shardok
container is active at game launch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
UpdateNotificationManager lives in Gameplay.unity which is the
persistent base scene — it is never unloaded. DontDestroyOnLoad is
unnecessary and produces a warning because the object is a non-root
UI child under a Canvas. Remove the call entirely.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Remove happy-path confirmation logs that fire on every normal startup
and provide no diagnostic value: tutorial UI built, state loaded, token
cache initialized, Sparkle version/platform info, auth configured,
session restored, and WhatsNew check results.
Error/warning logs (LogWarning, LogError) are preserved.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
FearAnimator maintained its own _activeObjects list, Track(), Untrack(),
and OnDestroy() that duplicated base class TrackedEffectAnimator's
tracking. Since all objects were created via CreateTrackedObject() (which
adds to the base's _trackedEffects), but Track() was never called,
_activeObjects was always empty — making FearAnimator's Untrack() calls
no-ops and its OnDestroy() cleanup redundant.
Remove the dead code and let the base class handle all tracking.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The tutorial battle is too hard for new players. Buff the defender's
starting longbowmen and two reinforcement battalions by ~20% to make
the battle easier, staying within battalion type capacity limits.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Handle ShardokBattleResetResponse in Unity client
Route the battle reset message through PersistentClientConnection and
EagleGameModel to the ShardokGameModel, which clears its mutable state
and signals the controller. The controller clears overlays, labels, and
the game-over UI so the restarted battle rebuilds normally from new
setup results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add Old Marek dialogue on battle reset
When a tutorial battle resets, Old Marek comments on a strange feeling
of deja vu and encourages the player to try a different approach.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wait for Eagle confirmation before allowing battle exit
Previously, the Shardok controller showed "Exit Battle" as soon as the
game reached Victory state and immediately cleaned up the model. This
meant a subsequent ShardokBattleResetResponse had nothing to reset.
Now the model stays alive after Victory, and "Exit Battle" only appears
once Eagle confirms removal via RemovedBattleIds. If a battle reset
arrives instead, the model is still intact for ResetForNewBattle().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix GetPlayerName crash with negative playerId after battle reset
ResetForNewBattle sets CurrentPlayer to -1, which passed the
"playerId < players.Count" check but threw on list access.
Add a lower bounds check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Reset battle tutorials on battle reset so they re-fire on retry
Adds ResetBattleTriggerFlags() for combat-only trigger flags and
ResetCompletedScriptsById() for selective dialogue reset. Strategic
tutorial progress is preserved.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Add receiveBattleReset to BattleUpdateReceiver trait
- Handle BattleResetResponse in ShardokInterfaceGrpcClient streaming
- Implement receiveBattleReset in GamesManager (delegates to controller)
- Add no-op implementation in CustomBattleManager
- Add postBattleReset to GameController: resets shardok history and
notifies human clients via ShardokBattleResetResponse
- Add sendBattleReset to HumanPlayerClientConnectionState
- Add withResetShardokResults to FullGameHistory/PersistedHistory/InMemoryHistory
- Add shardok_battle_scala_proto dependency to BUILD.bazel
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Add IsTutorialBattleEnabled() and DidDefenderLose() to ShardokEngine
- Add WaitResult enum (GAME_OVER, DISCONNECTED, BATTLE_RESET) replacing
bool return from WaitForUpdatesAndPush
- Add OnBattleReset() to StreamSubscriber interface
- Detect tutorial loss in WaitForUpdatesAndPush and return BATTLE_RESET
- In SubscribeToGame, loop on BATTLE_RESET: tear down the old game,
restart from serialized NewGameRequest, and stream fresh state
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix settings panel not showing on first Escape press
SettingsPanelController lives on an inactive panel prefab, so Start()
only runs the first time the panel is activated. When ToggleActive()
activated the panel, Start() would immediately set _active=false and
hide it again, swallowing the first Escape press. The slider
initialization in Start() also triggered volume change callbacks,
causing an audible volume shift.
Sync _active with the panel's current state in Start() instead of
forcing it to false.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove defensive null checks on Inspector fields in SettingsPanelController
Per project convention, Inspector fields should throw NullReferenceException
if not linked rather than silently skipping code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove tileBorderWidth serialized fields from Settings Panel prefab
These fields were removed from SettingsPanelController.cs; clean up the
prefab to avoid Unity serialization warnings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Shardok battle race condition — hex tiles sometimes never render
ImageForTerrainTracker and TacticalAssetLoader were plain C# singletons
whose LoadAsync() coroutines ran on ShardokGameController (which gets
deactivated). ConnectionHandler.Start() deactivates the shardokContainer,
killing any in-flight coroutines. The singletons' _loadStarted guard
then prevented reloading, leaving IsLoaded permanently false and
SetUpGameAfterLoad() spinning forever at its while-loop.
Convert both to MonoBehaviour singletons meant to live on a persistent
GameObject (Main Camera). They now own their own Awake() → LoadAsync()
lifecycle and are immune to container deactivation. Remove the redundant
StartCoroutine kicks from ShardokGameController and EagleGameController.
Requires adding ImageForTerrainTracker and TacticalAssetLoader components
to Main Camera in Gameplay.unity (scene change not included in this
commit).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add ImageForTerrainTracker and TacticalAssetLoader to Main Camera
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
xcodebuild archive can reset the user keychain search list during long
builds, dropping the temporary CI keychain that holds the Apple
Distribution signing cert. Re-add and unlock the keychain right before
the export step so xcodebuild -exportArchive can find the certificate.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Gitea's mirror-sync API is async — the GitHub webhook triggers it but
returns 200 before the sync completes. CI runners can start git lfs pull
before new LFS objects are available on Gitea. Extract LFS fetch into a
shared script with retry logic (5 attempts, 15s backoff) to bridge the
gap.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
RemoveController was using std::shared_lock (read lock) while calling
erase() on the runningControllers map. This is undefined behavior
since other threads could be concurrently reading the map. Use
std::unique_lock (write lock) instead.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add hippogryph beast effect using DragonEffect flight system
Reuses DragonEffect.cs (flight/ground state machine) with a custom
HippogryphMapAnims controller mapping 15 animation states to the
PROTOFACTOR Hippogriff asset. Includes PBR materials, 12 animation
FBXes, and tuned flight parameters (scale 8, fly height 12).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Reduce hippogriff texture maxTextureSize to 512
The Addressable bundle build was failing during ArchiveAndCompress
because the hippogriff textures were imported at 4096x4096 (default
from the asset pack). For a map-view beast effect, 512 is plenty.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix texture .meta files stored as LFS pointers instead of YAML
The broad directory-level LFS pattern in .gitattributes was catching
.meta text files. Replace with a global *.jpg pattern (other binary
formats already have global patterns) and re-add the 8 texture .meta
files as regular blobs so Unity can read them on CI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add BattleResetResponse proto messages for tutorial battle reset
Add BattleResetResponse to the internal Shardok interface (oneof case 4
in GameStatusResponse) and ShardokBattleResetResponse to the client-facing
Eagle API (oneof case 8 in GameUpdate). These signal that a tutorial battle
should reset after the defender loses.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add no-op handler for BattleResetResponse in exhaustive match
Prevents -Werror build failure from non-exhaustive pattern match
after adding the new oneof case. Full handling in follow-up PR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Each xcodebuild archive creates a new ~3.6GB Unity-iPhone-<random>
folder in DerivedData that was never cleaned up, accumulating on
the build runner.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add black panther, rhinoceros, gorilla, hyena, leopard, warthog, emu,
kangaroo, and tasmanian devil to the custom animations table. Remove
them from the "not yet set up" section and note which remaining pack
animals are not in beasts.tsv.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Engineers standing on a bridge tile can attempt to destroy it. The
player selects an adjacent traversable, unoccupied tile as the
destination. A D100 success roll determines if the bridge is destroyed.
Whether the roll succeeds or fails, the engineer moves to the selected
tile. Bridges are no longer valid Reduce targets.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add Unity client support for Blow Bridge command
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Improve Blow Bridge UX: icon, sound, animation position and rendering
- Change command icon from catapult (reduce.png) to manabomb
(Engineering_25_manabomb.png)
- Use explosion impact sound (Fire Spelll 02) for attempt, structural
breakage sound for success, failure horn for failure
- Fix animation position: use sourceGridIndex (bridge hex) instead of
targetGridIndex (engineer's destination) since the engineer starts ON
the bridge and jumps to an adjacent hex
- Preserve cached source position for BlowBridge results so the server's
post-move actor location doesn't override the bridge hex
- Replace hammer/axe attempt animation with debris explosion
(AnimateRepairFailed) matching the destructive nature of the action
- No visual animation on failure, just the negative sound
- Add zPosition parameter to AnimateRepairFailed so BlowBridge debris
renders in front of the bridge 3D object (at Z=-5)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When the AI thread throws an exception, instead of re-throwing (which
calls std::terminate and silently kills the thread), store the error
message, set an atomic flag, and wake up waiting subscribers. The gRPC
stream subscriber detects the failure and returns INTERNAL status to
Eagle, which triggers Sentry notification. The error message includes
game ID, player ID, and round number for easier debugging.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
ToPlayerInfoProto() was not copying the cannot_become_outlaw flag
when converting flatbuffer PlayerInfo to proto for the AI's
GameStateView. This caused the AI to always think outlaw was
available, generating extra BECOME_OUTLAW_COMMAND entries and
triggering a command count mismatch crash.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Each beast has TSV stats, FBX animation converted to Generic, animator
controller with idle/walk/eat/attack states, effect prefab with tuned
spawn parameters, and Addressables registration. Models sourced from
Africa Animals Pack V1 (black panther, rhinoceros), Africa Animals Pack
Low Poly V2 (gorilla, hyena, leopard, warthog), and Australia Animals
Pack V1 (emu, tasmanian devil, kangaroo).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add lion and velociraptor beast effects
Add animated lion (male + lioness) and velociraptor province beast effects
with Generic animation controllers. Includes only the specific model files
needed from Africa Animals Pack V1 and Dino Pack Low Poly V1.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix lioness animation with per-prefab animator controller
The lioness wasn't animating because the shared LionMapAnims controller
references clips from the Lion FBX, whose bone paths (LionSkeleton/...)
don't match the Lioness model (LionessSkeleton/...). Create a separate
LionessMapAnims controller referencing Lioness FBX clips and use the
per-prefab animatorControllers array to assign each model its own
controller. Also add the standalone Lion/Lioness prefabs that LionEffect
references.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add BLOW_BRIDGE_COMMAND (38), BLOW_BRIDGE (80), and
BLOW_BRIDGE_FAILED (81) enum values for the upcoming
Blow Bridge engineer command.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Import the Africa Animals Pack and wire up hippopotamus as an AnimalEffect
beast. Switch the hippo FBX import to Generic animation type for Mecanim
compatibility and enable loopTime on looping clips. AnimalEffect now strips
legacy Animation, Rigidbody, and Collider components and adds an Animator
when prefabs lack one, supporting asset packs that ship with legacy setups.
Document the per-animal setup workflow for future Africa Animals Pack imports.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add animated windsock wind direction & speed indicator to Shardok battle view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Redesign wind indicator as animated arrow, remove weather from round info
Replace windsock (pole + sock) with a simpler arrow that points in the
wind direction, shakes with layered sine waves for organic feel, and
grows thicker at higher speeds. Fix NW/SW direction mapping swap. Remove
weather text from round info label since visual indicators now convey
this. Show "No Wind" in speed label when calm.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Remove GraphicRaycaster from Weather Canvas (no interactive elements)
- Set raycastTarget=false on Odds Panel and Button Tooltip images
(display-only elements that don't need click handling)
- Use Color.clear instead of null when clearing bg highlights in HexGrid
to preserve mesh fill triangles
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Use ID-based lookup instead of object equality when diffing unaffiliated
heroes. The old code used Vector.contains (full object equality), so any
field change (profession, recruitmentInfo, etc.) caused the hero to
appear in both removed and added lists. If ordering or batching led to
the add arriving before the remove was processed, the client threw
"Duplicate unaffiliated hero" InvalidOperationException.
Now builds a Map by heroId for comparison, which correctly handles all
three cases: truly removed, truly added, and changed-in-place (emitted
as remove+add by ID so the client replaces the old entry).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
BattleFilter was setting shardokGameId to None for restricted
non-participant observers. This broke GameStateViewDiffer's
removedBattleIds computation, which uses flatMap(_.shardokGameId)
and silently drops None entries. The result: battle animations
were added to the client but could never be removed.
myPlayerId = None already prevents non-participants from joining
the Shardok game, so hiding the game ID served no purpose and
broke battle removal tracking.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a Shardok game ends (victory/defeat), the game model was
removed from ShardokGameModels but the battle was left in
ShardokBattles (OutstandingBattles). The only other removal path
was RemovedBattleIds in Eagle diffs, which may arrive late or not
at all for auto-resolved battles the player never entered.
This left the battle animation running on the strategic map
indefinitely after the battle ended.
Fix: also remove from ShardokBattles when processing ended games
in the Shardok update path.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
SwapModel returns early when both old and new model have available
commands, skipping mapController.Model assignment and all province
effect updates. This meant battle animations persisted after a
battle ended if commands were available at the time.
Fix: call mapController.UpdateBattleEffects before the early return
so battle start/end animations are always kept in sync with the
model, even when the full model swap is skipped.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a province changed hands after a battle, UpdateBattleEffects
identified the defender by looking up the current ruling faction of
the defender province. After conquest, the attacker's faction matched
the province owner and was skipped, while the old defender was
processed — with OriginProvinceIds containing the defender province
itself, producing key (Wichel, Wichel) and an animation at the
province center.
Fix: skip any origin that equals the defender province. This can
never produce a meaningful animation (midpoint = province center)
and eliminates the dependency on current province ownership for
defender identification.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The failure check treated "no longer allied" as a failure condition, but
that's the success condition. Now: failure only if the target faction no
longer exists. Also added a general fulfillment check so the quest succeeds
even if the inline check in BreakAllianceResolutionHelpers doesn't fire
(e.g. when the ambassador is imprisoned).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Non-involved players can no longer observe tactical Shardok combat. They
still see the battle-in-progress animation on the strategic map and learn
the outcome, but won't receive individual Shardok actions or be offered
the observation UI. Allied non-participants can still observe.
Controlled by the RestrictBattleObservation setting (default 1/on). Set
to 0 in the admin console to revert to the old behavior.
Also syncs settings.tsv with LLM settings that were previously only in
BUILD.bazel, and fixes the settings generator to include string_setting
dep and correct battalion_type_loader deps.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The settings generator had two issues:
- Missing string_setting dep when String-type settings exist
- Wrong battalion_type_loader deps (proto target instead of model state)
Also syncs settings.tsv with LLM settings (llmProvider, openAiModelName,
claudeModelName, geminiModelName) that were previously only manually
added to BUILD.bazel, and updates the openai_model_name target to match
the generator's snake_case convention.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Set cannotBecomeOutlaw for tutorial opening battle
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix: only set cannotBecomeOutlaw for the first battle, not all tutorial battles
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Instead of fixed directions (March slides right, Send Supplies launches
upward), both animations now compute the direction to the destination
province from the game state diff and animate toward it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add -500 trust toward the King (faction 2) for both Bridget (faction 4)
and Hedrick (faction 5) so the King's AI never sends them invitations.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add cannot_become_outlaw flag to per-faction battle request
When set on a faction's PlayerSetupInfo, the BecomeOutlaw command
is suppressed for that faction's units. Default false preserves
existing behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove default values for cannotBecomeOutlaw, require explicit specification
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tutorial Mobilization phase start logic (#6478)
When starting a tutorial at Mobilization or EagleAppears, fast-forward
the game state past the opening battle to a plausible mid-game state
where Sadar controls two provinces and King's satellites are vacated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Replace dynamic tutorial phase setup with JSON resource loading
Add an extractor tool and loader to store pre-computed tutorial phase
transition results as JSON resources instead of computing them at
runtime from game state. This eliminates fragile dependencies on
knowing exact hero IDs and post-battle state deltas.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Populate expansion_results.json with battle resolution data
Extracted from a tutorial playthrough: actions 4-7 covering battle
resolution, province held, Tarn disappearance, and end of resolution
phase. Actions 0-3 (GAME_START through FOOD_CONSUMED) are already
produced by the initial game creation flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Pretty-print expansion_results.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Increase Sadar's three starting battalions by 15% (460→529, 270→311,
346→398) to give the player a stronger opening battle position.
Raise Luke the Prank-tricker's Sadar faction bias from 80 to 110.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Increase all spatial fields (icon sizes, distances, heights, spreads,
radii) by approximately 1.3x across all province action animation types.
Durations, counts, angles, and colors are unchanged.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Match the Send Supplies behavior: goldLeftLabel and foodLeftLabel turn
red when the amount remaining in the origin province would exceed its
gold or food capacity.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Validates all maps to ensure starting positions are not placed on
impassable terrain (water, river, mountain) and that attacker starting
position sets don't overlap with each other.
Kojaria currently fails both checks and needs a map editor fix.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When an AI unit flees and ends the battle, the GameStateViewDiff
removes the unit from UnitsById before ModelUpdated processes the
history. The sound-playing code required the actor to be found in
UnitsById to determine animation coordinates, silently skipping
the entire sound block when the lookup failed.
Add a fallback else branch that plays the sound without animation
when the actor unit has been removed from the model.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The previous "battle_shout" sound felt more like rioting than
feasting. Replace with a group victory cry from freesound.org
(chripei, CC BY 4.0) that better conveys celebration.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Stop label updates from clearing hex background highlights
SetUnitInfoLabels and SetOverlayLabel were calling SetCellBaseColor
(which sets Geometry.Color = Color.clear) and hexMesh.Triangulate on
every invocation. These methods manage TMP_Text labels, not hex
geometry, so the SetCellBaseColor and Triangulate calls were
unnecessary. Removing them eliminates O(N²) Triangulate calls per
selection change (one per cell, each iterating all cells).
ClearOverlays now explicitly resets Geometry.Color for cells that had
a BackgroundHighlightColor and does a single Triangulate when needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Defer hex background highlight updates to HexGrid.Update for atomic mesh rendering
ClearOverlays was eagerly triangulating hexMesh (clearing background
highlights) while overlayMesh borders were only updated in Update().
This caused the two meshes to render out of sync for one frame when
Update order put HexGrid before MouseHandler. Now both meshes are
updated atomically in Update(), and ClearOverlays just clears the
data. Also reduces hexMesh.Triangulate calls from O(N) per overlay
cell per frame to at most one.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix overlay label/image flash by deferring all cleanup to Update
ClearOverlays was eagerly clearing labels and follow-up images while
the overlay border mesh was only updated in HexGrid.Update(). When
Unity's script execution order put HexGrid.Update before
MouseHandler.Update (which changed after the Shardok prefab
extraction in #6485), the labels/images were cleared mid-frame but
not re-set until next frame — causing a visible one-frame flash on
every unit selection.
Now ClearOverlays just clears the data list. Update() owns all visual
state: it clears stale labels, images, and background highlights from
the previous frame, sets new ones from current overlayCells, and
triangulates both meshes — all atomically regardless of script order.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Animation objects are parented to mapContainer, not the animator. When
the Map deactivates mid-animation (e.g. entering a battle), coroutines
die but the partly-faded sprites linger. Track all objects created by
CreateUIObject and destroy any survivors in OnDisable.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add elephant/mammoth beast effect using Animal Pack Deluxe v2
Wire up African and Indian elephant prefabs from the newly purchased
Animal Pack Deluxe v2 as a proper beast effect. Mammoths reuse the
same elephant models. Includes a custom ElephantMapAnims controller
to remap v2 animation state names to AnimalEffect's expected states.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix elephant animation: use v2 pack's own controller, fix prefab refs
- Use the v2 pack's African_elephant_anim_controller directly instead
of our custom ElephantMapAnims (which couldn't resolve FBX clips)
- Fix animalPrefabs fileIDs to match actual root GameObjects
- Regenerate animation FBX .meta files for Unity 6 compatibility
The v2 controller's state names don't fully match AnimalEffect's
expected names (idle A/attack A vs idle/attack), so animations are
partial. Next step: remap state names to get all 4 animations working.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Rename v2 controller states to match AnimalEffect expectations
Rename 'idle A ' → 'idle' and 'attack A' → 'attack' in the v2 pack's
African_elephant_anim_controller so all 4 animation states work with
AnimalEffect's Animator.Play() calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Action results arriving from the server while the Map game object is
inactive (e.g. during a battle) are queued and replayed 1s after the
Map reactivates, so the player sees what happened while they were away.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The full AICommandFilter was only applied during lookahead (inside
FindBestCommand), not at the root level of IterativeSearch. This
caused the AI to repeatedly choose METEOR_TARGET_COMMAND because the
re-target filter never ran on the top-level command list.
Adds FilterLoopingCommands — a minimal filter that only removes
genuinely never-useful commands (meteor re-target/cancel when a target
is already set). The full aggressive filter stays in lookahead only,
where pruning is a performance optimization. Applying the full filter
at the root could miss moves that look bad locally but prove
worthwhile with deeper search.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
METEOR_START uses ActionCost::usesAll which drains AP to 0. The
MeteorTargetCommandFactory then unconditionally generates a
METEOR_CANCEL command (cost 1 AP) for the mage in TARGET state.
When the AI evaluates this command, MutatingSpendActionPoints throws
because 1 > 0.
Two fixes:
1. Set meteorCancelActionPointCost to 0, matching #6454's fix for
meteorTargetActionPointCost.
2. Filter out meteor re-target/cancel in AICommandFilter when the
mage already has a target. Re-targeting exists for human misclick
correction; the AI commits to its first choice and could otherwise
loop endlessly re-targeting at 0 AP cost.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Mammoth had human-bandit-like stats (likelihood 0.10, power 1.5,
economy devastation) instead of large-animal stats. Update to be
rarer than elephants with appropriate power, count, and devastation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Extract Command Panel into prefab
Move the Command Panel hierarchy (37 command sub-panels) from
Gameplay.unity into its own prefab at Assets/Eagle/. This is the
largest single component in the scene at ~126K lines. Scene-level
cross-references (eagleGameController, connectionHandler, hexGrid,
shardokContainer, etc.) are preserved as PrefabInstance overrides.
Reduces Gameplay.unity from ~232K to ~102K lines (56% reduction).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Extract 35 command sub-panels into individual prefabs
Break down Command Panel.prefab (126K lines) into 35 individual
prefabs under Assets/Eagle/CommandPanels/. Command Panel itself
becomes a thin container (~6K lines) with PrefabInstance references.
Largest sub-panel (March Panel) is 12.7K lines; smallest are ~230
lines. All are independently editable and produce focused diffs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Move the Shardok Container hierarchy (Shardok Canvas + Weather Canvas)
from Gameplay.unity into its own prefab at Assets/Shardok/. External
field references (mainCamera, eagleCanvas, soundManager, audioClipSource)
are preserved as scene-level PrefabInstance overrides.
Also adds UNITY_POST_PROCESSING_STACK_V2 scripting define symbols.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates 3 duplicate instances by adding a static Instance property,
replacing all GetComponentInParent lookups and serialized field references
with EagleCommonTextures.Instance. Removes the duplicate components from
ConnectionCanvas, Shardok Container, and Eagle Canvas in the scene, and
moves the single remaining instance to Main Camera so it's guaranteed to
have Awake() called regardless of which canvases are active.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Extract Custom Battle Panel into prefab
Remove baked-in Custom Unit Row instances from the prefab since
EventBasedTable.Awake() destroys them and recreates from rowPrefab
at runtime. Also remove stale SimpleFileBrowser references from
eagle0.sln.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Convert Custom Battle Panel prefab from LFS to regular git
This prefab is actively edited line-by-line and benefits from normal
git diff/blame/merge. LFS is better for opaque binary assets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add missing Unity .meta files, HoneyBadger import artifacts, and fix_import.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove HoneyBadger import artifacts and gitignore FBX auto-generated files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Remove the SimpleFileBrowser plugin and its usage in CustomBattleHandler
(custom map loading and game request loading via file dialogs). Remove
the Custom Map Toggle, Load Button from the Custom Battle Panel, and
the obsolete Tutorial Lite button from the Lobby Panel prefab.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The Settings Panel prefab starts inactive, so Start() never runs until
the user opens it. This caused persisted values (province label font
size, audio volumes, tooltip behavior) to not be applied until Escape
was pressed, producing a visible font resize.
Split initialization: ApplyPersistedSettings() loads PlayerPrefs and
applies to game systems early via SettingsKeyListener.Start() on the
always-active PersistentCanvas. The deferred Start() just syncs UI
sliders/toggles to match the already-applied values.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Replace tutorial buttons with phase dropdown in ConnectionHandler
Replace the two separate tutorial buttons (tutorialButton, skipBattleTutorialButton)
with a TMP_Dropdown + Button combo so any tutorial phase can be selected from the lobby
without adding more buttons for each new phase.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix C# TutorialPhase enum names to match protobuf codegen
C# protobuf strips the common prefix, so the enum values are
TutorialPhase.OpeningBattle (not TutorialPhaseOpeningBattle), etc.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire tutorial phase dropdown and start button in Gameplay scene
Add missing using directive for Net.Eagle0.Eagle.Common namespace
and re-wire tutorialPhaseDropdown and startTutorialButton to the
Lobby Panel prefab instance.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reduces Gameplay.unity by ~13k lines by extracting three connection UI
panels into standalone prefabs with scene-level overrides for callbacks
that reference the scene-level ConnectionHandler.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Longbowmen can now target enemies at range 3 when shooting in the
direction the wind is blowing (±1 hex sector), provided wind speed
meets the configurable `longbowWindBonusRangeMinSpeed` threshold
(default 20 mph). The 20 mph threshold reflects the point where wind
meaningfully extends arrow range, below the 35 mph blizzard threshold
where archery is already disabled.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Replace tutorial booleans with TutorialPhase in CreateGameRequest
Replace `is_tutorial` + `skip_tutorial_battle` booleans with a single
`TutorialPhase tutorial_start_phase` field, making the API extensible
for future tutorial phases (Mobilization, etc.) without adding more
boolean flags.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add service visibility to tutorial_phase_converter BUILD target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Keep old tutorial boolean fields in CreateGameRequest proto
Keep is_tutorial and skip_tutorial_battle fields so the Unity build
doesn't break. They'll be reserved in the client-side PR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Extract Settings Panel into prefab to reduce Gameplay.unity size
The Settings Panel hierarchy (~11k lines) was inline in the scene file,
making Gameplay.unity harder to diff and merge. Extract it into a
standalone prefab with scene-level overrides for cross-references.
Fix 15 event callback targets that Unity zeroed out during extraction
(sliders, toggles, buttons all needed re-pointing to the
SettingsPanelController inside the prefab).
Add SettingsKeyListener on PersistentCanvas to handle Escape/F11 keys,
since the SettingsPanelController now lives on the (sometimes inactive)
prefab root and can't run Update() when hidden.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove scene overrides that zeroed out prefab event callbacks
The prefab instance in Gameplay.unity had overrides that set 6 event
callback targets to null, overriding the correct values in the prefab.
Remove these overrides so the prefab's own targets take effect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tiger and lion beast effects using ithappy Animals FREE pack
Import the free ithappy Animals FREE asset pack and create tiger/lion
beast effects. The tiger model is reused at 1.7x scale for lion.
A custom TigerMapAnims controller maps ithappy's idle/walk/idle_rare/run
clips to AnimalEffect's expected idle/walk/eat/attack state names.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove lion effect (scaled tiger is not convincing)
Lion falls back to generic vultures until a proper model is found.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix ithappy files stored as LFS objects breaking Unity import
The broad ithappy/** LFS rule was storing text YAML files (.controller,
.mat, .prefab, .anim, etc.) as LFS objects, causing Unity to fail with
"File may be corrupted" errors. Remove the rule and re-add files so text
files are stored as regular git objects while binaries (.fbx, .png) are
still covered by per-extension LFS rules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Strip third-party scripts/physics from animal prefabs on spawn
ithappy Tiger_001 prefab includes CharacterController, CreatureMover,
and MovePlayerInput components that conflict with AnimalEffect's own
movement system. DestroyImmediate these after instantiation to prevent
"CharacterController.Move called on inactive controller" errors.
Also wires up tigerEffectPrefab in Gameplay.unity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move beast effect prefabs to Addressables for CDN delivery (#6373)
Replace 33 serialized GameObject fields in ProvinceBeastsController with
Addressables.LoadAssetsAsync label-based loading ("beast-effects").
Prefabs stay in Assets/Eagle/Effects/ but are now delivered via CDN at
https://assets.eagle0.net/addressables/[BuildTarget] instead of being
bundled in the app binary. This removes ~80-100 MB of 3D models,
textures, and animations from the initial app download.
Loading strategy:
- Eager load in Start() (last priority — after music)
- Game state that arrives before loading completes is stashed and
applied when ready
- If loading fails (offline, no cache), beasts simply don't render
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tiger and elephant beast effects using Animal Pack Deluxe
Route "tiger" and "elephant" beast names to dedicated TigerEffect and
ElephantEffect prefabs in ProvinceBeastsController. Prefabs to be wired
up in the Unity editor using existing Animal Pack Deluxe assets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Register TigerEffect in Addressable beast-effects group
The TigerEffect prefab was added in the add-tiger-effect branch but was
never registered in the Beast Effects Addressable group, so LookupPrefab
returned null at runtime. Also removes a duplicate "tiger" switch case
from the merge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix AnimalEffect component removal errors on spawn
Instantiate prefabs inactive to prevent CharacterController stepOffset
validation errors, and remove MonoBehaviours in reverse component order
to respect [RequireComponent] dependency chains before removing
CharacterController.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix ithappy Animals_FREE material to use Built-in Standard shader
The material shipped with a URP Lit shader reference, causing solid pink
rendering on Built-in render pipeline. Swap to Built-in Standard shader
and remove the URP version tracker MonoBehaviour.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix tiger animations by using standalone .anim clips
The ithappy Animals_FREE FBX was imported with avatarSetup: 0 (no
avatar), so its embedded animation clips had no bone binding data.
Switch TigerMapAnims.controller to reference the standalone .anim files
which have explicit transform path bindings. Also build a Generic avatar
at runtime for prefabs missing one, and set up the Animator before
activation so it initializes with correct avatar and controller.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix duplicate Start/LoadBeastEffects from rebase
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove unused ithappy Animals_FREE assets
Only the tiger model, texture, material, and animations are needed.
Removes all other animals (chicken, deer, dog, horse, kitty, penguin),
demo scenes, skyboxes, scripts, floor material, animation controllers,
and render pipeline conversion files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove CharacterController and deleted scripts from Tiger prefab
The prefab still referenced CreatureMover and MovePlayerInput scripts
that were deleted in the previous commit, causing missing script errors
in the Unity editor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update Tiger_001 prefab after Unity re-serialization
Unity re-serialized the prefab after we removed the CharacterController
and missing script components. This is the clean editor-saved version.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add battle-in-progress animation on strategic map
Display looping weapon-clash effects between defender and attacker origin
provinces while battles are active. Follows the ProvinceFestivalController
pattern for centroid loading and effect lifecycle management.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Load battle sound from sfx-combat addressables instead of inspector field
Use the "melee" clip from the "sfx-combat" addressable label, matching
the pattern used by ProvinceActionAnimator and SoundManager.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Tune execution animation duration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix battle effect: orientation, sound, two weapons, test provinces
- Fix upside-down weapons by flipping weapon2 Y-scale (mirroring)
- Play melee clip as one-shot at each impact instead of continuous loop
- Use two different weapon sprites (sword + axe) for visual variety
- Change test ContextMenu to use adjacent provinces 2 and 5
- Add Gameplay.unity scene wiring
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add arrow volleys, fix weapon orientation, fix audio pattern
- Add arrow volley animations flying between province centers with
parabolic arc, random direction, and archery sound
- Fix weapon rotation: per-weapon offsets (sword blade down = +90,
mace head up = -90) so both swing business-end first
- Fix arrow sprite rotation offset (-135 for strategic map UI space)
- Fix audio: move playback to controller using Action callbacks,
matching ProvinceActionAnimator pattern (same object owns AudioSource
and calls PlayOneShot)
- Load audio in OnEnable with Dictionary<string,AudioClip> like
ProvinceActionAnimator
- Add configurable volume and rotation offset inspector fields
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add meta files for new battle effect scripts and addressable settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Expose attacker army origin provinces in the ShardokBattlePlayerInfo view so the
Unity client can draw battle-in-progress animations between defender and attacker
origin provinces on the strategic map.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Split captured hero recruitment into distinct ActionResultTypes and add captured hero animations
HandleCapturedHeroesCommand previously used RecruitHeroes (21) for both successful
and failed captured hero recruitment, making them indistinguishable on the client.
Add CapturedHeroRecruited (162) and CapturedHeroRecruitmentRefused (163) so the
Unity client can play the correct animation for each outcome. Also add province
animations for all 6 captured hero dispensations (recruited, refused, imprisoned,
exiled, executed, returned).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add sound effects for captured hero animations
Map each captured hero ActionResultType to an appropriate sound clip,
reusing the same clips as the animations they share patterns with.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
HandleCapturedHeroesCommand previously used RecruitHeroes (21) for both successful
and failed captured hero recruitment, making them indistinguishable on the client.
Add CapturedHeroRecruited (162) and CapturedHeroRecruitmentRefused (163) so the
client can play the correct animation for each outcome.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix tutorial duplicate hero bug by changing Tarn from Fled to Outlawed
PR #6464 set fleeProvinceId to None for the tutorial attacker army, but
TutorialBattleAutoResolve still marked Tarn as Fled. With no flee
province, unitReturned() returned true for Fled, triggering the
shattered army path which created outlaws for ALL attacker heroes —
duplicating the heroes already created via defenderCP's newOutlaws.
Changing Tarn to Outlawed avoids the shattered army path entirely while
preserving the tutorial storyline: Tarn escapes capture and doesn't
appear in HandleCapturedHeroes, then EagleAppearsAction picks him up.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update TutorialTarnDisappearsAction to handle Outlawed status
With Tarn now marked as Outlawed (instead of Fled), he ends up as an
unaffiliated hero in the battle province rather than in an incoming
army to Nikemi. Update the disappears action to remove him from
unaffiliated heroes so he's properly hidden until EagleAppearsAction.
Also remove the now-dead fled/incoming-army logic and unused MovingArmy
import.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Set speakerImagePath directly in tutorial_strategic.json for John Ranil
and Elena Fyar instead of relying on dynamic resolution, which can fail
due to timing issues with the Eagle model.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tutorial indicator to lobby running game items
Add a tutorialIndicator GameObject field to RunningGameItem that is
shown/hidden based on the game_type from the server. Pass GameType
through from ConnectionHandler so the client can distinguish tutorial
games in the lobby list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire tutorial indicator in Running Game prefab
Connect the Tutorial TextMeshPro label to the tutorialIndicator
field on RunningGameItem so it shows/hides based on GameType.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds one-shot visual animations at province centers for all remaining
player commands: Rest, Train, OrganizeTroops, ArmTroops, RecruitHeroes,
Trade, OrdersIssued, SentSupplies, SuppressBeasts (success/fail),
Divined, March, SwearBrotherhood (success/fail), ControlWeather, Alms,
HeroGift, ApprehendOutlaw, Recon, ExileVassal, and StartEpidemic.
Each animation includes inspector-tunable parameters, a ContextMenu test
method, and a sound effect mapping wired through the actionSounds array.
21 audio clips added to the sfx-combat Addressables group.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Plumb the existing GameType enum (NORMAL/TUTORIAL) through to the lobby
response so clients can distinguish tutorial games from normal games.
- Add game_type field to RunningGame (persistence) and GameInfo (API) protos
- Add gameType to GamePlayerInfo case class and populate it in all 4
construction sites (gamesFor cached/full-load, gamesForWithoutBlocking
loaded/unloaded) plus save()
- Pass gameType through to GameInfo in EagleServiceImpl
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The tutorial attacking army had fleeProvinceId set to its origin province, which
allowed Tarn's forces to flee during the opening battle. Since canFleeHeroIds was
always Set.empty, the flee logic fell back to fleeProvinceId.isDefined (true).
Fix by setting fleeProvinceId = None for the tutorial army, and remove the unused
canFleeHeroIds field entirely from ShardokBattle, its proto, converter, and all
call sites.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add TutorialPhase to GameType ADT to track tutorial progression
Converts GameType from a simple Normal/Tutorial enum into an ADT where
Tutorial wraps a TutorialPhase (OpeningBattle, Expansion, Mobilization,
EagleAppears, Complete). This separates "what kind of game" from "what
phase of the tutorial", enabling phase-aware event logic and future
ability to start at a specific phase.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix C# tutorial check to match on Tutorial specifically
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use TUTORIAL_PHASE_ prefix for proto enum values
Follow Google's protobuf enum naming best practice of prefixing all
values with the enum name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Pale yellow/white particle chain connects necromancers to their
controlled undead units. Shimmers in and out when not actively
controlled this round, becomes solid translucent when controlled.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Increase destinationScale from 45 to 60 for a wider cone at the target.
Track the mage's meteor phase per beam and apply a 3x speed multiplier
to vibration and shimmer during the Cast phase.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add sound effect for meteor start animation
Map ActionType.MeteorStart to Fire Spelll 01 from the magic spells
sound pack so the meteor preparation animation has an accompanying
magic charging sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add sound effect for meteor target animation
Map ActionType.MeteorTarget to Fire Spelll 02 so the targeting
crosshairs animation also plays a fire magic sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
SetupLobbyUI ran at startup before login, so IsAdmin was stale.
Re-evaluate admin-gated button visibility in OnOAuthLoginSuccess.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a feast animation to ProvinceActionAnimator that spawns food items
(drumstick, beer, wine, grapes, cheese, bread) tossed into the air on
parabolic arcs with spin and fade. Plays a battle_shout cheering sound.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Skip auto-selecting mage unit that already has a meteor target
When a mage already has a CastTarget set, don't auto-select them
via SelectAppropriateDefaultCommand. The player can still manually
click the unit to re-target, but we no longer default to showing
commands for a unit that has already targeted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Find untargeted mage when multiple have MeteorTarget available
Instead of checking the first unit with MeteorTargetCommand and
skipping the entire command type if that unit already has a target,
search through all available commands of the type to find a unit
that hasn't targeted yet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Wire ProvinceActionAnimator in Gameplay scene
Add ProvinceActionAnimator component to the scene with Inspector
references and action sound configuration for Improve animations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update ProvinceActionAnimator scene wiring
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Log game ID, player ID, round, and stack trace to stderr before
re-throwing, so AI thread crashes are diagnosable without a debugger.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add province action animations to Eagle strategic map
Play one-shot animations at province centers when action results arrive.
Starting with the Improve action, which shows a hammer-strike animation
with sparks. Sound effects loaded from sfx-combat Addressables by name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix hammer head alignment and restore rotation defaults
Offset tool position upward by half the tool size so the hammer head
(bottom of rotated image) aligns with the province center where sparks
appear. With Image+RectTransform the pivot is at center, unlike
SpriteRenderer which uses the sprite's imported pivot — without the
offset the middle of the hammer sat on the province.
Restore hammerBaseRotation=180 and hammerFlipHorizontal=true to match
ToolAnimator defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Set hammer rotation default to 90, fix sound loading robustness
Change hammerBaseRotation default to 90 to match tested working value.
Sound effects were silently failing — the Addressables coroutine started
in Awake() could be interrupted if the GameObject was deactivated during
scene setup. Now uses lazy-load pattern: retries loading on first play
attempt if clips aren't ready. Added diagnostic logging for load
success/failure and missing clip names.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove defensive null checks on inspector fields
Let missing inspector wiring throw NullReferenceException immediately
rather than silently skipping. This was hiding the sound bug — the old
guard clause bailed before any logging could fire.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix sound loading: move coroutine to OnEnable, remove silent null guard
The LoadSoundEffects coroutine started in Awake() was being killed when
the GameObject gets deactivated during scene setup. Moving it to
OnEnable() ensures it retries on reactivation. Also removes the
_loadedClips null guard so failures are visible via NullReferenceException
rather than silently skipping sound playback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Play metal strike sound per hammer impact, add to Addressables
Switch from repair sound to Metal Weapon Hit Stone 2_1 for Improve
animation. Sound now plays once per hammer strike at impact rather
than once at the start of the animation. Added the clip to the
sfx-combat Addressables group.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
PR #6450 allowed meteor re-targeting, but it fails at runtime because
the initial target uses ActionCost::usesAll which drains all AP to 0,
leaving nothing for the re-target. Setting the target cost to 0 fixes
this: usesAll with minimum 0 always succeeds (0 >= 0), still drains
remaining AP (ending the mage's turn), and removes the vigor penalty.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously province 6 (Soria) had battalions but no heroes, and province
stats were hardcoded only for province 32. Now The Eagle's faction matches
the normal game: Tarn defects from faction 2, random vassals are generated
(5 for province 32, 4 for province 6), per-province stats are correct,
faction is hostile to Bregos with trust -100, and battalion names use
"Tarn's" prefix for province 6.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The validateAtMostOneAttackingAlliance check was firing during
UncontestedConquest phase, before PerformUncontestedConquestAction could
resolve the situation (largest army conquers). Skip the validation during
UncontestedConquest, matching the existing AttackDecision exemption.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add is_admin field to UserInfo proto and populate it through Go auth
handlers, Scala fallback handler, and C# client storage. Replace the
hardcoded display name check with the server-provided admin flag.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously, once a mage selected a meteor target, the command was
locked in with no way to correct a misclick. Now the target/cancel
commands remain available after targeting, but as optional (not
required to end turn), so the player can re-target, cancel, or
simply end their turn keeping the current target.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
All two-stage actions now have distinct success/failure animations.
Remove the completed status tracking table and animation descriptions,
keeping only the 7-step wiring guide for adding new two-stage actions.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for BraveWater
Split the shared BraveWater animation into two-stage success/failure variants:
- Success: Strong upward splash with bright droplets, expanding ripples, blue-white flash
- Failure: Droplets rise partway then fall back, darkening color, foam churns turbulently
Updates ShardokGameController enum, mapping methods, and dispatch.
Adds AnimateBraveWaterSuccess/AnimateBraveWaterFailed to WaterEffectAnimator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add BraveWater success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use distinct splash sound for BraveWater success
Attempt uses braved_water (wading in), success now uses splash
(satisfying water crossing) so the two phases sound different.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make BraveWater failure animation more dramatic and violent
Rewrote AnimateBraveWaterFailedEffect with 4 phases: high promising rise
(14 large particles with wide spread), brief stall with shuddering and
color flickering at peak, violent cubic-acceleration crash back down past
center with particles growing on impact, and aggressive turbulent churn
with large oscillations darkening to murky water.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for DismissUnit
Split the shared DismissUnit animation into two-stage success/failure variants:
- Success: Golden particles rise upward with warm golden glow pulse
- Failure: Particles expand then snap back inward, turning red with rejection flash
Updates ShardokGameController enum, mapping methods, and dispatch.
Adds AnimateDismissSuccess/AnimateDismissFailed to DismissAnimator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add DismissUnit success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Double the spread width of DismissUnit success and failure effects
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use undead_break_control sound for DismissUnit failure
The undead have broken free — use the dramatic control-broken sound
instead of the generic failure horn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Scale up DismissUnit effects: attempt 50% wider/taller, particles 2x
Attempt: particle spread multiplied by 1.5x, rise height 1.5x.
Success and failure: particle sizes doubled (3-6 to 6-12, 3-5 to 6-10).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix attempt particles: use visible sizes (4-8) instead of near-zero (0-4)
The original Random.Range(4f, 0f) produced values 0-4 with many near
zero, making particles invisible. Now uses 4-8 for consistently visible
particles that shrink to zero as they drift.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for RaiseDead
Split the shared RaiseDead animation into two-stage success/failure variants:
- Success: More figures rise quickly with bright green glow and pulse
- Failure: Figures rise partway then collapse back, glow flickers green to grey
Updates ShardokGameController enum, mapping methods, and dispatch.
Adds AnimateRaiseDeadSuccess/AnimateRaiseDeadFailed to RaiseDeadAnimator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add RaiseDead success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use distinct success sound for RaiseDead (undead_grew instead of raise_undead)
Attempt uses raise_undead (eerie summoning), success now uses
undead_grew (undead power surge) so the two phases sound different.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make RaiseDead failure more dramatic and use impactful success sound
Failure: figures now rise 75% of the way (was 40%), fully visible at
near-full opacity, stall with flickering glow shifting green to grey,
then get yanked back down with cubic acceleration. Much more visible
contrast between "almost worked" and sudden collapse.
Success sound: use holy_wave_damage for a darker, more impactful surge
instead of undead_grew.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct Scout success/failure animations
Success: cone fully extended, glow pulses bright and expands at target.
Failure: cone extends partway, hits a dark barrier with red flash,
cone recoils, eye dims and shrinks to nothing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add Scout success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use boo sound for Scout failure instead of generic horn
Scout failure plays "boo" — a disappointed reaction when the scout
can't see through the barrier.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Rework Scout failure animation as dark reversal of attempt
Replace the red-barrier-and-recoil failure animation with a reversal:
glow disk at target turns dark, then cone and glow retract back toward
the source eye, which darkens and shrinks away. Also change failure
sound from boo to generic negative effect horn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove leftover merge conflict marker from TWO_STAGE_ANIMATIONS.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct FreezeWater success/failure animations
Success: crystals solidify with bright white flash and intense shimmer.
Failure: crystals form then crack, flicker, and shatter outward as
spinning shards fading from icy blue to dull grey.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add FreezeWater success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use splash sound for FreezeWater failure instead of generic horn
Ice shatters and reverts to water — splash conveys the ice losing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make freeze success 30% bigger in both spread and crystal scale
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct ExtinguishFire success/failure animations
Success: heavy water deluge with thick rising steam cloud.
Failure: droplets evaporate mid-fall, fire flares back with
orange/red burst.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add ExtinguishFire success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make ExtinguishFire failure more dramatic with fire flare-up
When extinguish fails, also trigger FireEffectAnimator.AnimateFireSuccess
so the fire blazes back up with tall flickering flames and rising embers
alongside the evaporating water droplets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use distinct sounds for ExtinguishFire attempt/success/failure
- Attempt: splash (water thrown at fire)
- Success: fire_extinguish (satisfying sizzle/hiss)
- Failure: raging_fire (fire flares back up)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Captured units' battalions are destroyed during BattleEnded processing,
but the multi-victor branch was storing those destroyed IDs in
AftermathClaimantUnit. This caused invalid battalion references when
claimants withdrew. Filter them out when building claimants and
notFledDefenders, and exclude them from armySize calculation.
Also update ALLIED_VICTORY_BATTLE_RESOLUTION.md to reflect PR 6429
changes (LastAllianceStanding for attackers, allied co-attacker added
to winners for solo castle holder) and document the dead-ally
GameOverResponsePopulator bug fixed in PR 6435.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Remove ClipRectMeshUnlit shader replacement for 3D beast models
The shader replacement multiplied texture color by vertex colors, turning
honey badger white stripes red. RendererViewportClipper already handles
viewport clipping for all 3D models generically, making the shader
approach redundant and harmful.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Set render queue on beast models to render above province texture
The old ClipRectMeshUnlit shader used Queue=Transparent+100 and
ZTest Always to draw above the province colour layer. Now that models
keep their original shaders, set mat.renderQueue = 3100 in
RendererViewportClipper.Awake() to match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix spelling: colour -> color
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reserve removed fields (ally_victory=2, draw=3) and delete the
DrawType enum to match the Eagle-side proto cleanup. Update C++
conversion code: ALLY_VICTORY now maps to victory, DRAW now throws.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When your alliance wins a battle but your own units were destroyed,
you should still get a victory — not an internal error. Change the
else-if branch from throwing to assigning victory for allied
non-winners.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The buttonTooltip and oddsPanel were setting transform.position/localPosition
using screen-space coordinates directly, which placed them incorrectly when
the canvas uses Screen Space Camera mode. Convert screen positions to canvas
world space via ScreenPointToWorldPointInRectangle.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When GetGameHistory returns a non-empty error field (game failed to load),
the admin server now transparently falls back to GetRawGameHistory. The UI
shows a warning banner and uses raw-rewind instead of normal rewind. This
lets admins view and fix broken games without any manual switching.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix allied attacker victory conditions
Allied attackers couldn't win by elimination because LastAllianceStanding
wasn't in their victory conditions. When one attacker held all castles,
the ally was treated as a loser because they weren't in winning_shardok_ids.
- Add LastAllianceStanding to attacker victory conditions in RequestBattlesAction
- Add mutually-allied co-attackers to winning_shardok_ids when one player holds all castles
- Add tests for both fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Extract PopulateGameOverResponse into testable library and add tests
Move PopulateGameOverResponse and its helpers (IncludeUnitProtoInReturn,
FromUnitFb, FromInternalStatus) from EagleInterfaceGrpcServer.cpp into a
separate GameOverResponsePopulator library so they can be unit tested
directly. Add 7 tests covering victory/loss assignment, allied player
validation, unit filtering, and status conversion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Shardok now emits Victory for all winners instead of distinguishing
AllyVictory. Remove the AllyVictory case from EndGameCondition, simplify
all isWinOrAllyVictory checks to isVictory, reserve the ally_victory
proto field, and update the allied victory documentation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a game's action history contains a broken action result, the game
can't load because formAwrs throws during state replay. This means the
admin console can't view history or rewind to fix it.
These two new RPCs operate directly on persisted chunk files, bypassing
the formAwrs state-replay pipeline:
- GetRawGameHistory: reads ActionResult protos from .e0a chunks and
returns them as JSON without replaying game state
- RawRewindGame: truncates chunk files to a target action count without
loading the game into memory
Also adds an error field to GetGameHistoryResponse so load failures are
reported instead of returning empty results.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
PR #6425 removed DrawType from the Eagle common proto but didn't
update the C++ conversion code in EagleInterfaceGrpcServer, causing
a linux-aarch64 cross-compile failure.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Every battle must have exactly one player with WIN_AFTER_MAX_ROUNDS.
This adds startup validation in StartGame and replaces the unreachable
DRAW result with a ShardokInternalErrorException.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Draws are dead code left over from removed Free For All battles. Shardok
battles always have exactly one defender with WinAfterMaxRounds, so if
max rounds is exceeded the defender wins — a draw is never produced.
- Remove DrawType enum and Draw case from EndGameCondition
- Remove isDraw method and all isDraw checks in unitReturned
- Remove Draw proto conversion code from ShardokBattleConverter
- Remove DrawType from victory_condition.proto (Eagle common only)
- Add internalRequire validation that exactly 1 player has
WinAfterMaxRounds when building shardokPlayers
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix tooltip positioning for Screen Space Camera canvas
HoveringTooltip.SetPosition() was mixing world-space and screen-space
coordinates. With Screen Space Camera, GetWorldCorners returns world
coordinates (not screen pixels), and the tooltip was placed at the
wrong position. Now routes all positioning through screen space using
RectTransformUtility, with the correct canvas plane z for projection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use world-space positioning instead of screen-space roundtrip
Work directly in world space using lossyScale for offsets and
canvas GetWorldCorners for overflow, avoiding broken coordinate
space conversions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Convert tooltip providers to pass screen-space Rects
The camera looks downward, so GetWorldCorners returns points where
the visual "height" is along z, not y. A 2D Rect from world coords
loses the z axis entirely (height=0). Fix by converting world corners
to screen space in the providers via WorldToScreenPoint, then
converting back to canvas world space in HoveringTooltip via
ScreenPointToWorldPointInRectangle for final placement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Players with no active games were being automatically thrown into a
new 7-faction game. Remove this behavior so they always land in the
lobby and can choose what to do.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix allied battle victory: treat AllyVictory as winner in ResolveBattleAction
The multi-victor battle aftermath code (added in #6411) was dead code because
the winner/loser partition used `isVictory` which excludes `AllyVictory`.
Allied winners were classified as losers, so only one faction ever "won"
and the province was awarded immediately without a claim decision.
Changes:
- Add `isWinOrAllyVictory` to EndGameCondition
- Use it for the winner partition, backstory, and quest fulfillment
- Fix `unitReturned` so AllyVictory heroes stay at the battle province
as claimants instead of being sent home
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update tests to expect multi-winner behavior for allied victories
Tests previously expected AllyVictory units to be sent home. With
the partition fix, allied winners now stay as claimants for the
BattleAftermath claim decision.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The auto-bounce results set hostile armies to Withdrawing status, but
WithdrawnArmiesReturnHomeAction received the original provinces where
those armies were still AwaitingDecision. This caused a validation
error when transitioning to DefenseDecision because non-allied factions
remained in hostileArmies. Apply bounce status changes to the province
list before passing to WithdrawnArmiesReturnHomeAction so it properly
removes the bounced armies.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When multiple non-allied factions attack the same occupied province and
none makes an AttackDecision, they all remain AwaitingDecision. The
existing auto-bounce logic only triggers when an army has Attacking
status. Add a new branch that detects non-allied undecided factions and
bounces all of them to Withdrawing, preventing the
validateAtMostOneAttackingAlliance runtime error.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Three correctness fixes for the battle aftermath decision system:
- Cap withdrawal gold/food at what each faction brought into battle (broughtGold/broughtFood)
- Auto-keep last undecided claimant when nobody has kept yet (prevents broken province state)
- Serialize PendingConquestInfo to proto so server restarts during BattleAftermath don't lose state
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Destroy previous label container before creating new ones when
OnMapLoaded fires multiple times. Add OnDestroy to unsubscribe
from the event.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add FinalizeAftermathAction to handle the all-decided case (run
ProvinceConqueredAction for keeper, create withdrawal armies, clear
pendingConquestInfo), fix hero-battalion pairing by replacing separate
heroIds/battalionIds vectors with paired AftermathClaimantUnit, and
remove dead destroyedBattalions field from MultiVictorBattleSetupAction.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add BattleAftermathDecisionCommandSelector for Unity client
Adds the UI component for the battle aftermath decision system. When
allied factions jointly conquer a province, each winning faction chooses
to Keep Province or Withdraw (with destination and supply sliders).
The selector follows the AttackDecisionCommandSelector pattern with
toggle group, province dropdown, and gold/food sliders. It is inert
until the server command infrastructure sends the corresponding
available command.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add missing EagleGUIUtils using for GUIUtils.DarkColoredProvinceName
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up BattleAftermathDecisionCommandSelector in Gameplay scene
Add the command selector as a child of CommandPanelController with
all Inspector fields connected: toggles, toggle group, withdraw
container, destination dropdown, gold/food sliders and labels, info
text, and button image. Unity events wired for ToggleChanged,
GoldSliderChanged, and FoodSliderChanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Wire up the full command pipeline for battle aftermath decisions so
winning allied factions can choose Keep Province or Withdraw after a
multi-victor assault.
- Add proto messages for available/selected command (field 41) and command type
- Add Scala model types: CommandType, AvailableCommand, SelectedCommand cases
- Add proto converters for all new command types
- Add AvailableBattleAftermathDecisionCommandFactory (sequential by army size)
- Add BattleAftermathDecisionCommand (keep triggers conquest, withdraw creates army)
- Add AutoResolveBattleAftermathAction (last claimant auto-keeps)
- Add BattleAftermathDecisionCommandChooser for AI (always keeps when possible)
- Update AvailableCommandsFactory and RoundPhaseAdvancer for aftermath phase
- Update CommandFactory routing and AIClient integration
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Remove Map Editor scene from build to eliminate duplicated hex tile assets
The Map Editor scene directly references hex tile textures via RawImage
components, pulling ~4.6 MB of textures into the player build that are
already served via Addressables from CDN.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove Map Editor button from connection screen
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove EditorButtonClicked and unused SceneManagement import
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Delete Map Editor scene and MapEditorController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Enable the toggle GameObject in the scene but hide it at runtime
via Application.isEditor, so it only appears in the settings panel
when running inside the Unity Editor. The checked state still comes
from PlayerPrefs as before.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When multiple allied attackers win a province assault, defer province
assignment by storing PendingConquestInfo on the province instead of
immediately running ProvinceConqueredAction. This lays the groundwork
for the battle aftermath decision command (next PR).
- Add PendingConquestInfo, AftermathClaimant, and AftermathDecision types
- Add pendingConquestInfo field to ProvinceT/ProvinceC/ChangedProvinceC
- Branch ResolveBattleAction for multi-victor case via MultiVictorBattleSetupAction
- Add ActionResultType values for aftermath actions
- Update proto converters to handle new province field
- Remove unused validate method from ResolvedEagleUnit (breaks dep cycle)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for Repair
Repair previously used the same hammer-strike animation for both attempt
and result. Now it follows the StartFire/Fear two-stage pattern: attempt
shows hammer strikes, success shows a golden shimmer beam rising, and
failure shows debris collapsing with a dust puff.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Increase two-stage animation delay to let attempt animations finish
The delay cap between attempt and result phases was 0.5s (or 0.3s with
no sound), but the hammer animation runs ~0.6s and fire runs ~0.8s.
Raised the cap to 0.8s and the no-sound fallback to 0.6s so result
animations don't overlap with attempt animations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct repair success sound and make debris explosive
Sound: Wire up a "repair_success" clip for the result sound so it
doesn't repeat the hammer attempt sound. Falls back to the hammer
clip until the audio asset is added to sfx-combat.
Debris: Replace gentle falling animation with a violent explosion —
pieces burst outward from center with physics-driven trajectories
(initial velocity + gravity), fast spin, an initial flash burst,
and a drifting dust cloud.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use "Positive Effect 6" as repair success sound
Add the clip to the sfx-combat addressable group and load it as the
repair success result sound so it plays a distinct chime instead of
repeating the hammer attempt sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When multiple allied players each occupy different critical tiles (castles)
such that all castles are covered, this now triggers a HOLDS_CRITICAL_TILES
victory for all occupants — provided they all have the victory condition set
and are mutually allied. Previously only a single player holding all castles
could trigger this win condition.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for StartFire
Split StartFire into three-stage visuals following the Fear pattern:
attempt shows ignition, success shows fire blazing up (taller/brighter
flames, hotter colors, outward burst), failure shows fire dying out
(shrinking flames, cooler colors, smoke puff).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move StartFire doc to repo-root docs/TWO_STAGE_ANIMATIONS.md
Update the existing doc instead of creating a duplicate in the Unity
client directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Thread client_id from StreamGameRequest through EagleServiceImpl →
GamesManager → GameController → HumanPlayerClientConnectionState.
Update withHumanClient eviction logic to always evict connections with
the same client_id (same device reconnecting/refreshing), while keeping
connections from different client_ids alive (multi-device support).
Cancelled connections for the same faction are still cleaned up.
Changed client_id type from int64 to string for future extensibility
(e.g. embedding device name, platform, or version).
This fixes duplicate updates when a single client calls
StreamOneGameAsync for a refresh on the same gRPC stream, which caused
the "Duplicate unaffiliated hero" crash.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a stable string client_id to StreamGameRequest so the server can
distinguish same-device reconnects from multi-device connections. The
C# client generates a GUID per PersistentClientConnection instance and
sends it with every StreamOneGameAsync request. The server currently
ignores this field; server-side eviction logic follows in a subsequent
PR. Using string instead of int64 for future extensibility (e.g.
embedding device name, platform, or version).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Guard UpdateUnaffiliatedHeroSelections against count mismatch by
rebuilding the table when the model changes between rebuilds.
Keep the throw on server-side duplicate as a safety net.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add post-Eagle tutorial events: Ranil & Elena depart and form factions
After the Eagle appears, John Ranil departs the player's faction 2 rounds
later and Elena Fyar departs 4 rounds later. At 7 rounds after Eagle,
both form their own factions in isolated provinces.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add tests for TutorialHeroDepartureAction and TutorialHeroFactionsAction
Tests cover action result creation, province selection, hero changes,
battalion/faction creation, unaffiliated hero removal, and edge cases
(missing heroes, no suitable province, hero in different province).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Eagle hero missing backstoryVersions causing validation failure
EagleAppearsAction.createEagleHero was creating the hero without
setting backstoryVersions, which defaults to Vector(). The runtime
validator requires backstoryVersions.nonEmpty for all heroes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Eagle appearance crash: split entity creation from province changes
The action result applier processes changedProvinces before newHeroes,
so when EagleAppearsAction put both in a single ActionResultC, the
province change tried to look up the Eagle hero (via
ProvinceUpdateHelpers.applyRulingAndBattalions) before it existed in
the game state, causing a HashMap key-not-found exception.
Split into two sequential action results: first creates the faction,
hero, and battalions; second applies the province changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix duplicate Eagle hero: only place in province 32, not both provinces
The Eagle hero was added to rulingFactionHeroIds for both provinces 32
and 6, violating the one-hero-per-province invariant and causing a
validation crash on game load. Province 6 now gets the Eagle's faction
and battalions but not the hero itself.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace blind `Add(AddedUnaffiliatedHeroes)` with per-hero loop that
throws InvalidOperationException when a hero already exists in the
province's unaffiliated list. This surfaces the root cause of duplicate
rows in the free heroes table instead of masking it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When two clients connected for the same faction (e.g., iPad + Unity
editor), withHumanClient evicted all existing same-faction connections.
Now only cancelled connections are removed, allowing multiple active
clients to coexist with independent update streams.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix province effects missing when CDN centroids arrive after model
Since #6396 moved centroids.json to CDN, it loads asynchronously and
often arrives after MapController.Model is set. All province effect
controllers (festival, epidemic, blizzard, drought, flood, beasts)
silently skipped spawning because _provinceCentroids was empty.
Re-apply effects in OnProvinceMapLoaded when the model is already set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix province effects missing when CDN centroids arrive after model
Since #6396 moved centroids.json to CDN, it loads asynchronously and
often arrives after MapController.Model is set. All province effect
controllers silently skipped spawning because _provinceCentroids was
empty.
Add lazy centroid loading to each controller's update method so they
load centroids on-demand from ProvinceIDLoader.CentroidsJsonText. This
eliminates the callback ordering dependency with OnMapLoaded. Also
re-apply effects in OnProvinceMapLoaded when model is already set.
Remove defensive null checks on Inspector-wired controller references
in UpdateProvinceEffects — missing wiring should fail loudly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sequential _colorIndex caused all firework instances to show correlated
color sequences. Use Random.Range per launch for independent colors.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reduce sync mismatch grace period (60s→5s), HTTP/2 keepalive interval
(15s→5s), and add per-command response timeout (10s) to dramatically
cut recovery time when the client misses a game update.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GamesManager.save() writes games.e0es to S3 synchronously on the gRPC
executor thread via CompletableFuture.join(), blocking all user
operations for the duration of the upload. JFR profiling showed this
blocking for 28-65ms per command, with potential for much longer spikes.
Wrap the top-level S3Persister in AsyncS3Persister, matching what
per-game persisters already do in LocalGamePersisterCreation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The exportArchive step was relying on the Xcode-Token in the keychain
for App Store Connect authentication. This token expires periodically,
breaking headless CI builds. Pass the App Store Connect API key (already
used by the upload step) to the export step as well.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fetch centroids.json from CDN via EagleMapInfo instead of baked TextAsset
ProvinceIDLoader now fetches both the map data and centroids JSON from
CDN using SHA256 hashes from the server-provided EagleMapInfo. OnMapLoaded
fires only after both arrive and validate. All controllers subscribe to
OnMapLoaded for centroid data instead of loading in Awake().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Guard MapController.ProvinceFromPoint against null map bytes
OnMapLoaded now fires later (after both map + centroids arrive),
so Update() can run before _mapIdBytes is set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The SHA256 hash is used as the content-addressable filename for CDN
fetches, making map_name redundant. Reserve field 1 in the proto to
prevent accidental reuse.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Extends EagleMapInfo with centroidsSha256 so the client can validate
centroids.json after CDN download, matching the existing rawGray pattern.
Bumps migration to v3 so existing saves get backfilled.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Centralize centroidsJson reference in ProvinceIDLoader
All 7 province controllers now read centroidsJson from ProvinceIDLoader
instead of each having their own Inspector-linked TextAsset field.
This prepares for making centroids server-driven in the future.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up provinceIDLoader references and remove stale centroidsJson links
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The province ID map is now fetched from CDN at runtime, so it no longer
needs to be a Unity TextAsset. Move it to the resources directory
alongside other game data files and delete the .meta file.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Load province map dimensions and hash from server-provided EagleMapInfo
Replaces hardcoded map dimensions (3786x1834) across 8 Unity client files
with server-driven values from the EagleMapInfo proto added in #6381.
ProvinceIDLoader no longer auto-loads in Awake() — instead, EagleGameController
calls Initialize(EagleMapInfo) on first GameStateView, which looks up the map
asset by name, validates SHA256 integrity, and fires OnMapLoaded for dependents
(MapController, ProvinceBorderDistanceGenerator).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Keep rawGrayCompressed as direct Inspector field instead of map lookup
The server provides dimensions and SHA256 but the client only has one
map asset — no need for a name-based lookup table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fetch province map from CDN instead of bundled TextAsset
ProvinceIDLoader now uses ResourceFetcher.mapFetcher to download the
map data at runtime rather than reading a baked-in TextAsset. The server
provides the map name, dimensions, and SHA256 via EagleMapInfo.
Also fixes:
- Duplicate `using Net.Eagle0.Eagle.Common` in EagleGameController
- ProvinceLabelsController timing: defers CreateLabels to OnMapLoaded
to avoid division by zero when mapWidth/mapHeight are still 0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use eagle0-assets CDN for map fetcher instead of headshots CDN
Make ResourceFetcher base URI an instance field so each fetcher can
point at a different CDN. Map data is at:
https://eagle0-assets.sfo3.digitaloceanspaces.com/strategic-maps/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use SHA256 as CDN fetch path instead of map_name
Content-addressable: the SHA256 hash serves as both the CDN filename
and the integrity check, so map_name is not needed for fetching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* new sha
* Update EagleMapInfoMigration to read from game_parameters.json
Replace hardcoded map info with GameParametersUtils.defaultExpandedGameParameters
so the migration stays in sync with the config file. Bump version to 2 to
re-migrate existing saved games with the new map hash.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When Eagle returns an error during command processing (e.g. battalion
over-capacity validation), the warmup tool now logs the server error
message and dumps its full state (game ID, province, hero, token,
command) instead of silently timing out.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add smooth_boundaries.py for natural province boundary smoothing
Post-processing tool for rawGray.gz.bytes that replaces jagged
province boundaries with smooth, natural-looking ones using:
1. Majority-filter smoothing (iterative mode filter on boundary pixels)
2. Fragment cleanup (BFS expansion preserving ocean-separated islands)
3. Domain warping (Gaussian displacement for organic character)
Includes automated verification (province preservation, neighbor
adjacency, connectivity) and before/after visualization output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Apply boundary smoothing to province map
Run smooth_boundaries.py --seed 42 to smooth jagged province boundaries.
Uses pixel-based adjacency instead of TSV for neighbor verification.
Updates centroids.json with new label positions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add targeted boundary smoothing for West Faluria borders
Adds --targeted-smooth mode to smooth_boundaries.py that applies a
bilateral majority filter to specific province border pairs. Smooths
the 40-37 (West Faluria/Yuetia) and 40-11 (West Faluria/Garholtia)
boundaries to reduce bubble protrusions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Redraw West Faluria borders via manual edit with roughened edges
Hand-edited province boundaries for West Faluria (40) expanding into
Usvol (33) and Hofolen (17), then applied localized domain warp to
roughen the new borders for a natural look. Added rawgray_editor.py
for PNG export/import workflow and roughen_borders.py for localized
border roughening.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Old saved games predate the EagleMapInfo proto fields and will break
once the client stops hardcoding map info. This adds a migration
framework that patches saved game files at server startup, plus the
first migration (version 1) to backfill eagleMap on all existing games.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
EagleMapInfo (map name, dimensions, SHA256) was hardcoded in GameStateViewFilter.
Now it flows through the event-sourcing pipeline: JSON config → GameParameters proto
→ ActionResultC → ActionResultApplierImpl → GameState → GameStateViewFilter, matching
the existing gameType pattern. The proto definition moves from views/ to common/ so
both internal and views packages can reference it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Server now sends map metadata (name, dimensions, SHA256) to clients
via the EagleMapInfo field. Clients will initially ignore this unknown
field until a follow-up client PR adds CDN-based map fetching.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Laufarvia (3) and West Faluria (40) are listed as neighbors in the TSV
but are 113 pixels apart on the actual map -- they haven't been adjacent
since an earlier map revision. Remove the stale adjacency from both
provinces' neighbor/neighborPositions/neighborNames fields.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Weather effect controllers (flood, drought, blizzard) each independently
decompressed the same 6.9 MB rawGray texture. Replace with a shared
reference to ProvinceIDLoader, eliminating 3x duplicate decompression.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Move combat sound effects to Addressables for CDN delivery
All 41 SoundManager AudioClip serialized fields replaced with
Addressables loading via single "sfx-combat" label. Clips are loaded
asynchronously and matched by name to build the ActionType dictionary.
Sound effects gracefully degrade (no sound) if loading hasn't completed.
GauntletThrowAnimator.impactSound and WeatherEffectAnimator audio clips
retain their serialized Inspector references (separate from SoundManager).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Route weather and gauntlet sounds through SoundManager
Move WeatherEffectAnimator's rain/thunder/blizzard audio clips and
GauntletThrowAnimator's impact sound to load via SoundManager instead
of serialized Inspector fields. This routes all game audio through
Addressables for CDN delivery.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Some terrain labels may load successfully but return 0 textures (e.g.
when Addressable bundles haven't been built yet or CDN cache is stale).
Return null instead of crashing on images[random % 0].
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Move tactical combat assets to Addressables for CDN delivery
Fire effect prefab, permanent bridge models (5), and constructed bridge
models (4) are now loaded via Addressables instead of serialized Inspector
fields. TacticalAssetLoader singleton pre-loads in EagleGameController.Start()
so assets are cached before the first battle. Empty arrays as defaults
provide graceful degradation if loading hasn't completed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix HexGrid NRE when cells not yet initialized, re-render on late load
HexGrid.Update() could NRE when cells array was null (grid not yet set
up due to async asset loading). Added null guard matching the pattern
used in all other HexGrid methods.
Also added LoadTacticalAssetsAndRefresh coroutine so bridges and fire
effects re-render if tactical assets finish loading after the initial
grid draw.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix IndexOutOfRange when tactical assets load before model setup
The LoadTacticalAssetsAndRefresh coroutine called SetModifiers() when
Model was non-null but _randomNumberForCellIndex was still empty. Guard
on the list being populated; the normal model-update paths will call
SetModifiers() once setup completes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove SetModifiers() call from asset loading coroutine
The coroutine's SetModifiers() crashes because _randomNumberForCellIndex
is populated in row-order but MapCoordsToGridIndex flips rows, so the
list indices don't match. This was a latent bug exposed by async loading.
SetModifiers() is already called from ModelUpdated() on every model
update, so modifiers will render correctly without the coroutine call.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wait for Addressables to load before setting up hex grid
SetUpGame() called GetImageForTerrain() immediately, but after moving
hex tiles to Addressables the textures load asynchronously. In custom
battles the shardok container activates and SetUpGame runs on the same
frame, before textures are ready — crashing with NullReferenceException
and leaving no hex grid visible.
Fix: split SetUpGame into a sync preamble + SetUpGameAfterLoad coroutine
that waits for IsLoaded. Also change _randomNumberForCellIndex from List
to int[] indexed via MapCoordsToGridIndex so indices match SetModifiers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Move 118 hex tile PNGs from Assets/Resources/Hex Tiles to
Assets/Hex Tiles and register them as Addressables with 22
per-directory labels (hex-plains, hex-forest, hex-winter-hills,
etc.). This removes them from the app binary so they're hosted on
CDN and only redownloaded when content changes.
ImageForTerrainTracker is now a shared singleton that loads textures
via Addressables.LoadAssetsAsync instead of Resources.LoadAll. The
load kicks off in EagleGameController.Start() (strategic scene) so
textures are cached before the player's first tactical battle.
ShardokGameController and MapEditorController reuse the same instance
with a load guard that prevents double-loading.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace 33 serialized GameObject fields in ProvinceBeastsController with
Addressables.LoadAssetsAsync label-based loading ("beast-effects").
Prefabs stay in Assets/Eagle/Effects/ but are now delivered via CDN at
https://assets.eagle0.net/addressables/[BuildTarget] instead of being
bundled in the app binary. This removes ~80-100 MB of 3D models,
textures, and animations from the initial app download.
Loading strategy:
- Eager load in Start() (last priority — after music)
- Game state that arrives before loading completes is stashed and
applied when ready
- If loading fails (offline, no cache), beasts simply don't render
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously the Tutorial button was gated to nolen/tarn but the Tutorial
Lite (skip-battle) button was editor-only. Now both buttons appear for
testers on any platform (iOS, macOS, etc.), and Tutorial Lite also
remains visible in the editor.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The nightly iOS TestFlight build already builds and uploads iOS
addressables as part of its pipeline. The standalone workflow added
unnecessary complexity and forced platform switches on the unity-mac
runner. Manual TestFlight dispatches cover any need for immediate
addressable updates.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes for iOS deep link handling:
1. CI: Remove UIApplicationSceneManifest from the generated Info.plist
during archive. Unity 6 adds this key, which opts the app into the
scene-based lifecycle. iOS then routes deep links through
UISceneDelegate instead of UIApplicationDelegate, but Unity doesn't
implement that path, so deep link URLs are silently dropped.
2. OAuthManager: Don't call Application.Quit() on iOS — it's documented
as having no effect. Instead, let the app go to background when the
browser opens and handle the deep link callback when iOS brings it
back via OnDeepLinkActivated.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
BuildMacPlayer and BuildWindowsPlayer called BuildAddressables() without
first switching the active build target. On self-hosted runners where
Unity state persists between runs, this caused Addressables to be built
for whatever platform ran last (e.g. iOS), resulting in incompatible
bundles at runtime.
Extract SwitchBuildTarget helper from BuildiOSAddressables and call it
in all three build methods before building Addressables.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a stored account's token expires and refresh fails, automatically
initiate a fresh OAuth login with the same provider instead of just
showing a brief error. On startup, show "Session expired" message
instead of silently clearing the status text.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Set roundIdJoined=1 for heroes with per-hero loyalty overrides, pushing
their earliest possible departure back by one month. This gives the
player more time before The Tumbler, Aldric the Overlooked, and similar
low-loyalty heroes leave.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
SubscribeToGame only caught NewGameException, so any other exception
(e.g. file-not-found during map loading) propagated uncaught through
gRPC as a generic "UNKNOWN" error with no logging. Now matches the
PostCommand/PostPlacementCommands pattern: catches ShardokClient,
ShardokInternal, and std::exception with stderr logging and proper
gRPC status codes.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously, FromPath silently returned garbage data when a file didn't
exist (tellg() returns -1, leading to std::bad_alloc from size_t(-1)).
Now throws std::runtime_error with the file path for clear diagnostics.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Have Ranil and Elena explain Improve/Alms instead of Marek
Replace Marek's combined Improve/Give Alms tutorial step with two
separate steps: John Ranil explains province improvement (highlighting
the Improve button) and Elena Fyar explains giving alms (highlighting
the Alms button). Each step notes that any hero can use the command
but the specialist gets a bonus.
Register each command button as a dynamic tutorial target in
CommandButtonPanelController so individual buttons can be highlighted.
Target IDs are derived from the button tooltip (e.g. "ImproveProvinceButton",
"GiveAlmsButton").
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix speaker portraits, highlight visibility, and notification timing
- Resolve speaker headshots from the Eagle model (strategic map) when
the Shardok model is unavailable, fixing blank portraits for Ranil
and Elena in post-battle dialogue
- Add override sorting Canvas (sortingOrder 999) to dialogue highlights
so they render above sibling UI elements like adjacent command buttons.
Increase default thickness from 3 to 5 and minimum strobe alpha from
0.2 to 0.5 for better visibility
- Delay tutorial_rebuild_support trigger until all notifications are
dismissed, so the Marek/Ranil/Elena dialogue sequence starts cleanly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use public GameModel accessor instead of private Model
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix NRE: Canvas already adds RectTransform, use GetComponent
Adding a Canvas component automatically creates a RectTransform, so
AddComponent<RectTransform>() returned null. Use GetComponent instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Re-check deferred tutorials when notifications are dismissed
CheckTutorialRebuildSupport only ran during OnModelUpdated, but after
dismissing notifications no model update occurs. Add OnAllDismissed
callback to NotificationPanel, and have TutorialTriggerRegistry
subscribe to it so deferred tutorials fire once notifications clear.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Filter nightly iOS TestFlight by relevant paths
Instead of rebuilding whenever any file changes, compare changed files
against paths that actually affect the iOS build (C#, protobuf, resources,
C++ plugins, scripts, CI config). Scala, Go, and test-only changes no
longer trigger unnecessary nightly builds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove src/main/cpp/ from iOS-relevant paths
Shardok C++ code is server-side only. Native Unity plugins come from an
external Bazel dep (@net_eagle0_unity_godice), not the in-repo C++ code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The Eagle faction is created mid-game via EagleAppearsAction but
withHandledEngineAndResults only removed AI clients for destroyed
factions — it never added AI clients for newly created ones. The
Eagle faction appeared on the map but never took any turns.
After filtering out removed AI clients, detect new factions from
action results and register AI clients for any that aren't already
human-controlled or AI-controlled.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds a check-changes job that compares HEAD against the last successful
scheduled run. If the SHA matches, the build is skipped. Manual
workflow_dispatch triggers always build regardless.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Flee and retreat commands have no target in their protobuf (only actor),
so the Unity client treated them as untargeted and called
PerformUntargetedCommand without triggering any animation or sound.
Add PlayUntargetedCommandAnimation which looks up the animation type
for the command, finds the nearest enemy to use as the flee direction,
and calls PlayAnimationAndSound with the two-stage flow so both the
attempt and result animations play correctly.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The game type check compared GameType (non-optional) against
Some(GameType.Tutorial), which always evaluates to true in Scala,
causing checkForEvents to always return empty.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Runs at 9 PM Pacific (5 AM UTC) every night, alongside the existing
manual workflow_dispatch trigger.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds a JSON-configurable faction bias that gets applied when a tutorial
hero becomes unaffiliated (imprisoned/exiled). Currently used to give
Luke the Prank-tricker a +30 bias toward Sadar's faction, making him
more willing to join after capture.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Include province name in ready-to-join hero tutorial dialogue
Marek now says which province the recruitable hero is in, e.g.
"there's a hero in Onmaa who's ready to join our cause."
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Advise taking most gold and food when expanding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Rework tutorial pacing: delay expansion until taxes, add town tutorials
- Change "support reached 40" dialogue to congratulate and encourage
continued development until January instead of immediately pushing expansion
- Move expansion instructions to the "taxes collected" dialogue
- Add tutorial_ready_to_join: fires when a free hero with WouldJoin status
appears in the player's province, instructs them to Travel and Recruit
- Add tutorial_in_town: fires when player enters town (ReturnCommand
available), explains all town activities and that Return advances the month
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refine tutorial expansion advice and remove redundant Give Alms
Remove Give Alms from the support tutorial instructions (already covered
earlier). Update the expansion panel to advise taking most heroes
(especially those with professions) and leaving only one or two behind.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add second hero departure tutorial when King loses another follower
Fires tutorial_hero_departed_again on the second month a King's hero
departs (must be a different month than the first departure). Marek
reacts with alarm that the King is losing authority across the realm.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Adjust second hero departure dialogue to hint at something strange
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Rewrite second departure dialogue in Marek's voice
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Raise loyalty from 30 to 35 for distant provinces (15% departure
chance) and from 25 to 30 for Nikemi/Pieksa (20% departure chance).
Heroes were departing too early even with the roundIdJoined fix.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
LoadedHeroConversion.toHeroC was not setting roundIdJoined, leaving it
as None. This meant hero.roundIdJoined.exists() returned false in
PerformHeroDeparturesAction, so the MinimumRoundsBeforeLoyaltyDegrades
check was silently skipped for all tutorial heroes. Move the
roundIdJoined = Some(0) assignment into toHeroC and remove the now-
redundant set in NewGameCreation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Polytope Studio characters (rebels, peasants, knights, etc.) use a
complex zone-coloring shader that can't be replaced by ClipRectMeshUnlit.
ShaderUtils.ReplaceWithClipRect now skips materials without _MainTex and
returns false. When any material is skipped, the effect attaches a new
RendererViewportClipper component that clips by toggling renderer.enabled
based on the global _ViewportClipRect/_PopupClipRect each frame.
Also force vertex color alpha to 1 in ClipRectMeshUnlit to prevent 3D
mesh models with zero vertex alpha from becoming invisible.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a roundIdJoined check to PerformHeroDeparturesAction so heroes
that joined within MinimumRoundsBeforeLoyaltyDegrades rounds cannot
depart. This mirrors the existing loyalty degradation gating in
NewYearAction.
Also raise tutorial hero loyalty from 25 to 30 for outer King's
provinces (20% departure chance), keeping 25 for Nikemi and Pieksa
which are adjacent to Onmaa (25% departure chance).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds Inspector wiring for the skip_tutorial_battle button in lobby prefabs
and includes PopupClipper.cs.meta for the popup effect clipping component.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The .bazelrc flags prefer_prebuilt_protoc and
incompatible_enable_proto_toolchain_resolution were set but the
prebuilt binary repos were never made available. Add the required
use_extension/use_repo calls so Bazel can actually find and use the
prebuilt protoc instead of compiling it from source.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds editor-only "Skip Battle Tutorial" button that creates a tutorial
game with skip_tutorial_battle=true. Fixes CheckTutorialBattleEnded to
detect server-side auto-resolved battles (no RunningShardokGameModels
were ever observed) and fire tutorial_battle_ended normally.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When skip_tutorial_battle is set in CreateGameRequest, the server
creates the tutorial game normally then immediately resolves the
opening battle with a synthetic BattleResolution — defender wins,
Tarn flees, other attackers captured, defender battalions at ~70%.
The existing ResolveBattleAction pipeline runs so all post-battle
logic executes authentically without Shardok involvement.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Render hero backstory popup above map effects
The hero popup panel was rendering behind 3D effects (dragons, animals,
monsters) because effects use sortingOrder=103 / renderQueue=4000 while
the popup was on the root Canvas at sortingOrder=0. Add an override
sorting Canvas (sortingOrder=200) to the popup panel at startup so it
draws above all map effects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Lower effect renderQueue so popup Canvas override can draw above them
The effects used renderQueue=4000 (Overlay), which draws after all
standard UI regardless of Canvas sortingOrder. Changed the
ClipRectMeshUnlit shader queue from Overlay to Transparent+100 and
removed explicit renderQueue=4000 from effect classes. The shader's
ZTest Always + ZWrite Off + sortingOrder=103 is sufficient to render
above the map without needing the Overlay queue.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use shader-based popup masking instead of Canvas override sorting
Canvas override sorting doesn't affect MeshRenderer draw order in
Screen Space - Overlay mode. Instead, use the same clip rect pattern
as viewport clipping: PopupClipper broadcasts the popup's screen-space
bounds as _PopupClipRect, and all effect shaders discard fragments
inside it. The popup rect is cleared on disable so effects render
normally when the popup is hidden.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add popup clip rect to weather shaders (drought/flood overlay)
The ProvinceWeatherShader and ProvinceWeatherMapShader render weather
overlays as UI elements using custom shaders, so they also need the
_PopupClipRect discard check to avoid rendering over the hero popup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
ViewportClipper broadcasts the viewport's screen-space bounds as a global
shader property (_ViewportClipRect). Each effect shader discards fragments
outside those bounds, preventing particles, 3D models, and sprites from
overflowing the map viewport during zoom/pan.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a quest grants a free hero, both the quest notification and
PleaseRecruitMe command arrive in the same batch. Both panels call
SetAsLastSibling() during setup, but PleaseRecruitMe was set up last,
hiding the notification behind it. After the blocking command switch,
re-raise the notification panel if it has pending info so the player
sees the quest completion first.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Self-hosted runners persist .git/lfs/objects/ between runs, and LFS
objects are now served from a local Gitea mirror on the LAN. The
actions/cache step was downloading ~1.8 GB from GitHub on every run,
adding ~35s and consuming transfer quota unnecessarily.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add Flee as two-stage animation with captured-while-fleeing variant
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move TWO_STAGE_ANIMATIONS.md to docs/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move TWO_STAGE_ANIMATIONS.md to repo root docs/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up flee attempt and run away sound effects in Unity scene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add Flee Captured test button to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Split Flee into three distinct animations: attempt, success, captured
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix test buttons to play full two-stage sequence for result animations
Result variant test buttons (Fear Success/Resisted, Flee Success/Captured,
Duel/Declined) now play the complete attempt→result sequence instead of
only the attempt sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make flee attempt animation more visible and cap two-stage delay at 0.5s
Flee attempt now uses full dust burst and multiple blur trail afterimages
instead of minimal dust and single blur. Two-stage delay between attempt
and result capped at 0.5s in both PlayTwoStageSequence and
PlayTwoStageSoundSequence (affects other players' actions and test buttons).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Tune flee animation parameters in Unity scene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a battle ends mid-animation, coroutines stop but GameObjects parented
to the hex grid survive and leak. This extracts the Track/OnDestroy pattern
(originally fixed in FearAnimator for the skull persistence bug) into a
shared base class and applies it to all 22 Shardok animators.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The King was consistently getting overwhelmed in the early game. Add a
Royal Lancers heavy cavalry unit (800 troops) to province 37 to give
him a stronger opening position. Hero count already matches at 7.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
HeroGenerator.getHero returned NoNameRequest for pregenerated heroes,
so no name text request was created when they appeared mid-game. The
name existed in the in-memory pregenerated store but was never written
to SQLite, and getCompleteTextsAccessibleTo only queries SQLite. The
client received the hero's nameTextId but never the actual text,
showing "Hero" as a fallback.
Now getHero assigns an hn_ nameTextId and returns FixedName, so the
name goes through the normal FixedHeroName pipeline: added to SQLite
by foldInLlmRequests, immediately resolved by UnrequestedTextHandler,
and delivered to the client like any other hero name.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
FearAnimator creates GameObjects (skull, tendrils, shield, etc.) as
children of the hex grid and only cleans them up at the end of each
coroutine. If the battle ends mid-animation, the coroutine is
interrupted and cleanup never runs, leaving orphaned sprites visible
in subsequent battles.
Track all created GameObjects and destroy them in OnDestroy().
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When rewinding a game and replaying to the same battle, the Shardok
server returned cached results instead of fighting fresh. The C++ server
kept completed game controllers in runningControllers indefinitely, so
ControllerForGame returned the old controller and SubscribeToGame
immediately sent back game_over_response with stale results.
Fix: detect rewind by checking for an existing game + NewGameRequest +
knownResultCount == 0, then replace the old controller with a fresh one.
Also cancel pending Eagle-side battle subscriptions on rewind.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Quest creation only checked if neighbors were owned by a different faction,
ignoring alliances. Fulfillment logic counted allied neighbors as secured.
This mismatch caused quests to be offered and immediately fulfilled. Now
creation also checks for alliances, matching the fulfillment logic.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Take the lower of the Inspector-configured count and the actual beast
count from BeastsEvent.Count. Effect classes now initialize in Start()
instead of Awake() so SpawnBeastsEffect can cap the count field after
Instantiate but before creatures are spawned.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
RoundPhaseConverter maps Unrecognized(13|14|15) to UncontestedConquest
instead of throwing, allowing saved games from removed FFA phases to
load. CommandTypeConverter maps Unrecognized(12) to None.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Remove Free-For-All battles, replace with sequential AttackDecision
When multiple hostile factions march on the same province, instead of a
separate FFA decision/battle phase, the AttackDecision phase now handles
them sequentially: armies sorted by troop count (largest first), only
the largest undecided army gets the command. After someone advances,
allied armies can join while hostile armies are auto-bounced (occupied)
or can contest (unoccupied).
Phase flow: HostileArmySetup → UncontestedConquest → AttackDecision →
DefenseDecision → Battle (removed FreeForAllDecision,
FreeForAllBattleRequest, FreeForAllBattleResolution phases).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove FFA protos, simplify unoccupied province attack logic
- Remove FFA proto fields from available_command, selected_command,
command_type, round_phase, and army protos (mark as reserved)
- Remove corresponding converter match arms (ArmyConverter,
SelectedCommandConverter, CommandTypeConverter, RoundPhaseConverter)
- Simplify isMyTurnToDecide: hostile armies are always blocked when
another army has advanced (no more unoccupied province special case)
- Replace preBattleConquestResults with uncontestedConquestResults:
handles single/allied/hostile armies on unoccupied provinces with
conquest + bounce of hostile AwaitingDecision armies
- Add 6 tests covering tiebreak, hostile blocking, allied joining,
auto-bounce, and uncontested conquest scenarios
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove FFA references from Unity C# client
Delete FreeForAllDecisionCommandSelector and its .meta file,
remove FFA tutorial entry from TutorialContentDefinitions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix alliance check and simplify uncontested conquest logic
Fix exists→forall bug in AvailableAttackDecisionCommandFactory: armies
must be allied with ALL advancing armies, not just any one. Move all
unoccupied province conquest into UncontestedConquest phase (largest army
always wins) and remove redundant logic from EndAttackDecisionPhaseAction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Improve army tiebreaking: troop count, then power, then pseudo-random
When choosing the "largest army" (for attack decision ordering and
uncontested conquest), tiebreak by total unit power after troop count,
then by factionId XOR randomSeed for a pseudo-random final tiebreak
that varies across games instead of always favoring lower factionIds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Materialize LINQ chain to avoid multiple enumeration in
HandleOverlays, simplify redundant return in HandleLongHover,
and replace always-matching UnitId type pattern with var.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a non-defender attacker lost a multi-faction battle, their surviving
units were silently dropped. ProvinceConqueredAction only processed the
winner's and defender's units, so a third-party loser's Normal units were
not returned (unitReturned=false for Loss), not captured (not a defender),
and not outlawed — they just vanished. Fixed by changing the filter from
defenders-only to all non-winning players.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The AllyVictory check in PopulateGameOverResponse compared the current
player's own ID (shardokPid) against their own allies list — which is
always false since a player is never listed as their own ally. Fixed by
using the lambda's `pid` parameter (the winning player's ID, previously
unused) instead of `shardokPid`.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GamesManager.leaderCache looked up faction heads with
gameState.heroes(faction.factionHeadId), which throws
NoSuchElementException if the hero died in battle and was moved to
killedHeroes. Use getOrElse fallback to killedHeroes, matching the
pattern used elsewhere (HeroInfoUtilities, ChronicleEventTextGenerator).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When an army retreats from battle, its return destination is set at march
time. If that province is conquered while the army is fighting, the
retreating army arrives at a now-hostile province, causing a "Mismatched
factionIds" validation error. Fix by detecting these stranded armies in
PerformProvinceMoveResolutionAction and shattering them (heroes become
outlaws) instead of letting them fall through to hostile army processing.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When reconnecting due to idle timeout, deadline exceeded, or normal
stream end, keep the displayed connection status unchanged instead of
briefly showing "Connecting..." or "Retry in Xs".
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The two animalPrefabs entries had their fileIDs swapped between the
scorpion_prefab and yellow_fattail_scorpion_prefab GUIDs, causing
MissingReferenceException at runtime.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Matches the existing Trade command selector behavior where food/gold
amounts turn red when they exceed the province's storage cap.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Two-stage actions from the opponent were playing the attempt sound
sequenced correctly but only one animation immediately. The attempt and
result animations were not sequenced.
Fear and Duel exposed this most visibly since they have distinct result
animations (FearSuccess/FearResisted, Duel/DuelDeclined), but the
pattern was wrong for all two-stage actions.
Replace the separate PlayTwoStageSoundSequence + PlayAnimation calls with
a unified PlayTwoStageSequence coroutine that sequences both animations
and sounds: attempt animation+sound → wait → result animation+sound.
Add AttemptAnimationType() to map result animation types back to their
attempt type (FearSuccess/FearResisted → Fear, Duel/DuelDeclined →
DuelChallenge). For actions where attempt and result use the same
animation, the result animation phase is skipped.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Decouple non-Mac builds from Xcode via BAZEL_NO_APPLE_CPP_TOOLCHAIN
Set BAZEL_NO_APPLE_CPP_TOOLCHAIN=1 in .bazelrc so apple_support skips
Xcode detection (xcode-locator) entirely for Scala/C++/Go builds.
Only mactools builds (Mac/Sparkle) re-enable the Apple CC toolchain.
This means Xcode version changes no longer affect Eagle, Shardok, test,
auth, docker, or Unity Windows builds — no expunge, no cache invalidation,
no rebuild. The sync script and its expunge/cache-key logic are now scoped
to mactools only and removed from all non-Mac CI workflows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert BAZEL_NO_APPLE_CPP_TOOLCHAIN; keep Apple CC with stable environ
BAZEL_NO_APPLE_CPP_TOOLCHAIN=1 broke builds because apple_support
registers platforms (e.g. macos_x86_64) that require Apple CC toolchains
for resolution — even in non-Mac builds.
Instead, keep Apple CC enabled but ensure its repo rule never
re-evaluates on non-Mac builds by keeping DEVELOPER_DIR pinned in
common:macos. The sync script only runs for mactools builds and
overrides DEVELOPER_DIR there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add actions/cache for .git/lfs in all workflows that fetch LFS objects
(unity_build, mac_build, ios_testflight, ios_addressables_build).
The cache key is based on .gitattributes so it updates when LFS tracking
rules change. Subsequent runs restore from cache and git lfs pull only
fetches new/changed objects, avoiding re-downloading the full 6+ GB.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Match the version that Gazelle's MVS already resolved transitively,
silencing the "implicitly updated to higher versions" debug warning.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Tag shardok_mcts_ai_basic_test as "manual" so it's skipped by
wildcard test patterns. MCTS is not in active use and this test
takes ~75s. It can still be run explicitly when needed.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The grpc 1.78.0 upgrade eliminated the rules_swift version conflict
that required SparklePlugin to live in a separate Bazel workspace.
Move it back into the main workspace under src/main/objc/.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add giant beast type using 3x-scaled peasant model
Reuse Polytope peasant prefabs at 3x scale (animalScale 45) with a single
wandering giant per province. Uses HumanMapAnims controller for idle/walk/
attack animations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix giant test method to use correct province name (Elekes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up GiantEffect prefab in Gameplay scene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Bazel Xcode version caching causing CI build failures
When Xcode updates on self-hosted runners, Bazel's cached
local_config_xcode still references the old version, causing
"xcode-locator <old_version> failed" errors.
Add sync_bazel_xcode.sh which detects Xcode version changes
and runs `bazel sync --configure` to refresh the config without
nuking the build cache. Added to all 8 CI workflows that use Bazel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use rm of cached repo instead of bazel sync --configure
bazel sync --configure requires WORKSPACE mode and fails with
bzlmod. Instead, delete the stale local_config_xcode from the
output base directly — the next bazel build regenerates it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use bazel clean --expunge instead of targeted repo deletion
Deleting just local_config_xcode is insufficient — the stale Xcode
version is also embedded in cached actions and other toolchain repos.
The only reliable fix is a full expunge. Since Xcode updates are
infrequent, the one-time rebuild cost is acceptable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use action_env to bake Xcode version into cache keys
Instead of expunging the cache (which doesn't help with the remote
cache anyway), write the Xcode build version to .bazelrc.xcode via
--action_env. This makes the version part of every action's cache
key, so stale entries in both local and remote caches are naturally
ignored after an Xcode update — no deletion needed anywhere.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add expunge back alongside action_env for belt-and-suspenders fix
action_env alone isn't enough — local_config_xcode is a cached
repository rule that bakes in the old Xcode version before any
actions run. Need expunge to clear the local toolchain config,
plus action_env to prevent stale remote cache hits afterward.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add Xcode sync to mac_build build-and-sign job
The sync step was only in the deploy job, but build-and-sign calls
bazel via build_protos.sh before that. This runs on the unity-mac
runner which also has the stale Xcode cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
It uses the Butcher 2D sprite via ButcherBeastNames HashSet,
but was missing from the documentation table.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
grpc 1.78.0 dropped its rules_swift dependency, resolving the version
conflict (rules_swift 3.x vs 2.x) that forced SparklePlugin into a
separate workspace. This removes the rules_swift single_version_override.
Includes a patch for a known BCR bug (grpc/grpc#41438) where grpc 1.78.0
requests Python 3.14.0b2 but rules_python >= 1.6.0 replaced it with 3.14.
The patch backports the upstream fix (commit 459279b).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace the 2D PSD-based DragonRed sprite with a 3D FBX model from 3dFoin
Dragon. The dragon now alternates between flying (circling at altitude with
fly/flyAttack/flyBreathFire/flyFast/flyHit actions) and grounded (wandering
with walk/run/attack/breathFire/whipTail/hit/stand/idle actions) modes.
- Add DragonMapAnims.controller with 15 animation states
- Configure dragon_skin.FBX for Generic animation with avatar
- Rewrite DragonEffect.cs with 4-mode state machine
- Delete old 2D DungeonMonsters2D dragon assets
- Add Dragon/ to LFS rules and clang-format excludes
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add 5 Polytope character archetypes for human beast animations
Replace the single Butcher sprite for all 34 human-type beasts with
5 thematic 3D archetypes using Polytope Studio Lowpoly Medieval
Characters: Knight (6 beasts), Soldier (8), Archer (3), Militia (8),
and Peasant (9). All share a HumanMapAnims controller that retargets
Clown animation clips via Unity's Humanoid system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Rename humanEffectPrefab to butcherEffectPrefab; route psychopath to it
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move butcherEffectPrefab under Human Effect Prefabs header
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up Polytope prefabs in scene; rename HumanEffect to ButcherEffect
Wire all 6 human effect prefabs (butcher, knight, soldier, archer,
militia, peasant) in Gameplay.unity scene. Rename HumanEffect.prefab
to ButcherEffect.prefab to match the field rename.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Exclude third-party asset packs from clang-format hook
Skip DungeonMonsters2D, Polytope Studio, RRFreelance-Characters,
and Raccoon directories — these are vendor code that should not
be reformatted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* LFS-track all files under third-party asset pack directories
Track all files under DungeonMonsters2D/, Raccoon/, Polytope Studio/,
and RRFreelance-Characters/ via Git LFS. These are imported third-party
assets that are never manually edited.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Convert DungeonMonsters2D and Raccoon assets to Git LFS
Re-add all files under DungeonMonsters2D/ and Raccoon/ through
the LFS filter so they're stored as LFS objects going forward.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The warmup-binary artifacts from docker_build were accumulating and
exceeding the 500 MB storage threshold. GitHub's retention-days cleanup
wasn't keeping up with deploy frequency. Add explicit artifact deletion
after deploy, matching what mac_build already does.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add raccoon beast animation using Sketchfab 3D model
Adds Raccoon.fbx (WildMesh 3D, CC BY 4.0) with textures, wires up
raccoonEffectPrefab in ProvinceBeastsController, and updates both
attributions.json files and BEAST_EFFECTS.md.
Editor steps still needed: create RaccoonMapAnims controller, create
RaccoonEffect prefab (duplicate HoneyBadgerEffect), wire in Gameplay.unity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up raccoon animation with Blender-converted FBX
Convert GLB to FBX via Blender (original FBX had no animation data).
Add RaccoonMapAnims controller, RaccoonEffect prefab, materials with
textures, scene wiring, and fix province name (Hella, ID 19).
Known issue: gray sphere artifact under each raccoon.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove Icosphere/hair artifacts from raccoon FBX and update scene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Smooth 3D animal turning with Quaternion.Slerp
Animals now rotate smoothly toward their target instead of snapping.
UpdateFacing is called every frame during walking, and uses Slerp
with a configurable turnSpeed (default 5) for gradual rotation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates the duplicate attributions.json by making the Unity copy
a symlink to src/main/resources/net/eagle0/attributions.json, ensuring
a single source of truth that can never diverge.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add honey badger 3D model and wire up AnimalEffect
Import HoneyBadger FBX (63 animations) and texture from Sketchfab.
Add honeyBadgerEffectPrefab field and switch case in
ProvinceBeastsController. Still needs Unity Editor setup: Animator
Controller, effect prefab, and scene wiring.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix honey badger test province name (ID 1 is Shumal, not Onmaa)
Onmaa is province ID 14, already used by TestWildPig.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up HoneyBadgerEffect prefab and animator controller
- HoneyBadgerMapAnims controller with idle/walk/eat/attack states
mapped to HoneyBadger FBX clips
- HoneyBadgerEffect prefab with AnimalEffect referencing the model
- Wired into Gameplay.unity ProvinceBeastsController
- Includes Unity-generated .meta files for imported assets
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix HoneyBadger FBX avatar setup to enable animations
avatarSetup: 0 (No Avatar) prevented the Animator from driving the
skeleton. Changed to avatarSetup: 1 (Create From This Model) to match
how the Animal Pack Deluxe models are configured.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add attribution for recently added Unity asset packs
Add Animal pack deluxe (janpec), 2D Monster Pack Basic Bundle/PSB
(SP1/DungeonMonsters2D), and Honey Badger 3D model (WildMesh 3D) to
ASSET_AUDIT.md. Update 3D model count in summary table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add 3D model attributions to in-app/website credits
Add Animal pack deluxe, 2D Monster Pack, and Honey Badger
to both server and Unity client attributions.json files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Prevents Claude from combining bash commands with &&, ||, or ;.
Each command must be a separate tool call, which makes permission
rules work correctly per-command.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add clown beast animation using both Clown Pack models
- Add per-prefab animator controller overrides to AnimalEffect
(animatorControllers[] array, indexed to match animalPrefabs[])
- Create ClownMapAnims and FatClownMapAnims controllers mapping
idle/walk/eat/attack states to each model's animation clips
- Create ClownEffect prefab using both Clown and Fat Clown models
- Add clown case to ProvinceBeastsController, remove from HumanBeastNames
- Wire up in Gameplay.unity scene
- Update BEAST_EFFECTS.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix ClownEffect prefab (recreated via Unity editor)
The hand-written prefab YAML was rejected by Unity's PrefabImporter.
Recreated by duplicating the working OgreEffect prefab in the editor
and configuring the AnimalEffect fields there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Update BEAST_EFFECTS.md: ogre/troll now have custom animation
Reflects the addition of the Orc-Ogre pack AnimalEffect for ogre and
troll beast types (PR #6287). Removes them from the "no custom
animation" list and adds them to the custom animations table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add complete beast coverage details to BEAST_EFFECTS.md
- Add missing animal effect entries (crab, frog, rabbit, salamander,
scorpion, stag, wild goat, wild pig) to custom animations table
- Add human-type beast breakdown by likelihood with thematic clusters
for potential visual differentiation beyond the generic Butcher prefab
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace unsafe asInstanceOf casts with pattern matching via helper
methods. This is more idiomatic Scala and will give clearer error
messages if the map contains unexpected types.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Fixes two issues with beast balance:
1. Demons and vampires were impossible to defeat - reduced maxCountMultiplier
from 0.5 to 0.15 so an elite battalion (1000 troops, 100/100 stats) can
just barely defeat them.
2. Clowns had a 39% chance of being deadlier than demons due to their huge
power variance (0.8-50). Added a powerExponent field to beasts.tsv that
controls how the random roll translates to power. Default is 4 (quartic),
clowns use 20 which reduces the chance of deadly clowns to ~10%.
The casualties formula is: casualties = beastsPower² / totalPower
Where beastsPower = count × relativePower
This means overwhelming force results in minimal casualties (intended),
but also means high beast power causes quadratically more damage.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ProvinceWasSelected accessed Model.AvailableCommandsByProvince and
Model.Provinces without checking if Model was null. This caused a
crash when the user clicked a province before the game model was
received from the server on first load.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Add animatorController override field to AnimalEffect so it can swap
the prefab's controller at runtime (needed because the Orc-Ogre pack
uses state names like Ogre_Idle1 instead of the expected idle/walk/etc)
- Create OgreMapAnims.controller with idle/walk/eat/attack states mapped
to Orc-Ogre FBX animation clips
- Create OgreEffect.prefab using AnimalEffect + controller override
- Add ogre/troll cases to ProvinceBeastsController.GetEffectPrefab()
- Wire OgreEffect in Gameplay.unity scene
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up animations for 8 new beast types
Add effect prefabs and scene wiring for crab, frog, rabbit,
salamander, scorpion, stag, wild goat, and wild pig using
Animal Pack Deluxe 3D models with AnimalEffect controller.
Multi-variant prefabs: frog (3), scorpion (2), rabbit (2),
stag (2), wild goat (2), wild pig (2).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix ContextMenu test labels to use real province names
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix animal effect prefab references to use root GameObjects
Each Animal Pack prefab contains many child objects (bones, meshes, etc).
The effect prefabs were incorrectly referencing the first child object
instead of the root prefab object (the one with m_Father: {fileID: 0}).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Tune animal effect scales per beast type
Adjusted animalScale values from the Unity editor after visual testing:
- Frog, salamander, scorpion, crab: 50 (small creatures need more zoom)
- Rabbit, stag, wild goat, wild pig: 20 (medium animals)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Order beasts without custom animations by how commonly they appear,
making it easier to prioritize which to add next.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Document all three effect types (MonsterEffect, AnimalEffect,
DragonEffect), list all beasts with custom animations vs vulture
fallbacks, and add 3D rendering tips.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add animal wandering effect for bear, boar, crocodile, snake, wolf
New AnimalEffect.cs drives Animal Pack Deluxe 3D prefabs using their
lowercase state names (idle, walk, run, eat, attack) and Y-rotation
for facing direction. Wolverine reuses the wolf prefab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add animal effect prefabs and scene wiring for bear, boar, crocodile, snake, wolf
Wire AnimalEffect prefabs to ProvinceBeastsController and include the
Animal Pack Deluxe asset pack. RatEffect now has both MonsterEffect
(2D sprites) and AnimalEffect (3D model) for a mixed group.
Add LFS tracking for .tga and .fbx files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix animal facing direction to use full angle rotation
3D models need to rotate Y by the actual movement angle, not just
0/180 degrees. Use atan2 of the XY direction vector so animals face
their actual travel direction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix animal rendering and remove die animation
Use sortingOrder=103 on 3D mesh renderers instead of Z offset hack
to render animals above the map. Guard against die animation state.
Default scale to 15. Remove unused run state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Disable depth test on animal renderers to prevent map clipping
When 3D animals rotate to face away, their geometry extends into
positive Z and clips behind the map plane. Setting ZTest=Always
ensures they always render on top regardless of orientation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix animal depth clipping with renderQueue and ZWrite/ZTest overrides
Use renderQueue=4000 (Overlay), disable ZWrite, and set ZTest=Always
via SetFloat (compatible with more shader types) to prevent 3D animal
meshes from clipping behind the map when rotated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The first hero departure notification in the tutorial has no narrative
context. Old Marek now comments when a non-player faction loses a hero,
noting that Bregos has always inspired loyalty and something deeper must
be at work after the upheaval with Tarn.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add wandering monster animations for beast events on Eagle map
Replace generic vulture effects with animated DungeonMonsters2D monsters
for skeleton, zombie, rat, spider, demon, orc, and human-type beasts.
Each MonsterEffect spawns a group of 3 monsters that wander randomly,
idle, and occasionally attack within a province.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add ContextMenu test actions for monster effects
MonsterEffect: Test All Idle/Move/Attack/SpecialAttack
ProvinceBeastsController: Test spawn for each beast type in Motcia
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Spread test beast spawns across different provinces
Also add "Test All Beast Types" to spawn all 8 at once.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix monster sprite facing direction
DungeonMonsters2D prefabs default to facing left, so invert the
X scale flip logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove malformed AnimationEvents from SkeletonArcher anims
The asset pack shipped with empty functionName fields on these events,
causing "AnimationEvent has no function name specified" errors at runtime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire humanEffectPrefab in Gameplay scene
The HumanEffect prefab was created but never assigned to the
ProvinceBeastsController Inspector field, so human-type beasts
fell through to generic vultures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add vampire beast type and remove unused DungeonMonsters2D assets
- Add VampireEffect prefab, code mapping, scene wiring, and test entry
- Remove Bat, Ghost, GhostKnight, and four Golem variants (animations,
characters, PSBs) that aren't mapped to any beast type
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove DragonEffect component from monster effect prefabs
These were created by duplicating the dragon prefab, which left a
DragonEffect component that spawned an extra dragon alongside the
intended monsters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Update Eagle appearance trigger for Hedrick and Sadar expansion
Replace Bridget (faction 4) with Hedrick (faction 5) as the tutorial
antagonist, and add a second trigger path: Sadar controlling 4+ provinces.
The Eagle now appears when either Hedrick is eliminated OR the player
faction grows strong enough to warrant attention.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Lower weak King province hero loyalty to 20
Makes the heroes in the King's weak provinces much more likely to
defect, as they barely have any loyalty to the King.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Adjust weak King province hero loyalty to 25
Loyalty 20 was causing heroes to depart too quickly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Horseshoe prints were facing forward (open end leading) instead of
trailing behind the horse. Added 180° rotation for mounted units.
Duel accepted animation was never playing because DuelAccepted wasn't
matched as a result of the pending DuelChallenge command. Added it to
IsResultVariant and cached source/target grid indices in
PendingCommandSound so the animation plays even when units have been
removed from the model by later duel combat results in the same batch.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tutorial-specific initial chronicle text
The tutorial was using the same initial chronicle text ID as regular
games, but the text content references "The Eagle" character which
doesn't exist in the tutorial scenario. When the LLM tries to generate
a chronicle update, it looks up the previous chronicle entry's text
as a dependency, causing "Unknown dependency for LLM request" errors.
This adds a tutorial-specific chronicle text that describes the
actual tutorial scenario: Sadar Rakon defending Onmaa against
King Bregos Fyar's army led by Ikhaan Tarn.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update tutorial chronicle text wording
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Register SupportField dynamically for tutorial dialogue highlight
The SupportField highlight in the tutorial_rebuild_support and
tutorial_support_deadline dialogues wasn't appearing because the
static Inspector reference on TutorialTargetRegistry was not wired up.
Dynamically register the field from EagleGameController, matching the
pattern used for GoToBattleButton and CommandPanel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Elena's reinforcement dialogue referencing Tarn incorrectly
Tarn and the King are still allies at this point, so "half of them
had already fled before Tarn's men even arrived" doesn't make sense.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Vary Ranil's siege dialogue to avoid repeating Marek's phrasing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix highlight not rendering inside layout groups
The dialogue highlight was invisible when targeting UI elements inside
a layout group (e.g. SupportField). The highlight container was being
treated as a layout child, which collapsed its rendering. Adding
LayoutElement with ignoreLayout=true makes it overlay correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make Reset Tutorials also reset dialogue and trigger state
ResetAllProgress previously only cleared the old tutorial system's
PlayerPrefs state. Now it also clears DialogueManager's completed
script IDs and TutorialTriggerRegistry's one-shot flags, so all
narrative dialogues can replay without restarting the game.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix NRE: add RectTransform before LayoutElement
LayoutElement is a UI component that auto-creates a RectTransform,
so the subsequent AddComponent<RectTransform>() returned null.
Reorder so RectTransform is added first.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The conditional `if po.support > 0` prevented explicitly setting
support to 0. Now support always uses the JSON value directly.
Provinces 37 and 40 already have their support values specified.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove the special case that allowed Ikhaan Tarn to flee.
Now all units in the tutorial battle must fight to the end.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When we added Hedrick as an independent faction (ID 5), we didn't
add it to the aiFids list in createTutorialGameForUser. This caused
the game to get stuck waiting for commands from a faction that had
no AI controller.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Override duel_base_accept_odds to 200 (max roll is 100) for tutorial
games so the player's first duel challenge is always accepted,
ensuring the tutorial can teach dueling mechanics reliably.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Split reinforcement dialogue into per-hero events for staggered arrivals
Now that reinforcements arrive at different times (Elena round 5, Ranil
round 6), inspect the arriving unit's profession to fire hero-specific
dialogue triggers instead of one generic event for all reinforcements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update tutorial dialogue and add capture/duel triggers
- Update Elena Fyar's reinforcement dialogue: escaped captivity in Pieska
- Update John Ranil's reinforcement dialogue: pushed through from Nikemi
- Add enemy hero captured dialogue trigger with dynamic hero name
- Add friendly hero captured dialogue with special cases for Marek/Sadar
- Prioritize duel tutorial over melee when target is not Ikhaan Tarn
- Add GetScriptForTrigger to DialogueManager for dynamic text injection
- Cache unit names for capture detection (units removed before processing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix capture detection to scan removed units instead of action type
CapturedWhileFleeing only fires for flee-based captures. Heroes also
leave the battle via duels, destruction, etc. Now scans every action
result's GameStateViewDiff.RemovedUnits for hero units in the name
cache, catching all forms of hero removal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Make Ranil reinforce from Tarn's direction
Elena continues to reinforce from position 6, but John Ranil
now enters the battle from the same starting position as Tarn
and the other attackers.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delay Ranil's arrival to round 7
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Reduce Sadar's unit sizes by 10%
- Rakon's Loyalists: 511 → 460
- Onmaa Defenders: 300 → 270
- Hunters of the Steppe: 384 → 346
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor beast prefab selection to switch and add extensibility doc
Replace IsDragon boolean check with GetEffectPrefab switch on beast
name string, providing a clear extension point for adding new
beast-type-specific effects. Add docs/BEAST_EFFECTS.md explaining
the architecture and step-by-step process for adding new beast types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use exact beast name matching from beasts.tsv
Switch from case-insensitive substring matching to exact string
matching on the canonical singularName from beasts.tsv. Update doc
with beasts.tsv reference and candidates for custom effects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add dragon beast animation on eagle map
When a province has a dragon beast event, show an animated 2D dragon
(DungeonMonsters2D DragonRed) circling the province instead of generic
vultures. The dragon plays its Move animation while orbiting and
periodically triggers Attack with fire rain particles falling below.
- New DragonEffect.cs: instantiates animated dragon prefab, drives
circular orbit with vertical bobbing, flips sprite to face travel
direction, emits fire rain particle bursts on attack
- ProvinceBeastsController: extracts beast name from BeastsEvent,
selects dragonEffectPrefab when name contains "dragon"
- Add DragonRed from DungeonMonsters2D asset pack (dragon-only files)
- Add .psb and .psb.meta to Git LFS tracking in .gitattributes
- Add 2D PSD Importer package dependency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix dragon animation and improve fire breath effect
Add com.unity.2d.animation package required for SpriteSkin bone
deformation at runtime. Rewrite fire breath as directional cone aimed
at province center using stretch render mode. Continuously enforce Move
animation state to prevent controller defaulting to Idle. Add context
menu test helpers for spawning dragon effects in editor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add animation variety and disable fire breath for now
Dragon now cycles through Move, Jump, Attack, and SpecialAttack
animations with weighted random selection. Fix IsInTransition check
so action animations play fully before returning to Move. Update
defaults to match tuned Inspector values. Disable fire breath
particle system pending direction/sizing fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add Hedrick faction in Kojaria, move Bridget to Alah
- Move Bridget's faction from Kojaria (39) to Alah (18)
- Create new faction for Hedrick the Hedge-Merchant in Kojaria (39)
with 300 light infantry, 23 support, same development as Onmaa
- Remove Hedrick from battle reinforcements (now only Elena and
John Ranil arrive as reinforcements at rounds 6 and 7)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix reinforcement hero IDs and adjust timing
- Update TutorialReinforcementHeroIds to Set(100, 101) to match
the two remaining reinforcement heroes (John Ranil and Elena)
- Change reinforcement timing: Elena arrives after round 5,
John Ranil arrives after round 6
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ExecuteReinforcementsAction was placing ALL PENDING_REINFORCEMENT
units when any reinforcement event fired, instead of only the units
belonging to that specific event. Match pending units by eagle_hero_id
against the event's CommonUnit list so each event only activates its
own reinforcement.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The updateWith call was only setting gold and food, ignoring the
support, economy, agriculture, and infrastructure values from the
tutorial_parameters.json configuration. This caused Onmaa to have
default values (0 support, 20/20/20 development) instead of the
configured values (23 support, 45/41/42 development).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Set up 12 King's provinces with single noProfession heroes, no troops,
no resources, and low loyalty to portray the realm falling apart:
- East Faluria, Regigia, Tegrot, Berkorszag, Oscasland, Wichel,
Oriyslia, Mesh, Musland, Motcia: loyalty 50-55
- Nikemi, Pieska: loyalty ~40 (even weaker)
Also refactored the attacking army (Ikhaan Tarn's force) to be
created directly in TutorialGameCreation.scala rather than read
from JSON, since the army is already in transit when the game
starts - it's not actually stationed in Nikemi.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add fear success and fear resisted animations
Fear success: dark cloud engulfs the target, pulses ominously, then
slowly dissipates. Fear resisted: a white flash at the target deflects
the wave back toward the source where it shrinks and fades.
Split Fear into three animation types (Fear attempt, FearSuccess,
FearResisted) so the server result triggers a distinct visual. The
pending command handler now also plays result animations, not just
sounds. Rename IsFailureVariant to IsResultVariant to handle both
success and failure result matching.
Add test buttons for Fear Success and Fear Resisted in the animation
test controller.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refactor test controller to use real sound+animation flow and fix fear attempt sound
Route most animation tests through TestPlayAnimationAndSound to exercise
the real two-stage sound pipeline. Consolidate duplicate PlayAnimation
methods and remove ?? fear fallback that caused double-play.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix fear sound routing and remove test debug logging
Add FearSuccess/FearResisted to ActionTypeForSound so they play the
correct result sounds. Pass isOwnCommand=true in test path so Fear
exercises the two-stage attempt sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Improve fear success and fear resisted animations
Fear success: tendrils start 2x farther out with ease-in spiral, add
ghost skull at 40% opacity filling the hex. Fear resisted: shield sprite
pops up and bashes the skull back toward the source with spin and flash.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Cut defending unit sizes by 25% (681→511, 400→300, 512→384)
- Cut reinforcement unit sizes by 25% with battle-worn variation
(450→443, 375→382, 450→446)
- Double food in Onmaa (2000→4000) to support larger army
- Set Onmaa development stats: 45 economy, 41 agriculture, 42 infrastructure
Reinforcements already arrive one at a time (rounds 5, 6, 7).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Tutorial game creation was passing an empty pregeneratedHeroes pool to
HeroGenerator, causing a crash when PerformUnaffiliatedHeroesAction
tried to spawn new heroes in provinces (e.g., when building support in
Onmaa).
Changed to use GameParametersUtils.pregeneratedOthers as the pool,
excluding hero names already used in the tutorial to avoid duplicates.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add duel animations and programmatic test buttons
Add gauntlet throw animation for duel challenges (arcing projectile with
dust landing effect) and duel declined animation (dismissive sword sweep).
Wire duel challenge as a two-stage sound so the challenge clip plays on
command and accepted/declined plays on server response.
Refactor AnimationTestController to generate buttons programmatically
from a prefab instead of requiring manual wiring per animation. Remove
redundant private animator fields in favor of direct gameController
access.
Remove black background from Iron-Etched-Gloves sprite for use as the
gauntlet throw projectile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make gauntlet throw more aggressive and play sound at impact
Change gauntlet animation from a gentle arc to a violent downward slam
from above the source hex to the lower half of the target hex with
cubic ease-in for an accelerating impact feel.
Move challenge sound into the animator so it plays at impact rather
than when the command is issued. The two-stage system still tracks the
pending command for the accepted/declined result sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Change deploy conditions from event_name == 'push' to
event_name != 'pull_request', matching the pattern already used by
the Upload Addressables step. This allows workflow_dispatch to
trigger a full deploy, which is needed for re-deploying without
a code change.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Migrate client update system to SHA-addressed blob storage
Upload game files and installer to blobs/<sha256> with immutable cache
headers, serve manifests with Cache-Control: no-cache so CDN always
revalidates. This eliminates the need for CDN cache purges after builds.
During the transition period, files are also uploaded to their legacy
paths (unity3d/win/<path> and installer/Eagle0.exe) so existing
installers continue to work until users auto-update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add blob cleanup workflow and fix installer manifest URL
- Add daily scheduled GitHub Action to garbage-collect unreferenced blobs
- Fix installer_build.yml manifest step to use blobs/<sha> URL instead
of the legacy installer/Eagle0.exe path
- Add installer_sha256 to manifest for blob cleanup tool
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert installer blob changes — installer staleness is benign
The installer self-updates on next launch, so CDN staleness for
installer/Eagle0.exe is harmless. Keep blob addressing only for
game files where manifest/file mismatch causes download failures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use manifest comparison instead of HEAD requests for blob dedup
Fetch the previous manifest to determine which SHAs already exist as
blobs, avoiding N HEAD requests per build. Also only upload to legacy
unity3d/win/ paths when the file at that path actually changed, matching
the old behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove legacy unity3d/win/ uploads — installer self-updates first
Old installers fetch the manifest, see a new installer version, and
self-update before downloading game files. The new installer fetches
from blobs/<sha>, so there's no need to keep uploading to the legacy
unity3d/win/<path> paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Set no-cache on installer and whats-new.json uploads
Both are mutable files at fixed CDN URLs that were relying on default
cache behavior. Setting no-cache ensures the CDN always revalidates,
so users get the latest installer on first download and see current
changelog entries immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add unit tests for the Eagle strategic tutorial event system:
- Game type tests: verify only Tutorial games trigger events
- Eagle appearance negative cases: events don't fire when:
- Bridget still has provinces
- Eagle faction already exists
- Idempotence tests: verify no re-trigger after Eagle appears
Also updates visibility in BUILD files to allow test access.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Tests verify the tutorial config structure including:
- 3 reinforcement events with correct IDs
- Round triggers at 5, 6, 7 for Hedrick, Elena, John Ranil
- Correct hero IDs in reinforcement units
- Tarn visibility configured for defender
- Reinforcement heroes and battalions in game state
Also fixes :engine BUILD target to export game_history and
battle_resolution which appear in the Engine interface.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Battalion backstory updates were never generated - only initial backstories
are created. This removes the unused code:
- BattalionBackstoryUpdateAction.scala
- BattalionBackstoryUpdatePromptGenerator.scala
- BattalionBackstoryUpdateRequest from LlmRequestT sealed trait
- BattalionBackstoryUpdateRequest proto message (field 41 reserved)
- Related cases in LlmResolver and proto converter
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a province is conquered, the new owner can now see the backstory
text of unaffiliated heroes in that province. This matches the behavior
of PerformReconResolutionAction which extends backstory visibility for
ruling heroes to the faction performing recon.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The admin server was fetching whats-new.json from the public CDN URL
(assets.eagle0.net) which caches aggressively. After adding an entry,
the page reload would fetch the stale cached version, making it look
like the entry wasn't saved. Read directly from S3 so the admin server
sees its own writes immediately.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Detect NewRoundAction with month=1 after the tutorial battle has ended.
Old Marek explains what taxes provide (gold and food) and advises on
how to spend them wisely.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The GraphQL layer was a workaround for a token permissions issue that
has since been fixed. The REST /git/refs endpoint works correctly with
fine-grained PATs when contents:write is properly configured. This
removes ~90 lines of GraphQL code.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Right-click (or long-press on mobile) is a well-known RTS convention that
eliminates confusion from having two melee input methods. Left-click remains
the default action (move/archery); right-click triggers melee on adjacent
enemies.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 06:46:49 -08:00
7538 changed files with 644994 additions and 830600 deletions
**NEVER chain bash commands.** Do not use `&&`, `||`, or `;` to combine commands. Each command must be a separate Bash tool call. Use parallel tool calls when commands are independent.
## CRITICAL GIT RULES (NEVER VIOLATE)
**NEVER use `git -C`.** Instead, cd to the repo root and run git commands from there.
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
**NEVER merge PRs.** You create PRs. The user merges them. No exceptions.
# This switches Unity to iOS target and builds addressables for CDN upload
set -euxo pipefail
# Read Unity version from project file
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
# Use runner-specific build directory if EAGLE0_BUILD_DIR is set, otherwise default
When a province has a `BeastsEvent`, `ProvinceBeastsController` spawns a visual effect at the province centroid. By default this is the generic `BeastsEffect` (circling vultures/crows). Beast-specific effects can override this based on `BeastInfo.SingularName`.
| `Assets/Eagle/DragonEffect.cs` | Dragon-specific effect (3D model with flying/ground modes) |
| `Assets/Eagle/MonsterEffect.cs` | Wandering 2D monster group (DungeonMonsters2D prefabs) |
| `Assets/Eagle/AnimalEffect.cs` | Wandering 3D animal group (Animal Pack Deluxe prefabs) |
| `Assets/Eagle/Effects/` | All effect prefabs |
## Adding a new beast type
### 1. Create the effect MonoBehaviour (or reuse an existing one)
Three effect scripts exist for different asset types:
**MonsterEffect** (2D sprites, DungeonMonsters2D): Spawns a group of wandering 2D monsters. Uses PascalCase animator states (`Idle`, `Move`) and triggers (`AttackTrigger`, `SpecialATrigger`). Flips sprite X scale for facing direction. Best for DungeonMonsters2D prefabs.
**AnimalEffect** (3D models, Animal Pack Deluxe): Spawns a group of wandering 3D animals. Uses lowercase animator states (`idle`, `walk`, `eat`, `attack`) with `Animator.Play()`. Rotates model on Y axis for facing. Sets `sortingOrder=103`, `renderQueue=4000`, and `ZTest=Always` to render above the map. Best for Animal Pack Deluxe prefabs.
**DragonEffect** (single animated 3D creature): Spawns a 3D dragon that alternates between flying (circling at altitude with fly actions) and grounded (wandering with ground actions) modes. Uses `Animator.Play()` with DragonMapAnims controller. Best for large creatures with flight capabilities.
**Particle-based** (like `BeastsEffect.cs`): Good for simple effects like swarms or atmospheric particles.
To create a new effect script, use `[RequireComponent(typeof(RectTransform))]` for UI canvas positioning and accept tuning parameters as public fields.
### 2. Create the prefab
1. Create an empty GameObject in `Assets/Eagle/Effects/`
2. Add a `RectTransform` component
3. Add your effect MonoBehaviour
4. Wire up any references (animated prefab, textures, materials)
5. Save as prefab
### 3. Register in ProvinceBeastsController
Add a prefab field and a case in `GetEffectPrefab()`:
```csharp
// In ProvinceBeastsController.cs
// Add inspector field alongside existing ones:
publicGameObjectwolfEffectPrefab;
// Add case in GetEffectPrefab(), keyed by singularName from beasts.tsv:
In `Gameplay.unity`, find the `ProvinceBeastsController` component and assign your new prefab to the new field.
### 5. Test
Use `[ContextMenu]` methods on `ProvinceBeastsController` to test:
- "Test Dragon in Motcia (ID 2)" spawns a dragon effect
- Add similar methods for your beast type
- "Clear All Effects" removes all active effects
## Beast name matching
Beast names come from `BeastInfo.SingularName` in the proto, which uses the canonical `singularName` from `src/main/resources/net/eagle0/eagle/beasts.tsv`. The switch in `GetEffectPrefab` uses exact string matching on this canonical name (e.g. `"dragon"`, `"wolf"`, `"skeleton"`).
Human-type beasts (pirates, bandits, etc.) are matched via a `HumanBeastNames` HashSet in `ProvinceBeastsController` rather than individual switch cases.
## Beast animation coverage
### Custom animations
| Beast | Effect Type | Asset Pack | Prefabs Used |
| hippogryph | DragonEffect | Heroic Fantasy Creatures Full Pack Vol 2 | Hippogriff_PBR (w/ HippogryphMapAnims controller) |
### Human-type beasts
34 human-type beast names use 3D Polytope Studio characters grouped into 5 archetypes. Each archetype uses a shared `HumanMapAnims.controller` that retargets Clown animation clips (idle, walk, attack, damage) via Unity's Humanoid system. One additional human type (psychopath) uses the 2D Butcher sprite from DungeonMonsters2D.
| Archetype | Polytope Type | Beast Names (count) |
### No custom animation (falls back to generic vultures)
Ordered by spawn likelihood (most common first):
| Beast | Likelihood | Notes |
|---|---|---|
| chimpanzee | 0.04 | Primate |
| unknown | 0.00 | Intentional fallback |
## Using low-poly animal packs (Africa Animals, Dino Pack)
Assets from these packs (by the same author) require extra setup because they ship with **Legacy** animation import and include physics components that conflict with AnimalEffect. The workflow below applies to all of them:
- Africa Animals Pack Low Poly V1 (Lion, Lioness, Black Panther, Crocodile, Elephant, Giraffe, Rhinoceros, Tiger, Zebra)
1.**Switch FBX import to Generic animation type.**
In the `.fbx.meta` file (location varies by pack):
- Change `animationType: 1` to `animationType: 2`
- Change `avatarSetup: 0` to `avatarSetup: 1` (newer format) — older metas without `avatarSetup` will auto-create an avatar at runtime
- Set `loopTime: 1` on looping clips (idle, walk, eat, run) but NOT one-shot clips (attack, death, hurt)
Unity will re-import the FBX with Mecanim-compatible clips when it next opens.
2.**Create an AnimatorController** in `Assets/Eagle/Effects/` named `<Animal>MapAnims.controller`.
Use the same 4-state pattern (idle, walk, eat, attack) as ElephantMapAnims. Reference the animation clips by their fileIDs from the FBX meta's `internalIDToNameTable` (type 74 entries) or `fileIDToRecycleName` (older format, type 7400000+). These IDs are stable across import type changes. Note: if the FBX names its idle clip `idle1`, create the controller state as `idle` but reference the `idle1` clip by fileID.
3.**Create an AnimalEffect prefab** in `Assets/Eagle/Effects/` named `<Animal>Effect.prefab`.
Reference the pack's prefab as the animal prefab, and wire up the new AnimatorController. Multiple color/sex variants from the same or similar FBX can share one controller (e.g., Lion and Lioness). AnimalEffect will automatically strip the pack's legacy Animation, Rigidbody, and Collider components at spawn time, and add an Animator if the prefab doesn't already have one.
4.**Register in ProvinceBeastsController** with a case in `GetEffectPrefab()` and a test ContextMenu method.
5.**Mark the effect prefab as Addressable** with the `beast-effects` label in the Unity editor (Window > Asset Management > Addressables > Groups).
### Notes
- The packs include both SingleTexture and TextureAtlas variants; use SingleTexture for best visual quality.
- Hippos have water-specific animations (idlewater, deathwater) that could be used for future water-province effects.
- The packs' prefabs include Rigidbody/BoxCollider components intended for gameplay use. AnimalEffect strips these automatically since it controls position directly.
### Available animals not yet set up as beasts
These animals exist in the imported packs and could be set up as in-game beasts using the workflow above:
**Africa Animals Pack V1** (already imported):
| Animal | Good fit for | Notes |
|---|---|---|
| Giraffe (x2 variants) | giraffe beast | Tall model, may need larger scale |
| Zebra | zebra beast | Herd animal; good with higher animalCount |
(Black Panther, Rhinoceros, Crocodile, Elephant, and Tiger already have effects.)
**Africa Animals Pack V2** (already imported):
| Animal | Good fit for | Notes |
|---|---|---|
| Antelope | antelope/gazelle beast | Fast-moving; good with higher moveSpeed |
(Gorilla, Hyena, Leopard, Warthog, and Hippopotamus already have effects.)
**Dino Pack Low Poly V1** (already imported):
| Animal | Good fit for | Notes |
|---|---|---|
| Brontosaurus | brontosaurus/sauropod beast | Very large; needs big scale, low count |
| Pteranodon | pteranodon/flying beast | Has fly animation; could use DragonEffect-style flight |
| Stegosaurus | stegosaurus beast | Large herbivore |
| T-Rex | tyrannosaurus beast | Iconic predator; good as a solo beast |
| Triceratops | triceratops beast | Large herbivore; good for tough beast |
(Velociraptor already has an effect.)
**Australia Animals Pack V1** (already imported):
| Animal | Good fit for | Notes |
|---|---|---|
| Koala | koala beast | Peaceful; good for low-threat province events |
This document describes how the Unity client's tutorial system is wired together: the classes involved, the trigger catalog, the step lifecycle, and the expected first-session flow. It complements two existing docs:
- **`TUTORIAL_CONTENT.md`** — human-facing copy for tutorial steps and dialogues
- **`TUTORIAL_BATTLE_SYSTEM.md`** — the scripted first-battle scenario (Tarn vs. John Ranil)
It also overlaps slightly with the in-tree `Assets/Tutorial/TUTORIAL_PLAN.md`, which is an older implementation plan.
All paths below are relative to `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/`.
---
## Current State (important)
There are **two parallel subsystems** that share the same trigger plumbing:
| Narrative dialogue system | **Live** | `Tutorial/Dialogue/*`, `Resources/Dialogues/*.json` |
`TutorialContentDefinitions.RegisterAll()` returns early at line 17 with the comment *"Old tutorial content suppressed — replaced by narrative dialogue system."* As a result:
-`OnboardingSequence` is never assigned, so `StartOnboarding()` no-ops at the "No onboarding sequence assigned" branch.
- No contextual tutorial sequences are registered with the trigger registry.
- The trigger registry still fires events normally, but nothing on the step-UI side is listening.
-`DialogueManager` consumes those same events and matches them against scripts in `Resources/Dialogues/` (`tutorial_strategic.json`, `tutorial_battle.json`).
**In practice today, the entire active tutorial experience runs through the dialogue system.** The step-UI machinery is preserved for future use — content definitions still exist below the early return in `TutorialContentDefinitions.cs` for reference.
---
## Class Map
### Orchestration
- **`Tutorial/TutorialManager.cs`** — Singleton. Owns `OnboardingSequence`, the active step queue, the trigger registry, and the dialogue manager handle. Initialized by `EagleGameController.SetUpGame()` and `ShardokGameController.SetUpGame()`.
- **`Tutorial/TutorialState.cs`** — PlayerPrefs persistence. Tracks `OnboardingCompleted`, `OnboardingStepReached`, completed sequence IDs (HashSet for O(1) lookup), dismissed hints, and a global "tutorials disabled" preference.
- **`Tutorial/TutorialTargetRegistry.cs`** — Maps string IDs to `RectTransform`s for highlighting. Static targets are Inspector-assigned (`ProvinceInfoPanel`, `SupportField`, `CommitButton`, etc.); dynamic targets register at runtime (e.g., hero rows from `HeroesAndBattalionsPanelController`).
### Triggers
- **`Tutorial/Triggers/TutorialTriggerRegistry.cs`** (~1100 lines) — Routes game events to both the step UI (when sequences are registered) and `DialogueManager`. Maintains one-shot session flags so the same first-encounter trigger doesn't fire twice.
- **`Tutorial/Content/TutorialSequence.cs`** — `ScriptableObject` holding ordered `TutorialStep`s plus `IsOnboarding` flag and lifecycle callbacks.
- **`Tutorial/Content/TutorialContentDefinitions.cs`** — Static class that *would* register all sequences. Currently short-circuits before any registration.
### UI (step UI — dormant)
- **`Tutorial/UI/TutorialUIManager.cs`** — Coordinates which presenter renders each step.
- **`Tutorial/UI/TutorialCanvasBuilder.cs`** — Builds the Canvas at runtime (no prefab dependency).
1.**Triggered** — `TutorialTriggerRegistry` raises an event (e.g., `game_started`, `battle_entered`).
2.**Queued or shown** — If no active sequence, show immediately. If an active step is hidden (`DisplayMode.None`), interrupt it. If both new and active are command tutorials, interrupt for responsiveness. Otherwise queue.
3.**Rendered** — `TutorialUIManager.ShowCurrentStep()` dispatches to the modal/overlay/hint presenter based on `DisplayMode`.
4.**Awaiting completion** — One of:
-`ButtonClick` — user dismisses
-`GameEvent` — wait for a named event (e.g., `province_selected`)
-`Timer` — auto-advance after delay
-`UIInteraction` — wait for the highlighted target to be interacted with
-`Condition` — custom predicate (rare)
5.**Advance** — `AdvanceStep()` moves to the next step or completes the sequence; completion writes to `TutorialState`.
Hidden steps (`DisplayMode.None`) deliberately don't block — they exist so contextual tutorials can fire while the onboarding sequence is waiting on an async event.
---
## Display Modes
| Mode | Renderer | Notes |
|------|----------|-------|
| **Modal** | `TutorialModalPanel` | Full blocking dialog with dim background |
| **Overlay** | `TutorialOverlayController` | Dimmer with cutout + tooltip near a target |
| **Tooltip** | `TutorialOverlayController` | Currently same renderer as overlay |
| **Hint** | `TutorialHintIndicator` | Pulsing dot only (stub) |
| **None** | (invisible) | Waits for a completion event |
Panels can be anchored via `TutorialPanelAnchor` (`Center` / `Left` / `Right` / `Top` / `Bottom`).
---
## Trigger Catalog
All triggers are raised via `TutorialTriggerRegistry`. Today these flow to `DialogueManager.TriggerDialogue()` and are matched against the dialogue JSON; if step-UI sequences are re-registered, they will also route there. File:line citations are for the registry unless noted; line numbers may drift.
### Bootstrap / first-session
| Trigger | Fires from | When |
|---------|-----------|------|
| `game_started` | `EagleGameController.SetUpGame()` | First time entering a tutorial game |
| `first_battle_available` | `OnModelUpdated()` ~L204 | A `RunningShardokGameModel` first appears |
| `battle_entered` | `OnBattleEntered()` ~L793 | User clicks **Battle!** |
| `shardok_battle_reset` | `ShardokGameController.cs:627` (fired *directly* to `DialogueManager`, bypassing the registry) | Battle retry; the registry's battle flags and the battle dialogue scripts' completed-set are reset alongside, so per-battle tutorials re-fire |
### One-shot semantics
Most contextual triggers are guarded by booleans on the registry (e.g., a `_diplomacyShown` flag). These are session-local — they don't persist via `TutorialState`, but the registry has `ResetBattleTriggerFlags()` for replays. The strategic completion list *does* persist across sessions.
---
## Highlight Targets
Steps reference UI by string ID through `TutorialTargetRegistry`:
1. Static targets are wired in the Inspector on the registry.
2. Dynamic targets register at runtime (`RegisterTarget(id, rectTransform)`).
3. Lookup falls back to `GameObject.Find()` if not registered.
Step options:
-`TargetGameObjectPath` — primary highlight
-`AdditionalHighlightTargets[]` — multiple at once
-`HighlightBoundsFromChildren` — use children's bounding box
-`HighlightPulsing` — strobe animation
Dialogue scripts use the same registry via `highlightTarget` (and `persistHighlight: true` to keep the highlight after the dialogue closes).
---
## Sequences vs. Contextual Dispatch
- **Onboarding sequence** (`IsOnboarding = true`) — Single linear sequence, started by `StartOnboarding()`. Resumes from `OnboardingStepReached`. Counts only visible steps for progress.
- **Contextual** (`IsOnboarding = false`) — Single- or multi-step, dispatched on demand by `TriggerContextualTutorial()`.
- Dispatch rules in `TriggerContextualTutorial()`:
1. No active sequence → show immediately.
2. Active step is hidden (`None`) → interrupt.
3. Both are command tutorials → interrupt.
4. Otherwise → queue.
---
## Dialogue System (Currently Live)
`DialogueManager` is a parallel system, not a step-UI subclass. It loads every `TextAsset` in `Resources/Dialogues/` at startup, parses them as `DialogueScript` objects, and indexes by `trigger`.
"instructionText":"Click Battle! to enter tactical combat.",
"highlightTarget":"GoToBattleButton",
"persistHighlight":true,
"highlightProvince":"Onmaa",
"completionEvent":null
}]
}]
}
```
When a registry trigger fires, `OnGameEvent()` calls `DialogueManager.TriggerDialogue(triggerId)`. If a script matches and hasn't completed, it queues (or shows immediately) and the panel renders speaker headshot + body + instruction. Steps can pause for a `completionEvent` (e.g., wait for the player to click the highlighted button), allowing the dialogue to chain across game state changes.
Dialogue panel position defaults sensibly per scene (`top` during combat) and can be overridden per script.
---
## Persistence
`TutorialState` (PlayerPrefs JSON):
-`OnboardingCompleted` (bool)
-`OnboardingStepReached` (int)
-`CompletedTutorials` (List → HashSet at load)
-`DismissedHints` (List → HashSet at load)
-`AllTutorialsDisabled` (bool)
Reset paths:
-`TutorialState.Reset()` clears everything.
- Entering a tutorial game (`GameType.Tutorial`) resets so it can be replayed.
- Settings → "Reset Tutorials" calls `TutorialManager.ResetAllProgress()`, which also calls `DialogueManager.ResetCompletedScripts()`.
`DialogueManager` tracks completed scripts in memory only — they reset whenever `TutorialState` does.
---
## Bootstrap
1.`TutorialManager` is in the scene as a singleton; `Awake` loads state and creates the trigger registry. `RegisterAll()` is called here but currently no-ops.
2.`ConnectionHandler` sets `IsTutorialGame` and `IsMultiplayerGame` when the game is created/joined.
3.`EagleGameController.SetUpGame()` calls `TutorialManager.Instance.Initialize(this, null)`. If `IsTutorialGame && !OnboardingCompleted`, it would call `StartOnboarding()` — currently a no-op because `OnboardingSequence` is null.
4. The first model update raises `game_started`. `DialogueManager` matches `tutorial_opening` from `tutorial_strategic.json` and the dialogue runs.
5.`ShardokGameController.SetUpGame()` calls `TutorialManager.Instance.Initialize(null, this)` when battle starts; raises `battle_entered` and battle-only triggers from there.
---
## Expected Tutorial Flow (today)
Driven by `tutorial_strategic.json` + `tutorial_battle.json`. Old Marek the Learned narrates almost everything.
### Narrative arc
You play **Sadar Rakon**, formerly Ikhaan Tarn's most trusted lieutenant. Tarn's behavior grew erratic, the King wouldn't listen, so Sadar broke off and raised the **Reclamation**. He's been pushed back to **Onmaa** for a last stand against Tarn's superior royal army. After surviving, the real twist arrives: a new invasion — **The Fracture Covenant**, led by a mysterious figure called *The Eagle* — lands ships on the western coast and starts taking provinces. Heroes from the King's own service mysteriously begin to desert. The Reclamation's framing pivots from "rebellion" to "the only force that can stop the actual invasion."
| 1 | `game_started` | **Opening monologue** (3 steps): Sadar's backstory, the Reclamation, last stand at Onmaa. Ends highlighting the **Battle!** button (`persistHighlight`) |
| 2 | `tutorial_battle_ended` | **Aftermath**: Tarn has *vanished*; you've captured his lieutenants. Recruit them or hold them |
| 3 | `tutorial_rebuild_support` | **Rebuild Onmaa** (3 steps): Marek frames it; **John Ranil** introduces *Improve* (engineer bonus); **Elena Fyar** introduces *Give Alms* (paladin bonus). Goal: 40 support by January |
| 4 | `tutorial_loyalty_warning` | November-ish, low-loyalty hero may leave at year end → use *Give Gold* / *Feast* |
| 5 | `tutorial_support_deadline` | December nag if support < 40: have Elena give alms now |
| 6 | `guidance_expand` (script `tutorial_expansion`) | Once support is stable: keep developing, taxes coming in January |
| 7 | `tutorial_taxes_collected` | January: gold/food collected. Now expand — March your warlord + most heroes to a neighbor, leave 1–2 behind |
| 8 | `tutorial_ready_to_join` | A free hero in `{provinceName}` is ready to recruit → Travel + Recruit |
| 9 | `tutorial_in_town` | When you Travel: explains Trade / Arm Troops / Divine / Recruit / Manage Prisoners; *Return* ends the turn |
| 10 | `tutorial_faction_appears` | **The twist**: Fracture Covenant lands at Ingia and Soria. *The Eagle* commands them. Tarn may be with them |
| 11 | `tutorial_hero_departed` | First King's hero abandons service — "something deeper is at work" |
| 12 | `tutorial_hero_departed_again` | Second one in a different month — "something is wrong, I can feel it" (foreshadowing) |
### Tactical battle flow (`tutorial_battle.json`)
| # | Trigger | What happens |
|---|---------|--------------|
| 1 | `shardok_placement_started` | **Placement** (3 steps): identify Tarn's units (knights, heavy infantry, dragoons), color legend (red/blue/black), placement instructions, highlight Commit |
| 2 | `shardok_battle_running` | **Turn 1 strategy** (2 steps): victory conditions for both sides, advice to hold castles and End Turn |
| 3 | `archery_available` | First archery opportunity → purple outlines, longbows vs. armor |
| 14 | `shardok_battle_reset` | If you lose and replay: Marek "had the strangest sensation… as though we'd already fought this battle, and lost" — fourth-wall-adjacent retry framing |
Most remaining "Tutorial / first-session onboarding" items in `SMALL_EAGLE_TODO.md` (Shardok tutorial, narrative hook, first-session goal, early small victory, guided first scenario vs. sandbox) map to *adding new dialogue scripts* against existing trigger IDs, or — if the step-UI is revived — to populating `TutorialContentDefinitions.RegisterAll()` and removing the early return.
- **Combat tutorials are deliberately paced.** Don't expect every available-ability trigger to fire on its first eligible turn — the registry intentionally spaces them.
- **One-shot flags are session-local.** A trigger that "already fired" will not re-fire even if `TutorialState` is reset, until the registry is recreated.
- **`TutorialTargetRegistry` lookups silently fall back to `GameObject.Find`.** Typos in target IDs will render with no highlight rather than throwing.
- **`TutorialContentDefinitions.RegisterAll()` returning early is load-bearing.** If the step UI is revived without auditing trigger overlap, dialogue and step-UI tutorials may double-fire on the same trigger.
- **`Tutorial/TUTORIAL_PLAN.md`** in the Unity project is an older implementation plan and partially overlaps this doc.
Unity is removing Built-In Render Pipeline (BiRP) support after Unity 6.7. We're currently on Unity 6.4. Each Unity minor version ships roughly quarterly, giving us a few release cycles before BiRP is dropped. This document lays out the migration plan.
## Strategy: Long-Lived Feature Branch
All URP conversion work happens on a **long-lived `urp-migration` branch**. Main stays fully functional on BiRP throughout the migration.
- **Main branch**: Continues using BiRP. All gameplay, AI, and server development proceeds normally.
- **URP branch**: Accumulates rendering pipeline changes across all phases.
- **Regular rebasing**: Periodically rebase/merge `main` into the URP branch to stay current. Shader and material changes rarely conflict with gameplay code, so merge conflicts should be manageable.
- **Merge to main**: Only when everything renders correctly and visual QA passes.
This avoids the "everything is pink" problem of switching the pipeline on main before shaders are converted.
@@ -8,13 +8,25 @@ The AI attempts to complete quests via `FulfillQuestsCommandSelector`, which is
## Quests the AI Actively Completes
The AI processes quest handlers in priority order. The first handler that produces a valid command wins.
### Diplomacy Quests
| Quest | Handler | Conditions |
|-------|---------|------------|
| `AllianceQuest` | `AllianceQuestCommandChooser` | Target faction must meet trust conditions for alliance and not already be in an alliance |
| `TruceWithFactionQuest` | `TruceWithFactionQuestCommandChooser` | Target faction must meet trust conditions for truce |
| `TruceCountQuest` | `TruceCountQuestCommandChooser` | Picks a random faction that meets trust conditions and isn't already in a truce/alliance |
| `AllianceQuest` | `AllianceQuestCommandChooser` | Target faction must meet trust conditions for alliance and not already be in an alliance |
| `BetrayAllyQuest` | `BetrayAllyQuestCommandChooser` | Break alliance with target faction. Only if factions don't share a border. Sends weakest non-leader hero. |
### Province Development Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `ImproveAgricultureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Agriculture type |
| `ImproveEconomyQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Economy type |
| `ImproveInfrastructureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Infrastructure type |
| `TotalDevelopmentQuest` | `TotalDevelopmentQuestCommandChooser` | Improve the lowest stat in the target province |
### Resource Giving Quests
@@ -24,72 +36,96 @@ The AI attempts to complete quests via `FulfillQuestsCommandSelector`, which is
| `AlmsAcrossRealmQuest` | `AlmsAcrossRealmQuestCommandChooser` | Gives food from the province with the largest surplus |
| `GiveToHeroesInProvinceQuest` | `GiveToHeroesInProvinceQuestCommandChooser` | Gives gold to the hero with the lowest loyalty in the specified province |
| `GiveToHeroesAcrossRealmQuest` | `GiveToHeroesAcrossRealmQuestCommandChooser` | Gives gold from the province with the most gold available |
| `SpendOnFeastsQuest` | `SpendOnFeastsQuestCommandChooser` | Spend gold on feasts |
| `SendSuppliesQuest` | `SendSuppliesQuestCommandChooser` | Send food to the target province |
### Province Development Quests
### Prisoner Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `ImproveAgricultureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Agriculture type |
| `ImproveEconomyQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Economy type |
| `ImproveInfrastructureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Infrastructure type |
| `ReleasePrisonerQuest` | `ReleasePrisonerQuestCommandChooser` | Release a specific prisoner |
| `ExilePrisonerQuest` | `ExilePrisonerQuestCommandChooser` | Exile a specific prisoner |
| `ExecutePrisonerQuest` | `ExecutePrisonerQuestCommandChooser` | Execute a specific prisoner |
| `ReturnPrisonerQuest` | `ReturnPrisonerQuestCommandChooser` | Return a prisoner to their faction |
| `ReleaseAllPrisonersQuest` | `ReleaseAllPrisonersQuestCommandChooser` | Release all prisoners |
### Province Order Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `DevelopProvincesQuest` | `DevelopProvincesQuestCommandChooser` | Switches provinces to Develop order. Prefers provinces without hostile neighbors. |
| `MobilizeProvincesQuest` | `MobilizeProvincesQuestCommandChooser` | Switches provinces to Mobilize order. Prefers provinces with hostile neighbors. Skipped if a DevelopProvincesQuest also exists (conflict avoidance). |
### Weather/Epidemic Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `StartEpidemicQuest` | `StartEpidemicQuestCommandChooser` | Start an epidemic in a province. Skipped if the target province belongs to the acting faction. |
| `StartBlizzardQuest` | `ControlWeatherQuestCommandChooser` | Start a blizzard via ControlWeather command. Skipped if the target province belongs to the acting faction. |
| `StartDroughtQuest` | `ControlWeatherQuestCommandChooser` | Start a drought via ControlWeather command. Skipped if the target province belongs to the acting faction. |
### Military Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `GrandArmyQuest` | `GrandArmyQuestCommandChooser` | Uses OrganizeTroops to top off existing battalions and hire Light Infantry. Only executes if resources are sufficient to fully meet the troop target. |
| `BattalionDiversityQuest` | `BattalionDiversityQuestCommandChooser` | Hires one battalion of a missing type to reach 3+ distinct types. Only attempts if the province already has 2+ existing types, has sufficient development (`meetsRequirements`), and gold/food surplus. |
| `UpgradeBattalionQuest` | `UpgradeBattalionQuestCommandChooser` | Walks a priority tree per turn: arm-completes (with optional travel and/or food sale setup) → train-completes → march in a pre-qualified neighbor battalion → organize/top-off of the target type → march in an unqualified neighbor battalion → develop Economy/Agriculture → train progress → develop Infrastructure → arm progress. Only toggles Travel when it directly enables an arm-completes finisher. |
### Riot Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `SuppressRiotByForceQuest` | `CommandChoiceHelpers.handleRiotSelectedCommand` | When this quest exists and the faction has battalions, the AI prefers CrackDown over Give when handling riots. Not a quest command chooser — modifies existing riot handling priority. |
| `ReconProvincesQuest` | `ReconProvincesQuestCommandChooser` | Issues a Recon command to reconnoiter a province |
### Beast Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `FightBeastsAloneQuest` | `FightBeastsAloneQuestCommandChooser` | Send an expendable non-leader hero to fight beasts without a battalion. Hero must have power at most `MaxExpendableHeroPowerRatio` (default 0.75) times the quest-giving hero's power. Picks the weakest eligible hero. |
### Special Event Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `ApprehendOutlawQuest` | `ApprehendOutlawQuestCommandChooser` | Apprehend a specific outlaw hero when present in the province |
### Other Quests
| Quest | Handler | Conditions |
|-------|---------|------------|
| `RestProvinceQuest` | `RestProvinceQuestCommandChooser` | Use the Rest command in a specific province |
| `SwearBrotherhoodWithHeroQuest` | `SwearBrotherhoodQuestCommandChooser` | Swear brotherhood with a specific hero |
| `DismissSpecificVassalQuest` | `DismissSpecificVassalCommandChooser` | Only if province has more than 2 heroes AND the unaffiliated hero's power >= target hero's power * `RequiredPowerMultiplierForDismiss` |
## Quests the AI Does Not Attempt to Complete
## Quests the AI Does Not Yet Attempt to Complete
The following quests have no handler in `FulfillQuestsCommandSelector` and must be completed naturally through gameplay:
The following quests have no handler and must be completed naturally through gameplay. They are listed in rough priority order for future implementation.
### Combat/Military Quests
-`DefeatFactionQuest` - Defeat a specific faction
-`GrandArmyQuest` - Accumulate a large number of troops
-`UpgradeBattalionQuest` - Upgrade a battalion to minimum armament/training
-`WinBattleOutnumberedQuest` - Win a battle while outnumbered
-`WinBattlesQuest` - Win a number of battles
-`RescueImprisonedLeaderQuest` - Rescue an imprisoned leader from another faction
### Planned for Implementation
### Expansion Quests
-`ExpandToProvincesQuest` - Expand to control a certain number of provinces
-`SpecificExpansionQuest` - Conquer a specific province
-`BorderSecurityQuest` - Have troops in a border province
None currently planned.
### Prisoner Quests
-`ExecutePrisonerQuest` - Execute a specific prisoner
-`ExilePrisonerQuest` - Exile a specific prisoner
-`ReleasePrisonerQuest` - Release a specific prisoner
-`ReturnPrisonerQuest` - Return a prisoner to their faction
-`ReleaseAllPrisonersQuest` - Release all prisoners
### Not Planned
### Province Order Quests
-`DevelopProvincesQuest` - Maintain provinces in Develop order for months
-`MobilizeProvincesQuest` - Maintain provinces in Mobilize order for months
-`RestProvinceQuest` - Use the Rest command in a specific province
These quests are too situational, passive, or risky to actively pursue. They may be completed naturally through gameplay.
### Reconnaissance Quests
-`ReconProvincesQuest` - Reconnoiter a number of provinces
-`ReconSpecificProvincesQuest` - Reconnoiter specific provinces
### Economic Quests
-`TotalDevelopmentQuest` - Achieve total development level in a province
-`WealthQuest` - Accumulate gold and food
-`SpendOnFeastsQuest` - Spend gold on feasts
-`SendSuppliesQuest` - Send food to a specific province
-`RepairDevastationQuest` - Repair devastation
### Special Event Quests
-`SuppressRiotByForceQuest` - Suppress a riot by force
-`FightBeastsAloneQuest` - Fight beasts alone
-`StartBlizzardQuest` - Start a blizzard in a province
-`StartEpidemicQuest` - Start an epidemic in a province
-`ApprehendOutlawQuest` - Apprehend an outlaw hero
### Miscellaneous
-`BattalionDiversityQuest` - Have diverse battalion types
-`SwearBrotherhoodWithHeroQuest` - Swear brotherhood with a specific hero
-`BetrayAllyQuest` - Betray an allied faction
-`BorderSecurityQuest` - Have troops in a border province. Involves taking provinces; better left to organic expansion.
-`DefeatFactionQuest` - Defeat a specific faction. Too risky to pursue proactively.
-`SpecificExpansionQuest` - Conquer a specific province. Too risky.
-`ExpandToProvincesQuest` - Expand to control a certain number of provinces. Too risky.
-`WinBattleOutnumberedQuest` - Win a battle while outnumbered. Can't reliably engineer.
-`WinBattlesQuest` - Win a number of battles. Passive.
-`RescueImprisonedLeaderQuest` - Rescue an imprisoned leader. Complex multi-step.
-`WealthQuest` - Accumulate gold and food. Happens passively.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.