Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.6 690d8c8903 Remove 1100+ lines of dead code from TutorialContentDefinitions
RegisterAll() had an early return followed by all the original tutorial
content definitions — unreachable since the narrative dialogue system
replaced them. Remove the dead code, keeping only the empty method stub.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 16:22:23 -07:00
d1b675cb99 Remove dead duplicate tracking in FearAnimator (#6549)
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>
2026-03-18 16:18:54 -07:00
df7a724542 Increase tutorial battle defender units by 20% (#6547)
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>
2026-03-18 07:02:03 -07:00
6f4041a510 Handle tutorial battle reset in Unity client (#6546)
* 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>
2026-03-18 06:56:56 -07:00
15e6cc2e5f Handle BattleResetResponse in Eagle for tutorial battle resets (#6537)
- 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>
2026-03-18 06:49:43 -07:00
657e46ad4c Detect tutorial battle loss and send BattleResetResponse in Shardok (#6536)
- 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>
2026-03-18 06:48:56 -07:00
3c2dd7f562 Fix settings panel not showing on first Escape press (#6545)
* 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>
2026-03-17 19:33:44 -07:00
fd851c83b0 Fix Shardok battle race condition: hex tiles not rendering (#6544)
* 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>
2026-03-17 19:16:42 -07:00
d3712699d8 Bump actions/github-script v7 to v8 for Node 24 support (#6543)
Node.js 20 actions are deprecated and will be forced to Node 24 starting
June 2, 2026.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 08:06:59 -07:00
94459d6e87 Restore keychain search list before iOS export (#6542)
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>
2026-03-17 08:05:36 -07:00
64fdb5e08a Remove chimpanzee beast — no suitable animation available (#6541)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 07:05:31 -07:00
52a7accfa2 Retry LFS pull to handle Gitea mirror sync race (#6540)
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>
2026-03-17 06:59:13 -07:00
71d7f62023 Add 26 new human-type beasts reusing existing Polytope archetypes (#6539)
Knight: inquisitor, zealot
Soldier: smuggler, racketeer, raider, looter, privateer
Archer: poacher, rustler, bushwhacker
Militia: vandal, goon, scoundrel, lout, delinquent, rapscallion
Peasant: anarchist, dissident, malcontent, provocateur, demagogue,
         mutineer, firebrand, deserter, squatter
Butcher: maniac

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 06:58:37 -07:00
1f0104e484 Fix RemoveController using shared_lock for map mutation (#6538)
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>
2026-03-17 06:54:43 -07:00
98d70fa995 Add hippogryph beast effect using DragonEffect flight system (#6533)
* 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>
2026-03-16 22:16:51 -07:00
e7f9f5cc35 Add BattleResetResponse proto messages (#6534)
* 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>
2026-03-16 22:11:57 -07:00
fc0edc38ef Clean up Unity-iPhone DerivedData after iOS archive (#6535)
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>
2026-03-16 22:11:38 -07:00
e6aa15ec50 Update BEAST_EFFECTS.md with 9 new beast animations (#6531)
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>
2026-03-16 06:35:08 -07:00
5f082f9e4d Implement Blow Bridge command and factory (#6524)
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>
2026-03-15 21:53:28 -07:00
25592e7268 Add Unity client support for Blow Bridge command (#6526)
* 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>
2026-03-15 21:50:04 -07:00
b49ffbadbe Propagate AI thread errors to Eagle via gRPC stream (#6530)
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>
2026-03-15 16:32:11 -07:00
efe99e298b Fix AI generating invalid Become Outlaw commands (#6529)
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>
2026-03-15 16:28:59 -07:00
a2aea00034 Add 9 new beast effects: black panther, rhinoceros, gorilla, hyena, leopard, warthog, emu, tasmanian devil, kangaroo (#6527)
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>
2026-03-15 16:09:15 -07:00
5f17877e71 Add lion and velociraptor beast effects (#6525)
* 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>
2026-03-15 10:59:21 -07:00
9cd22c43c9 Add Blow Bridge proto definitions (#6523)
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>
2026-03-15 10:48:05 -07:00
7786191430 Add hippopotamus beast animation using Africa Animals Pack Low Poly V2 (#6522)
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>
2026-03-14 22:11:32 -07:00
dfc334267c Add animated wind direction indicator to Shardok battle view (#6518)
* 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>
2026-03-14 14:43:49 -07:00
7c4b4ef490 Reduce unnecessary UI raycasting in Shardok battle view (#6520)
- 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>
2026-03-14 07:58:13 -07:00
79d26fa73a Fix duplicate unaffiliated hero exception in client (#6519)
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>
2026-03-13 19:17:16 -07:00
72ef203a8a Always include shardokGameId in battle views for non-participants (#6517)
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>
2026-03-13 18:56:00 -07:00
85b620b644 Remove battle from ShardokBattles when Shardok game ends (#6515)
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>
2026-03-13 18:30:39 -07:00
fb36684e35 Update battle effects even when SwapModel skips due to commands (#6514)
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>
2026-03-13 18:14:42 -07:00
b6d83e3775 Fix battle animation appearing at center of conquered province (#6513)
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>
2026-03-13 16:43:12 -07:00
2d3050eded Fix BetrayAllyQuest: breaking an alliance was failing the quest instead of fulfilling it (#6512)
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>
2026-03-13 16:38:33 -07:00
bb848e73f3 Restrict battle observation to participants and allies (#6509)
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>
2026-03-13 16:15:49 -07:00
37c96eceec Fix settings generator and sync settings.tsv with BUILD (#6511)
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>
2026-03-13 16:08:56 -07:00
64e41a3c12 Set cannotBecomeOutlaw for tutorial opening battle (#6508)
* 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>
2026-03-13 15:55:06 -07:00
b81e795350 Direct March and Send Supplies animations toward destination province (#6506)
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>
2026-03-13 15:48:11 -07:00
f8c2b19609 Prevent King from inviting Bridget and Hedrick in tutorial (#6507)
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>
2026-03-13 15:42:58 -07:00
95fbbf91a3 Add cannot_become_outlaw flag to battle request (#6505)
* 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>
2026-03-13 15:42:02 -07:00
a1bd0a61ae Add tutorial Mobilization phase start logic (#6482)
* 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>
2026-03-13 15:38:48 -07:00
0553da332e Boost tutorial defender unit sizes and Luke's faction bias (#6504)
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>
2026-03-13 14:57:58 -07:00
d39038296f Update Unity prefab layouts and reimported assets (#6503)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:30:03 -07:00
36bc30f2bb Scale province action animations up ~30% (#6502)
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>
2026-03-13 11:08:35 -07:00
1952ab796d Color March "keep" amount red when over province capacity (#6501)
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>
2026-03-13 10:43:40 -07:00
118311209f Add map validation: no starting positions on water, mountains, or overlapping (#6498)
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>
2026-03-13 07:40:47 -07:00
adminandGitHub 75b93ac3fc Update map: Kojaria (#6500) 2026-03-13 07:34:30 -07:00
fe5360d6f1 Fix missing flee/captured sound when battle ends (#6497)
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>
2026-03-13 07:26:29 -07:00
adminandGitHub 7dc0ce220f Update map: Chapellia (#6496) 2026-03-13 07:20:49 -07:00
74ade84dcd Replace Feast sound effect with crowd victory cheer (#6495)
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>
2026-03-13 07:16:53 -07:00
f626eff404 Update Gameplay scene RectTransform anchors and prefab overrides (#6494)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 21:44:53 -07:00
0b06173e96 Fix Shardok overlay flash on unit selection (#6493)
* 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>
2026-03-12 21:41:24 -07:00
32a58fd8eb Clean up orphaned animation sprites when Map deactivates (#6492)
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>
2026-03-12 21:22:17 -07:00
53eb739cfe Add elephant/mammoth beast effect (#6489)
* 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>
2026-03-12 20:09:17 -07:00
4e39baf3fa Queue province animations while Map is inactive, replay on reactivate (#6490)
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>
2026-03-12 19:53:24 -07:00
95ce201b20 Apply lightweight loop filter at root of iterative deepening search (#6491)
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>
2026-03-12 19:47:28 -07:00
aeeb3ec8e0 Fix AI crash: meteor cancel with 0 AP after meteor start (#6487)
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>
2026-03-12 18:41:54 -07:00
6886964efa Fix mammoth stats from bandit template to large animal (#6488)
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>
2026-03-12 06:40:48 -07:00
aa77094790 Extract Command Panel and 35 sub-panels into prefabs (#6486)
* 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>
2026-03-11 21:45:54 -07:00
3ccdcaa6cd Extract Shardok Container into prefab (#6485)
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>
2026-03-11 14:18:52 -07:00
c8c95ac1b6 Refactor EagleCommonTextures to singleton pattern (#6484)
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>
2026-03-11 14:03:04 -07:00
bbbe4211e3 Extract Custom Battle Panel into prefab (#6483)
* 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>
2026-03-11 11:25:51 -07:00
a1391cdad2 Add missing Unity .meta files and import artifacts (#6481)
* 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>
2026-03-11 09:21:22 -07:00
c25d042690 Remove SimpleFileBrowser plugin and associated UI (#6480)
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>
2026-03-11 08:58:21 -07:00
d9666dc01d Apply persisted settings at startup even when Settings Panel is inactive (#6479)
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>
2026-03-11 07:57:40 -07:00
cd9fc49bde Replace tutorial buttons with phase dropdown (#6474)
* 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>
2026-03-11 07:25:31 -07:00
1ca4ee5cc0 Extract Connection, Lobby, and DropGameConfirmation panels into prefabs (#6478)
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>
2026-03-11 07:12:39 -07:00
42957266ba Add wind-assisted range 3 archery for longbowmen (#6457)
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>
2026-03-11 07:06:31 -07:00
6a81ce97db Replace tutorial booleans with TutorialPhase enum (#6477)
* 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>
2026-03-11 07:05:11 -07:00
5b84d20f69 Extract Settings Panel into prefab (#6476)
* 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>
2026-03-10 22:14:49 -07:00
b3adebbb0a Add tiger beast effect using ithappy Animals FREE pack (#6475)
* 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>
2026-03-10 16:44:05 -07:00
007fd039be Add battle-in-progress animation on strategic map (#6473)
* 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>
2026-03-10 13:31:54 -07:00
6472e1dabe Add origin province IDs to ShardokBattleView for battle-in-progress animations (#6472)
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>
2026-03-10 12:30:19 -07:00
d9e7b55279 Split captured hero recruitment ActionResultTypes and add animations (#6470)
* 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>
2026-03-10 11:15:24 -07:00
2e8d2bc83d Split captured hero recruitment into distinct ActionResultTypes (#6471)
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>
2026-03-10 11:08:39 -07:00
1776198890 Fix tutorial duplicate hero bug by changing Tarn from Fled to Outlawed (#6469)
* 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>
2026-03-10 09:50:28 -07:00
a74cf4fbbf Fix missing Ranil and Elena portraits in post-battle tutorial dialogue (#6468)
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>
2026-03-10 08:51:52 -07:00
90c9327b25 Add tutorial indicator to lobby running game items (#6467)
* 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>
2026-03-10 08:27:15 -07:00
51579752a2 Add 19 province action animations with sound effects (#6466)
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>
2026-03-10 07:40:23 -07:00
e8be706370 Add game_type to lobby GameInfo proto and server (#6465)
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>
2026-03-10 07:37:04 -07:00
b313ad6507 Fix tutorial battle flee by setting fleeProvinceId to None and remove dead canFleeHeroIds (#6464)
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>
2026-03-10 07:11:49 -07:00
bb02481df9 Add TutorialPhase to GameType ADT (#6453)
* 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>
2026-03-10 06:47:00 -07:00
10b43e9225 Add necromancer control chain visual effect (#6463)
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>
2026-03-09 22:06:59 -07:00
f79b89a721 Widen meteor beam destination and speed up during Cast phase (#6462)
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>
2026-03-09 21:57:10 -07:00
bcf1641d39 Add sound effect for meteor start animation (#6461)
* 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>
2026-03-09 21:55:52 -07:00
c43750c206 Fix tutorial button visibility not updating after login (#6460)
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>
2026-03-09 21:38:44 -07:00
4c81a90aac Add Feast province animation with food toss effect (#6459)
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>
2026-03-09 21:36:13 -07:00
491a9427e6 Skip auto-selecting mage with existing meteor target (#6458)
* 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>
2026-03-09 19:38:51 -07:00
f43a2b852c Wire ProvinceActionAnimator in Gameplay scene (#6456)
* 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>
2026-03-09 19:25:38 -07:00
85e499e47f Add game ID and backtrace to AI thread exceptions (#6455)
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>
2026-03-09 19:17:05 -07:00
62730414bd Add province action animations to Eagle strategic map (#6451)
* 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>
2026-03-09 19:15:48 -07:00
5cd5163c79 Make meteor targeting free (0 AP) to fix re-targeting (#6454)
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>
2026-03-09 18:50:11 -07:00
ed35d00c60 Fix Eagle Appears tutorial event to match normal game faction setup (#6448)
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>
2026-03-09 18:43:04 -07:00
8772b5a988 Fix validation error when non-allied armies converge on unoccupied province (#6452)
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>
2026-03-09 18:41:09 -07:00
fa1f0ffcb0 Replace hardcoded IsTester with is_admin from UserInfo proto (#6449)
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>
2026-03-09 18:25:49 -07:00
05cb22dcd7 Allow meteor re-targeting after initial target selection (#6450)
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>
2026-03-09 18:23:58 -07:00
adminandGitHub 60203343ab Tutorial tuning: boost Luke's faction bias, add Hedrick prestige penalty (#6447) 2026-03-08 22:08:53 -07:00
adminandGitHub 072749c63f Add persistent meteor targeting beam effect (#6446) 2026-03-08 22:07:49 -07:00
e850ba7546 Trim two-stage animation doc to just the implementation pattern (#6445)
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>
2026-03-08 21:39:27 -07:00
f2ff4cc912 Add distinct success/failure animations for BraveWater (#6443)
* 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>
2026-03-08 21:36:26 -07:00
4e84dbc4a4 Add distinct success/failure animations for DismissUnit (#6442)
* 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>
2026-03-08 21:25:04 -07:00
f040b4df3a Add distinct success/failure animations for RaiseDead (#6441)
* 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>
2026-03-08 21:01:21 -07:00
cab6883c66 Add distinct Scout success/failure animations (#6440)
* 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>
2026-03-08 18:55:08 -07:00
732ffb2aa8 Move AnimationTestController to Right Sidebar for better visibility (#6444)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 18:50:01 -07:00
79a64e2be7 Add distinct FreezeWater success/failure animations (#6439)
* 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>
2026-03-08 17:25:21 -07:00
d562266044 Add distinct ExtinguishFire success/failure animations (#6438)
* 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>
2026-03-08 17:11:39 -07:00
11430be186 Filter destroyed battalion IDs from aftermath claimants and update doc (#6437)
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>
2026-03-08 14:15:35 -07:00
57703ffbda Fix honey badger red stripes: remove shader replacement for 3D models (#6434)
* 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>
2026-03-08 14:08:31 -07:00
dad8c9ebe7 Remove ally_victory and draw from Shardok victory_condition proto (#6436)
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>
2026-03-08 14:07:25 -07:00
13ecf9cd1a Fix dead-ally throw in GameOverResponsePopulator (#6435)
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>
2026-03-08 13:24:38 -07:00
beabcede11 Fix Shardok tooltip positioning for Screen Space Camera canvas (#6433)
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>
2026-03-08 12:50:16 -07:00
2b54f4aa7c Add raw game history fallback to admin server for broken games (#6432)
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>
2026-03-08 12:48:12 -07:00
b292644f76 Fix allied attacker victory conditions (#6429)
* 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>
2026-03-08 12:47:35 -07:00
528335c8d9 Remove AllyVictory from Eagle battle resolution (#6431)
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>
2026-03-08 12:26:26 -07:00
4dbd6279cd Add GetRawGameHistory and RawRewindGame admin RPCs for broken games (#6430)
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>
2026-03-08 12:24:34 -07:00
ce18908688 Remove draw handling from EagleInterface game-over conversion (#6427)
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>
2026-03-08 08:36:05 -07:00
fd89f9e059 Validate WIN_AFTER_MAX_ROUNDS at game setup and remove DRAW codepath (#6426)
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>
2026-03-08 08:07:25 -07:00
8602f1cf72 Remove Draw from battle resolution and add WinAfterMaxRounds validation (#6425)
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>
2026-03-08 08:05:20 -07:00
43bb0bcf74 Fix tooltip positioning for Screen Space Camera canvas (#6421)
* 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>
2026-03-08 08:02:54 -07:00
36e61da168 Remove auto-create first game logic from lobby (#6424)
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>
2026-03-08 07:59:12 -07:00
4bdade4744 Fix allied battle victory: treat AllyVictory as winner in ResolveBattleAction (#6423)
* 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>
2026-03-08 07:44:30 -07:00
42dfd143ea Fix bounced armies not processed by WithdrawnArmiesReturnHomeAction (#6422)
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>
2026-03-07 19:33:18 -08:00
db77a33003 Fix bounce for non-allied co-attackers when no army advanced (#6420)
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>
2026-03-07 19:07:16 -08:00
468c863a63 Fix aftermath supply caps, auto-keep last claimant, and persist PendingConquestInfo (#6417)
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>
2026-03-07 18:15:24 -08:00
865fe2c58d Fix Royal Lancers HEAVY_CAVALRY size exceeding capacity (800 → 600) (#6419)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:14:57 -08:00
7f01067d4c Fix duplicate province labels on map zoom (#6418)
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>
2026-03-07 18:14:41 -08:00
b3d494d820 Fix battle aftermath finalization and hero-battalion pairing bugs (#6416)
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>
2026-03-07 16:41:15 -08:00
73bc28e13a Add BattleAftermathDecisionCommandSelector for Unity client (#6413)
* 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>
2026-03-07 16:09:30 -08:00
a22041f975 Add battle aftermath decision command infrastructure (#6415)
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>
2026-03-07 16:02:14 -08:00
7c6bff4464 Remove Map Editor scene from build (#6414)
* 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>
2026-03-07 15:58:09 -08:00
81e7aa6d34 Show animation tests toggle only in Unity Editor (#6412)
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>
2026-03-07 15:38:14 -08:00
859087f1bb Add multi-victor battle state model and ResolveBattleAction branch (#6411)
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>
2026-03-07 15:37:47 -08:00
0f96a7bfdf Add distinct success/failure animations for Repair (#6410)
* 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>
2026-03-07 15:33:10 -08:00
b5ec52ea44 Add allied castle victory: allies collectively holding all castles triggers win (#6409)
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>
2026-03-07 15:25:38 -08:00
5a4f65b416 Add distinct success/failure animations for StartFire (#6408)
* 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>
2026-03-07 14:26:13 -08:00
a1c00e45c2 Use client_id to deduplicate same-device connections on the server (#6407)
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>
2026-03-07 08:31:04 -08:00
543cd02e51 Add client_id field to StreamGameRequest proto and populate in C# client (#6406)
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>
2026-03-07 08:25:44 -08:00
3eac386757 Throw on duplicate unaffiliated hero and fix table crash (#6405)
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>
2026-03-07 07:53:45 -08:00
407d7701e8 Add post-Eagle tutorial events: Ranil & Elena depart and form factions (#6365)
* 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>
2026-03-06 21:31:39 -08:00
cc49924af8 Throw on duplicate unaffiliated heroes instead of silently appending (#6402) (#6403)
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>
2026-03-06 21:06:24 -08:00
f79b9d61f5 Fix multi-client eviction: allow simultaneous connections per faction (#6402)
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>
2026-03-06 18:34:04 -08:00
a46dc40198 Fix province effects missing after CDN centroids change (#6401)
* 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>
2026-03-06 16:21:19 -08:00
b93171528c Randomize firework burst colors instead of cycling sequentially (#6400)
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>
2026-03-06 16:02:47 -08:00
1508fce835 Faster connection recovery on flaky networks (#6399)
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>
2026-03-06 15:55:22 -08:00
52cd8397c2 Make games.e0es S3 upload non-blocking with AsyncS3Persister (#6398)
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>
2026-03-06 15:54:21 -08:00
fd75833208 Use API key auth for iOS export to prevent Xcode-Token expiry failures (#6397)
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>
2026-03-06 15:17:14 -08:00
8aa3be6401 Fetch centroids.json from CDN via EagleMapInfo (#6396)
* 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>
2026-03-06 14:27:56 -08:00
192aaf73c6 Remove unused map_name from EagleMapInfo (#6395)
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>
2026-03-06 14:27:31 -08:00
026d962ca6 Add centroids SHA256 to EagleMapInfo for CDN validation (#6394)
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>
2026-03-06 14:13:33 -08:00
100700582e Centralize centroidsJson in ProvinceIDLoader (#6393)
* 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>
2026-03-06 14:12:07 -08:00
ef683e939b Move rawGray.gz.bytes from Unity Assets to src/main/resources (#6392)
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>
2026-03-06 13:59:37 -08:00
bd38b4ad5b Load province map from server-provided EagleMapInfo (#6388)
* 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>
2026-03-06 13:54:08 -08:00
5eba718632 Update eagle map hash and make migration read from config (#6391)
* 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>
2026-03-06 13:43:27 -08:00
e1babbda41 Add error diagnostics to warmup tool for server-side failures (#6390)
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>
2026-03-06 13:08:51 -08:00
e0dc941799 Update Gameplay scene: clean up stale serialized fields and adjust UI layout (#6389)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:08:15 -08:00
9aec075f51 Add smooth_boundaries.py for natural province boundary smoothing (#6382)
* 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>
2026-03-06 12:05:18 -08:00
c461d5d04e Add git -C prohibition to CLAUDE.md (#6387)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:01:01 -08:00
087aeb3484 Add reusable game save migration system with EagleMapInfo backfill (#6385)
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>
2026-03-06 11:26:40 -08:00
b3b57db0fa Move EagleMapInfo from hardcoded values to JSON game config via event sourcing (#6384)
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>
2026-03-06 10:25:13 -08:00
5317a3f8a2 Add EagleMapInfo to GameStateView proto and server (#6381)
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>
2026-03-06 10:09:15 -08:00
e5e636801c Fix stale Laufarvia-West Faluria neighbor entry in province_map.tsv (#6383)
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>
2026-03-06 09:43:47 -08:00
e5d9dd00ca Centralize rawGray province ID map references to ProvinceIDLoader (#6379)
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>
2026-03-06 09:09:25 -08:00
641f09fced Move combat sound effects to Addressables for CDN delivery (#6378)
* 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>
2026-03-06 08:01:12 -08:00
7c27737fe9 Fix DivideByZeroException when hex tile Addressables return empty lists (#6377)
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>
2026-03-06 07:12:57 -08:00
161eba96b7 Move tactical combat assets to Addressables for CDN delivery (#6376)
* 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>
2026-03-06 06:49:04 -08:00
24e4942d20 Move hex tile textures to Addressables for CDN delivery (#6375)
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>
2026-03-06 05:35:21 -08:00
84af6ceb61 Fix artifact retention-days to match repo maximum of 3 (#6374)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 21:41:13 -08:00
11c65ab063 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>
2026-03-05 19:56:54 -08:00
fcb8758762 Show Tutorial and Tutorial Lite buttons for testers on all platforms (#6372)
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>
2026-03-05 16:59:40 -08:00
046daa08a9 Remove standalone iOS Addressables build workflow (#6371)
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>
2026-03-05 16:59:21 -08:00
faadd91ceb Remove UIApplicationSceneManifest from iOS plist, don't quit on iOS (#6369)
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>
2026-03-05 16:31:56 -08:00
3d45938d33 Fix Mac/Windows builds using wrong platform for Addressables (#6370)
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>
2026-03-05 16:31:41 -08:00
adminandGitHub b66659f3c7 Register eagle0:// URL scheme via PlayerSettings, remove post-processor (#6368) 2026-03-05 12:59:53 -08:00
aacecfe1cb Auto re-auth when stored OAuth token expires (#6367)
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>
2026-03-05 10:16:01 -08:00
1bdd888886 UI layout tweaks in Gameplay scene (#6364)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:26:29 -08:00
971e632c22 Delay departures for king's low-loyalty outer-province heroes in tutorial (#6363)
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>
2026-03-04 13:41:23 -08:00
0575ae865c Add catch-all exception handling to SubscribeToGame (#6362)
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>
2026-03-04 10:25:37 -08:00
370a2af356 Throw on failed file open in byte_vector::FromPath (#6361)
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>
2026-03-04 10:24:52 -08:00
437d417a93 Have Ranil and Elena explain Improve/Alms in tutorial (#6359)
* 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>
2026-03-04 10:23:44 -08:00
e8e967fde2 Filter nightly iOS TestFlight by relevant paths (#6360)
* 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>
2026-03-04 10:16:20 -08:00
09d31630f8 Register AI client for factions created mid-game (#6357)
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>
2026-03-04 09:57:16 -08:00
e6acac9bd6 Skip nightly iOS TestFlight build when nothing changed (#6358)
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>
2026-03-04 09:55:26 -08:00
88a311094b Fix missing flee/retreat animations for untargeted commands (#6356)
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>
2026-03-04 07:22:29 -08:00
503d92fc3c Fix Eagle Appears event never firing in tutorial (#6355)
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>
2026-03-03 22:03:47 -08:00
81179dc268 Add nightly schedule to iOS TestFlight workflow (#6354)
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>
2026-03-03 21:48:49 -08:00
68ded773ec Add per-hero initial faction biases for tutorial captured heroes (#6353)
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>
2026-03-03 21:48:18 -08:00
5ff2639b84 Include province name in ready-to-join hero tutorial (#6352)
* 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>
2026-03-03 21:18:11 -08:00
16ffe21d46 Rework tutorial pacing: delay expansion, add town tutorials (#6348)
* 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>
2026-03-03 21:01:30 -08:00
e76873515a Bump tutorial outer province hero loyalty by 5 (#6351)
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>
2026-03-03 20:52:10 -08:00
85f7ecbde9 Set roundIdJoined in LoadedHeroConversion so tutorial heroes are protected (#6350)
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>
2026-03-03 20:43:26 -08:00
bb93676bce Fix Polytope humanoid models rendering as solid white/black (#6349)
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>
2026-03-03 20:33:58 -08:00
b336594173 Prevent newly-joined heroes from departing and tune tutorial loyalty (#6347)
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>
2026-03-03 20:20:33 -08:00
2fabbba081 Wire skip tutorial battle button and PopupClipper in Unity scenes (#6346)
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>
2026-03-03 19:21:16 -08:00
652cb7e732 Enable prebuilt protoc binaries via module extension (#6345)
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>
2026-03-03 19:02:38 -08:00
30d1822981 Add skip_tutorial_battle client-side support (#6344)
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>
2026-03-03 18:54:45 -08:00
3ee8293794 Add skip_tutorial_battle server-side auto-resolution (#6343)
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>
2026-03-03 18:12:25 -08:00
57c42d71f7 Clip map effects behind hero backstory popup (#6342)
* 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>
2026-03-03 16:57:49 -08:00
40f379c206 Clip map effects to scroll view viewport using shader clip rect (#6341)
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>
2026-03-03 13:47:37 -08:00
b0bcc5fd8d Fix PleaseRecruitMe appearing before quest completion notification (#6339)
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>
2026-03-02 19:23:01 -08:00
c310894fb0 Remove redundant LFS cache step from CI workflows (#6338)
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>
2026-03-02 19:21:40 -08:00
4717423f8b Add Flee as two-stage animation with captured-while-fleeing variant (#6337)
* 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>
2026-03-02 19:16:34 -08:00
5730873601 Add TrackedEffectAnimator base class to prevent effect leak on battle end (#6336)
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>
2026-03-01 16:35:34 -08:00
858338307f Add Heavy Cavalry battalion to King's starting province (#6335)
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>
2026-03-01 16:17:21 -08:00
93b7b723b9 Fix pregenerated hero names not delivered to client during gameplay (#6334)
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>
2026-03-01 16:04:12 -08:00
c81ce990bc Fix fear animation sprites persisting after battle ends (#6333)
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>
2026-03-01 15:04:50 -08:00
08cca4c5f0 Fix Shardok battle replay after admin rewind (#6331)
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>
2026-03-01 14:54:03 -08:00
20fd177746 Fix border security quest offered when already fulfilled by alliances (#6332)
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>
2026-03-01 14:45:58 -08:00
4aefe06683 Cap drawn beast count at actual ProvinceEvent beast count (#6330)
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>
2026-03-01 14:32:41 -08:00
9eb954be65 Handle removed FFA proto values gracefully in converters (#6329)
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>
2026-03-01 13:20:15 -08:00
a1c436b398 Remove Free-For-All battles, replace with sequential AttackDecision (#6328)
* 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>
2026-03-01 13:01:02 -08:00
f38f638512 Fix analyzer warnings in ShardokGameController (#6327)
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>
2026-02-28 09:48:45 -08:00
96a1f0580e Remove debug logging and unused code from ShardokGameController (#6326)
Remove MeteorCast/MeteorTarget Debug.Log calls, unused HeroId alias,
and unused NoHistoryText constant.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 09:43:02 -08:00
675daa2424 Capture losing non-defender attackers' units after province conquest (#6325)
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>
2026-02-28 08:14:07 -08:00
24307445ed Fix AllyVictory never granted in Shardok battle resolution (#6324)
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>
2026-02-28 08:11:55 -08:00
dcf9ce6a84 Fix leader cache crash when faction head is in killedHeroes (#6322)
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>
2026-02-27 22:04:25 -08:00
655e37e1f2 Shatter retreating armies arriving at conquered provinces (#6321)
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>
2026-02-27 19:50:40 -08:00
f389053e33 Suppress UI status flicker during expected gRPC reconnects (#6320)
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>
2026-02-27 19:25:44 -08:00
640a2e0269 Fix cross-wired fileIDs in ScorpionEffect prefab references (#6319)
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>
2026-02-27 16:51:28 -08:00
c5b38f7adc Color Send Supplies held-back amounts red when over province capacity (#6318)
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>
2026-02-27 16:40:35 -08:00
2b29dc6b06 Fix opponent two-stage animations and sounds not sequencing properly (#6317)
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>
2026-02-27 16:20:12 -08:00
79c5be3a54 Decouple non-Mac builds from Xcode dependency (#6316)
* 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>
2026-02-27 16:08:28 -08:00
e8780134ee Cache Git LFS objects in CI to reduce bandwidth usage (#6315)
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>
2026-02-27 15:20:27 -08:00
be48f54f2c Update golang.org/x/sys from v0.28.0 to v0.30.0 (#6314)
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>
2026-02-27 14:52:22 -08:00
f2487fc8ee Exclude slow MCTS basic test from default test suite (#6312)
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>
2026-02-27 14:39:38 -08:00
93ad8c4ae1 Reintegrate SparklePlugin into main workspace (#6308) (#6311)
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>
2026-02-27 13:16:02 -08:00
8cb75d48e2 Add giant beast type using 3x-scaled peasant (#6308)
* 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>
2026-02-27 13:05:59 -08:00
97bf29d4a2 Fix Bazel Xcode version caching in CI (#6309)
* 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>
2026-02-27 12:43:14 -08:00
38c3fbd6bd Add psychopath to beast effects coverage doc (#6310)
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>
2026-02-27 09:49:46 -08:00
47f936df05 Upgrade grpc from 1.74.1 to 1.78.0 (#6307)
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>
2026-02-27 09:27:13 -08:00
85a3f4bc39 Upgrade dragon beast to 3D model with full animation suite (#6306)
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>
2026-02-27 09:25:37 -08:00
ce49304406 Add Polytope 3D character archetypes for human beast animations (#6304)
* 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>
2026-02-27 07:36:59 -08:00
c9afc4a6b8 Convert existing asset packs to Git LFS (#6303)
* 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>
2026-02-27 07:18:02 -08:00
90f20e25b2 Add psychopath beast type (#6301)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 06:58:27 -08:00
d6c3abd505 Reduce raccoon animal scale from 15 to 5 (#6300)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:05:46 -08:00
ee31265ff6 Clean up warmup-binary artifacts after deploy (#6299)
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>
2026-02-26 20:58:47 -08:00
e289913e18 Add raccoon beast animation (Sketchfab 3D model) (#6298)
* 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>
2026-02-26 20:58:19 -08:00
62cba309d8 Replace Unity attributions.json with symlink to canonical copy (#6297)
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>
2026-02-26 15:00:13 -08:00
25d1d2b268 Note controller override in honey badger doc entry (#6296)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:14:11 -08:00
cd7e21cfa4 Add honey badger beast animation (3D model) (#6293)
* 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>
2026-02-26 14:01:27 -08:00
5bb6b4ca9d Add attribution for new beast animation asset packs (#6294)
* 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>
2026-02-26 13:33:55 -08:00
9030c455e0 Add no-chained-commands rule to CLAUDE.md (#6295)
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>
2026-02-26 11:41:27 -08:00
49c14be159 Add clown beast animation with both Clown Pack models (#6292)
* 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>
2026-02-26 10:08:18 -08:00
e63735f428 Update beast effects doc for ogre/troll coverage (#6291)
* 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>
2026-02-26 08:48:57 -08:00
f4b9a9268c Refactor BeastUtils to use pattern matching instead of asInstanceOf (#6290)
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>
2026-02-26 08:39:22 -08:00
272badda47 Balance beast danger levels and add powerExponent (#6288)
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>
2026-02-26 08:34:46 -08:00
080329e369 Fix NullReferenceException when clicking map before model loads (#6289)
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>
2026-02-26 08:32:31 -08:00
f9afcea9f7 Add ogre/troll beast animation using Orc-Ogre 3D asset pack (#6287)
- 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>
2026-02-26 08:15:49 -08:00
cec058c369 Wire up animations for 8 new beast types (#6286)
* 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>
2026-02-26 07:35:57 -08:00
679b5ef1ce Add 8 new beast types with matching asset pack animations (#6285)
New beasts from Animal Pack Deluxe:
- crab (0.04): coastal swarm, low power, agriculture damage
- frog (0.10): plague swarm, very high count, minimal power
- rabbit (0.20): crop-destroying swarm, high agriculture damage
- salamander (0.02): rare, poisonous, moderate power
- scorpion (0.15): venomous swarm, like spiders
- stag (0.04): territorial wild deer, moderate power
- wild goat (0.10): mountain crop raiders, agriculture damage
- wild pig (0.10): aggressive crop destroyers

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 07:07:36 -08:00
f44b7351b4 Add spawn likelihood to uncovered beast types in docs (#6284)
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>
2026-02-26 07:01:52 -08:00
03e82ae635 Update beast effects docs with full animation coverage (#6283)
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>
2026-02-25 21:25:18 -08:00
3e4aa6d6d2 Update animal effect prefab scales from 20 to 15 (#6282)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 21:21:37 -08:00
6bfbf38fb4 Add animal wandering effects for bear, boar, crocodile, snake, wolf (#6281)
* 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>
2026-02-25 21:08:55 -08:00
28560940cb Add Old Marek commentary when King's heroes depart in tutorial (#6280)
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>
2026-02-25 20:40:20 -08:00
38d097461d Add wandering monster animations for beast events (#6279)
* 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>
2026-02-25 20:32:09 -08:00
ce6bc0b975 Update Eagle appearance trigger for Hedrick and Sadar (#6278)
* 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>
2026-02-25 20:00:05 -08:00
8ce0a18d09 Fix horseshoe print orientation and duel accepted animation (#6277)
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>
2026-02-25 19:31:18 -08:00
2befe8b67f Add tutorial-specific initial chronicle text (#6276)
* 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>
2026-02-25 19:27:20 -08:00
76cf6f0624 Fix SupportField highlight in tutorial dialogue (#6272)
* 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>
2026-02-25 19:08:47 -08:00
b0c409d62c Fix support=0 not being applied to weak King provinces (#6275)
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>
2026-02-25 19:01:34 -08:00
b23ee1485b Simplify tutorial battle: no attackers can flee (#6274)
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>
2026-02-25 17:37:59 -08:00
03874da321 Add Hedrick's faction to tutorial AI controllers (#6273)
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>
2026-02-25 17:30:05 -08:00
7ca5db5509 Guarantee duel acceptance in tutorial battles (#6271)
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>
2026-02-25 17:07:45 -08:00
2364db42a7 Split reinforcement dialogue into per-hero events (#6268)
* 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>
2026-02-25 16:59:38 -08:00
cb1391e417 Make Ranil reinforce from Tarn's direction (#6270)
* 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>
2026-02-25 16:04:08 -08:00
d940393f3f Refactor beast effect selection for extensibility (#6269)
* 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>
2026-02-25 15:42:08 -08:00
6e484f14ca Add dragon beast animation on eagle map (#6267)
* 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>
2026-02-25 15:30:43 -08:00
e421bac79c Add Hedrick faction in Kojaria, move Bridget to Alah (#6266)
* 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>
2026-02-25 13:40:03 -08:00
960344a9d6 Fix tutorial reinforcements all arriving at once (#6265)
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>
2026-02-25 12:34:11 -08:00
5eb195188e Fix tutorial province stats not being applied from JSON (#6264)
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>
2026-02-25 11:28:35 -08:00
454aa7edf6 Add weak King's provinces to show collapsing authority in tutorial (#6263)
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>
2026-02-25 11:08:01 -08:00
aa3bcbb25d Fix fear animations and sound routing (#6262)
* 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>
2026-02-25 07:33:33 -08:00
05c1a49645 Balance tutorial battle difficulty and Onmaa resources (#6261)
- 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>
2026-02-25 06:58:47 -08:00
665a4f1e04 Fix tutorial HeroGenerator crash when spawning unaffiliated heroes (#6260)
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>
2026-02-25 06:40:07 -08:00
8fe0779422 Add duel challenge/declined animations and programmatic test buttons (#6259)
* 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>
2026-02-24 21:30:34 -08:00
1456276710 Allow manual workflow_dispatch to deploy Unity builds (#6258)
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>
2026-02-24 21:12:54 -08:00
8ed3a168dc Replace gh CLI with curl in installer artifact cleanup (#6257)
The self-hosted runner doesn't have gh installed. Use curl + GitHub
REST API directly instead.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:12:20 -08:00
6f03ef86f0 Migrate client update system to SHA-addressed blob storage (#6253)
* 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>
2026-02-24 16:50:14 -08:00
d71ad680c5 Add Scala tests for TutorialStrategicEvents (#6256)
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>
2026-02-24 13:51:04 -08:00
a4c8c0670e Add comprehensive tests for TutorialBattleController (#6255)
Add C++ unit tests for the Shardok tutorial battle event system:

- RoundTrigger tests: fires at/after trigger round, multiple events
- UnitsLostTrigger tests: threshold, counts statuses, player filtering
- DamageTakenTrigger tests: threshold, sum across units, initial_sizes
- UnitKilledTrigger tests: by unit_id, by eagle_hero_id
- ReinforcementsAction tests: activates pending units, places directly
- FleeAction tests: generates results, specific units, by hero_id
- Fire-once semantics tests: events don't re-fire
- Controller state tests: disabled controller, default constructor

28 total tests covering all trigger types and action execution.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-24 13:50:42 -08:00
8f219f1f56 Add unit tests for TutorialBattleConfig generation (#6254)
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>
2026-02-24 13:14:02 -08:00
8ce2adfca9 Remove dead battalion backstory update code (#6252)
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>
2026-02-24 09:09:39 -08:00
e2bc6df62e Extend unaffiliated hero backstory visibility to new province owner (#6251)
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>
2026-02-24 07:06:12 -08:00
dc5d342b17 Read What's New data from S3 instead of CDN URL (#6250)
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>
2026-02-24 07:02:46 -08:00
b0ba44c4d5 Add tutorial dialogue when January taxes are collected (#6241)
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>
2026-02-24 06:49:51 -08:00
d3c3df7772 Replace GraphQL branch creation with plain REST API (#6249)
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>
2026-02-24 06:48:40 -08:00
fb2fcf4497 Replace shift-click and Melee button with right-click to melee attack (#6246)
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
7804d6e160 Add tutorial dialogue for expansion guidance (#6242)
When the player has a stable province (40+ support) but only one province,
Old Marek suggests using March to expand to a neighboring province and
explains the sworn sibling/Warlord control mechanic.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:59:28 -08:00
8836c8f65a Add tutorial dialogue for The Eagle's appearance (#6240)
When the TUTORIAL_FACTION_APPEARS action result fires, Old Marek warns
the player about the Fracture Covenant seizing Ingia and Soria, and
introduces The Eagle as a mysterious new threat.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:58:00 -08:00
21891c2627 Use GraphQL API for branch creation in map editor PR flow (#6244)
The REST /git/refs endpoint returns 403 with fine-grained PATs even
when contents:write is granted. Switch to the GraphQL createRef
mutation which handles the same permission correctly.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 06:42:34 -08:00
18c4c08583 Replace low-bravery tutorial heroes with high-bravery ones (#6239)
Marty McPolitics (BRV 8) and Aldric the Overlooked (BRV 14) would
decline duel challenges in the tutorial, frustrating players trying
to learn dueling mechanics. Replace them with Luke the Prank-tricker
(BRV 100) and Lucia, Queen of the Marshes (BRV 100) from
generated_heroes.tsv. Both are noProfession and would still lose
to Sadar Rakon.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 06:29:25 -08:00
5649 changed files with 627931 additions and 316316 deletions
+6 -1
View File
@@ -37,9 +37,14 @@ common:macos --host_linkopt=-Wl,-no_warn_duplicate_libraries
# See: https://github.com/grpc/grpc/issues/37619
common:macos --features=-module_maps
# Fix Xcode version caching issue - avoids need for `bazel clean --expunge` after Xcode updates
# Pin DEVELOPER_DIR so the apple_cc_autoconf repo rule doesn't re-evaluate
# when Xcode updates in-place. The sync script overrides this for mactools.
common:macos --repo_env=DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
# Xcode config for mactools builds only. Generated by scripts/sync_bazel_xcode.sh.
# Bakes the Xcode build version into action cache keys and sets DEVELOPER_DIR.
try-import %workspace%/.bazelrc.xcode
common --java_language_version=25
common --java_runtime_version=remotejdk_25
common --tool_java_language_version=25
+14
View File
@@ -5,12 +5,26 @@
*.tif filter=lfs diff=lfs merge=lfs -text
*.bytes filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.psb filter=lfs diff=lfs merge=lfs -text
# PSB meta files contain bone/sprite import data and can be very large
*.psb.meta filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
# Exclude pre-existing font files that were committed as blobs (not LFS pointers)
src/main/csharp/**/GUI[[:space:]]Pro[[:space:]]Kit*/**/*.ttf !filter !diff !merge
src/main/csharp/**/Modern[[:space:]]UI[[:space:]]Pack/**/*.ttf !filter !diff !merge
*.herodata filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.FBX filter=lfs diff=lfs merge=lfs -text
*.pdf filter=lfs diff=lfs merge=lfs -text
# Third-party asset packs: track all files via LFS (never manually edited)
src/main/csharp/**/DungeonMonsters2D/** filter=lfs diff=lfs merge=lfs -text
src/main/csharp/**/Raccoon/** filter=lfs diff=lfs merge=lfs -text
src/main/csharp/**/Polytope[[:space:]]Studio/** filter=lfs diff=lfs merge=lfs -text
src/main/csharp/**/RRFreelance-Characters/** filter=lfs diff=lfs merge=lfs -text
src/main/csharp/**/Dragon/** filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
# Unity Smart Merge - use UnityYAMLMerge for Unity files
# Requires configuring unityyamlmerge in ~/.gitconfig:
+2 -2
View File
@@ -106,7 +106,7 @@ jobs:
with:
name: test.json
path: test.json
retention-days: 5
retention-days: 3
- name: Archive failed test logs
if: always()
uses: actions/upload-artifact@v4
@@ -114,4 +114,4 @@ jobs:
name: failed-test-logs
path: failed_test_logs/
if-no-files-found: ignore
retention-days: 5
retention-days: 3
+27
View File
@@ -0,0 +1,27 @@
name: Blob Cleanup
on:
schedule:
# Run daily at 04:00 UTC
- cron: '0 4 * * *'
workflow_dispatch: # Allow manual trigger
permissions:
contents: read
jobs:
cleanup:
runs-on: [self-hosted, bazel]
steps:
- uses: actions/checkout@v4
with:
lfs: false
clean: false
- name: Clean up unreferenced blobs
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: |
bazel run //src/main/go/net/eagle0/build/blob_cleanup:blob_cleanup -- --min-age=1h
+18 -2
View File
@@ -40,7 +40,7 @@ concurrency:
permissions:
contents: read
actions: read
actions: write # Required to delete artifacts after deploy
jobs:
# Single consolidated build job - builds all images with one bazel invocation
@@ -76,7 +76,7 @@ jobs:
- name: Fetch LFS files needed for admin server
if: steps.check-latest.outputs.skip != 'true'
run: git lfs pull --include="src/main/go/net/eagle0/admin_server/static/tiles/*"
run: ./ci/github_actions/fetch_lfs.sh --include="src/main/go/net/eagle0/admin_server/static/tiles/*"
- name: Build all Docker images
if: steps.check-latest.outputs.skip != 'true'
@@ -471,3 +471,19 @@ jobs:
# Remove images older than 24h (keeps recent images for rollback)
docker image prune -a -f --filter "until=24h" || true
DEPLOY_SCRIPT
cleanup:
needs: [build-all, deploy]
if: always()
runs-on: ubuntu-latest
steps:
- name: Delete warmup-binary artifact
env:
GH_TOKEN: ${{ github.token }}
run: |
ARTIFACT_ID=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
-q '.artifacts[] | select(.name == "warmup-binary") | .id')
if [ -n "$ARTIFACT_ID" ]; then
echo "Deleting warmup-binary artifact (ID: $ARTIFACT_ID)"
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$ARTIFACT_ID" || true
fi
+21 -6
View File
@@ -112,14 +112,29 @@ jobs:
- name: Delete all installer artifacts
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ github.token }}
run: |
# Delete ALL eagle-installer artifacts to free up storage
echo "Fetching all eagle-installer artifacts..."
artifact_ids=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
--paginate -q '.artifacts[] | select(.name == "eagle-installer") | .id')
for id in $artifact_ids; do
echo "Deleting artifact ID: $id"
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" || true
page=1
while true; do
response=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?per_page=100&page=$page")
ids=$(echo "$response" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for a in data.get('artifacts', []):
if a['name'] == 'eagle-installer':
print(a['id'])
" 2>/dev/null)
if [ -z "$ids" ]; then
break
fi
for id in $ids; do
echo "Deleting artifact ID: $id"
curl -s -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts/$id" || true
done
page=$((page + 1))
done
echo "Cleanup complete"
@@ -1,98 +0,0 @@
name: iOS Addressables Build
on:
push:
branches: [ "main" ]
paths:
- ".github/workflows/ios_addressables_build.yml"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/AddressableAssetsData/**"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Music/**"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Editor/BuildScript.cs"
- "ci/github_actions/build_ios_addressables.sh"
- "ci/github_actions/upload_addressables.sh"
pull_request:
paths:
- ".github/workflows/ios_addressables_build.yml"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/AddressableAssetsData/**"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Music/**"
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Editor/BuildScript.cs"
- "ci/github_actions/build_ios_addressables.sh"
- "ci/github_actions/upload_addressables.sh"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
# Runner-specific build directory to allow parallel builds on multiple runners
EAGLE0_BUILD_DIR: /tmp/eagle0-${{ github.run_id }}
jobs:
build-ios-addressables:
runs-on: [self-hosted, macOS, unity-mac]
steps:
- name: Prune stale PR refs
run: |
# Self-hosted runners persist .git between runs. When a PR is updated,
# old local refs (refs/remotes/pull/*/merge) may point to commits whose
# objects were never fetched or have been pruned. Remove these stale refs
# before checkout to prevent "missing object" errors.
if [ -d ".git" ]; then
echo "Pruning stale PR refs..."
git for-each-ref --format='%(refname)' refs/remotes/pull/ 2>/dev/null | \
xargs -r git update-ref -d 2>/dev/null || true
fi
- uses: actions/checkout@v4
with:
lfs: false # Fetch LFS after checkout to avoid stale ref issues
clean: false # Library/ persists between runs on self-hosted runners
- name: Clean stale files
run: |
git clean -ffd
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
- name: Fetch LFS files
run: |
git lfs install
git lfs pull
- name: Ensure Unity version installed
run: ./ci/github_actions/ensure_unity_installed.sh ios
- name: Build iOS Addressables
run: ./ci/github_actions/build_ios_addressables.sh "$EAGLE0_BUILD_DIR/editor_ios_addressables.log"
- name: Upload Addressables to CDN
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: ./ci/github_actions/upload_addressables.sh iOS
- name: Purge CDN cache for iOS addressables
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
env:
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
run: |
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
-H "Authorization: Bearer $DO_CDN_PAT" \
-H "Content-Type: application/json" \
-d '{"files": ["addressables/iOS/*"]}' \
--fail || echo "Warning: CDN purge failed (non-fatal)"
- name: Archive Build Log
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: editor_ios_addressables.log
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_ios_addressables.log
retention-days: 5
- name: Cleanup build directory
if: always()
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
+100 -4
View File
@@ -1,6 +1,8 @@
name: iOS TestFlight
on:
schedule:
- cron: '0 5 * * *' # 9 PM Pacific (UTC-8) / 10 PM PDT (UTC-7)
workflow_dispatch:
inputs:
skip_upload:
@@ -11,6 +13,7 @@ on:
permissions:
contents: read
actions: read
env:
# Runner-specific build directory to allow parallel builds on multiple runners
@@ -19,7 +22,92 @@ env:
KEYCHAIN_NAME: ios-build-${{ github.run_id }}.keychain
jobs:
check-changes:
# Skip nightly builds when nothing has changed since the last successful
# scheduled run. Manual workflow_dispatch always builds.
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.check.outputs.should_build }}
steps:
- name: Check for relevant changes since last successful run
id: check
uses: actions/github-script@v8
with:
script: |
if (context.eventName === 'workflow_dispatch') {
core.setOutput('should_build', 'true');
console.log('Manual trigger — building');
return;
}
const { data: { workflow_runs: runs } } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'ios_testflight.yml',
status: 'success',
event: 'schedule',
per_page: 1,
});
if (runs.length === 0) {
core.setOutput('should_build', 'true');
console.log('No previous successful scheduled run — building');
return;
}
const lastSha = runs[0].head_sha;
console.log(`Last successful scheduled run: ${lastSha}`);
console.log(`Current SHA: ${context.sha}`);
if (lastSha === context.sha) {
core.setOutput('should_build', 'false');
console.log('No changes since last successful run — skipping');
return;
}
// Paths that can affect the iOS Unity build.
const relevantPrefixes = [
'src/main/csharp/', // Unity project
'src/main/protobuf/', // Generates C# code
'src/main/resources/', // Game data & maps
'scripts/', // Build scripts
'ci/', // CI scripts & workflows
'.github/workflows/ios_testflight.yml',
'MODULE.bazel',
'.bazelrc',
];
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: `${lastSha}...${context.sha}`,
per_page: 1, // We only need the file list, not patches
});
// If the diff is too large, build to be safe
if (comparison.files === undefined) {
core.setOutput('should_build', 'true');
console.log('Could not retrieve file list — building to be safe');
return;
}
const relevant = comparison.files.filter(f =>
relevantPrefixes.some(p => f.filename.startsWith(p))
);
if (relevant.length > 0) {
core.setOutput('should_build', 'true');
console.log(`${relevant.length} relevant file(s) changed:`);
relevant.slice(0, 20).forEach(f => console.log(` ${f.filename}`));
if (relevant.length > 20) console.log(` ... and ${relevant.length - 20} more`);
} else {
core.setOutput('should_build', 'false');
console.log(`${comparison.files.length} file(s) changed, none in relevant paths — skipping`);
}
build-unity:
needs: check-changes
if: needs.check-changes.outputs.should_build == 'true'
runs-on: [self-hosted, macOS, testflight]
steps:
@@ -46,9 +134,7 @@ jobs:
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
- name: Fetch LFS files
run: |
git lfs install
git lfs pull
run: ./ci/github_actions/fetch_lfs.sh
- name: Ensure Unity version installed
run: ./ci/github_actions/ensure_unity_installed.sh ios
@@ -82,7 +168,7 @@ jobs:
with:
name: editor_ios.log
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_ios.log
retention-days: 5
retention-days: 3
archive-and-upload:
needs: build-unity
@@ -140,10 +226,20 @@ jobs:
- name: Archive and Export IPA
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
run: |
# Write API key to file for xcodebuild
API_KEY_PATH=$(mktemp)
echo "$APP_STORE_CONNECT_API_KEY" > "$API_KEY_PATH"
export APP_STORE_CONNECT_API_KEY_PATH="$API_KEY_PATH"
chmod +x ./ci/github_actions/archive_ios.sh
./ci/github_actions/archive_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS" "$EAGLE0_BUILD_DIR/archive" "$APPLE_TEAM_ID" "$PROFILE_UUID"
rm -f "$API_KEY_PATH"
- name: Upload to TestFlight
if: ${{ github.event.inputs.skip_upload != 'true' }}
env:
+9 -4
View File
@@ -32,6 +32,7 @@ on:
- "src/main/go/net/eagle0/build/mac_build_handler/**"
- "scripts/build_mac_plugin.sh"
- "scripts/build_sparkle_plugin.sh"
- "src/main/objc/net/eagle0/clients/unity/sparkle/**"
- "scripts/inject_sparkle.sh"
- "scripts/codesign_mac_app.sh"
- "scripts/notarize_submit.sh"
@@ -108,13 +109,14 @@ jobs:
fi
- name: Fetch LFS files
run: |
git lfs install
git lfs pull
run: ./ci/github_actions/fetch_lfs.sh
- name: Ensure Unity version installed
run: ./ci/github_actions/ensure_unity_installed.sh mac
- name: Sync Bazel Xcode config
run: ./scripts/sync_bazel_xcode.sh
- name: Build Mac Unity
run: ./ci/github_actions/build_unity_mac.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC"
@@ -237,7 +239,7 @@ jobs:
with:
name: editor_mac.log
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_mac.log
retention-days: 5
retention-days: 3
- name: Cleanup build directory
if: always()
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
@@ -320,6 +322,9 @@ jobs:
ditto -x -k eagle0.app.zip .
rm eagle0.app.zip
- name: Sync Bazel Xcode config
run: ./scripts/sync_bazel_xcode.sh
- name: Deploy Mac Build
id: deploy-mac
env:
+6 -16
View File
@@ -96,9 +96,7 @@ jobs:
fi
- name: Fetch LFS files
run: |
git lfs install
git lfs pull
run: ./ci/github_actions/fetch_lfs.sh
- name: Ensure Unity version installed
run: ./ci/github_actions/ensure_unity_installed.sh windows
- name: Build Windows unity
@@ -115,14 +113,14 @@ jobs:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: ./ci/github_actions/upload_addressables.sh StandaloneWindows64
- name: Deploy Windows unity
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" "/tmp/unity_manifest.txt"
- name: Update unified manifest
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
@@ -147,7 +145,7 @@ jobs:
- name: Export deployed version
id: get-version
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
run: |
VERSION=$(grep "^version=" /tmp/unity_manifest.txt | cut -d= -f2 || date +%Y.%m.%d)
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
@@ -158,7 +156,7 @@ jobs:
with:
name: editor_win.log
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_win.log
retention-days: 5
retention-days: 3
- name: Cleanup build directory
if: always()
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
@@ -168,18 +166,10 @@ jobs:
if: needs.windows-unity.outputs.deployed_version != ''
runs-on: ubuntu-latest
steps:
- name: Purge CDN cache and notify clients
- name: Notify clients of update
env:
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
run: |
# Purge CDN cache so clients get the latest files immediately
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
-H "Authorization: Bearer $DO_CDN_PAT" \
-H "Content-Type: application/json" \
-d '{"files": ["windows/*", "addressables/StandaloneWindows64/*"]}' \
--fail || echo "Warning: CDN purge failed (non-fatal)"
curl -X POST "https://admin.eagle0.net/notify-update?platform=windows&version=${{ needs.windows-unity.outputs.deployed_version }}&required=false" \
-H "X-Notify-Secret: $NOTIFY_SECRET" \
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
+1
View File
@@ -23,6 +23,7 @@ bazel-bin
bazel-eagle0*
bazel-out
bazel-testlogs
.bazelrc.xcode
.ijwb
.clwb
buildWin.sh
+10 -1
View File
@@ -14,7 +14,16 @@ repos:
- id: clang-format
args: [-i, --no-diff]
types_or: ["c++", "c#"]
exclude: ^src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins
exclude: >-
(?x)^src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/(
Plugins|
DungeonMonsters2D|
Dragon|
ithappy|
Polytope\ Studio|
RRFreelance-Characters|
Raccoon
)/
- repo: https://github.com/yoheimuta/protolint
rev: v0.42.2
hooks:
+6
View File
@@ -1,7 +1,13 @@
# CLAUDE.md
## CRITICAL BASH RULES (NEVER VIOLATE)
**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.
+47 -18
View File
@@ -149,38 +149,54 @@ use_repo(crate, "map_generator_crates")
#
# Platform Support - Apple/iOS
#
# Note: rules_apple is NOT included in the main workspace due to rules_swift
# version conflicts with grpc. SparklePlugin (which needs rules_apple) is built
# in a separate workspace at sparkle_workspace/ to isolate the conflict.
#
bazel_dep(name = "apple_support", version = "1.24.1", repo_name = "build_bazel_apple_support")
# rules_swift is a transitive dep of grpc and flatbuffers with conflicting versions:
# - grpc 1.76.0.bcr.1 requires rules_swift 3.x (compatibility level 3)
# - flatbuffers requires rules_swift 2.x (compatibility level 2)
# We don't use Swift directly, but we need to force a single version.
# Force 3.x since grpc is more complex and harder to downgrade.
single_version_override(
module_name = "rules_swift",
version = "3.1.2",
)
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
# Register Apple CC toolchain for Objective-C compilation
apple_cc_configure = use_extension(
"@build_bazel_apple_support//crosstool:setup.bzl",
"apple_cc_configure_extension",
)
use_repo(apple_cc_configure, "local_config_apple_cc")
use_repo(apple_cc_configure, "local_config_apple_cc", "local_config_apple_cc_toolchains")
#
# Protocol Buffers & RPC
#
bazel_dep(name = "protobuf", version = "33.5", repo_name = "com_google_protobuf")
# Use pre-built protoc binaries instead of compiling from source.
# See: https://protobuf.dev/reference/cpp/cpp-generated/#invocation
prebuilt_protoc = use_extension("@com_google_protobuf//bazel/private:prebuilt_protoc_extension.bzl", "protoc")
use_repo(
prebuilt_protoc,
"prebuilt_protoc.linux_aarch_64",
"prebuilt_protoc.linux_ppcle_64",
"prebuilt_protoc.linux_s390_64",
"prebuilt_protoc.linux_x86_32",
"prebuilt_protoc.linux_x86_64",
"prebuilt_protoc.osx_aarch_64",
"prebuilt_protoc.osx_x86_64",
"prebuilt_protoc.win32",
"prebuilt_protoc.win64",
)
bazel_dep(name = "rules_proto", version = "7.1.0")
bazel_dep(name = "rules_cc", version = "0.2.14")
bazel_dep(name = "grpc", version = "1.74.1")
bazel_dep(name = "grpc", version = "1.78.0")
# Patch grpc 1.78.0 to fix rules_python incompatibility: grpc requests Python
# 3.14.0b2 toolchain but rules_python >= 1.6.0 removed it in favor of 3.14.
# Upstream fix: https://github.com/grpc/grpc/commit/459279b
# TODO: Remove this override once a fixed grpc version is published to BCR.
single_version_override(
module_name = "grpc",
patch_strip = 1,
patches = ["//third_party/patches:grpc_fix_python_version.patch"],
version = "1.78.0",
)
bazel_dep(name = "grpc-java", version = "1.78.0")
bazel_dep(name = "rules_proto_grpc_csharp", version = "5.8.0")
bazel_dep(name = "flatbuffers", version = "25.12.19")
@@ -384,8 +400,16 @@ http_archive(
],
)
# Note: Sparkle framework is defined in sparkle_workspace/MODULE.bazel
# (not in main workspace due to rules_swift version conflicts)
# Sparkle framework for macOS auto-updates
SPARKLE_VERSION = "2.6.4"
http_archive(
name = "sparkle",
build_file = "//src/main/objc/net/eagle0/clients/unity/sparkle/external:BUILD.sparkle",
sha256 = "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b",
strip_prefix = "",
url = "https://github.com/sparkle-project/Sparkle/releases/download/%s/Sparkle-%s.tar.xz" % (SPARKLE_VERSION, SPARKLE_VERSION),
)
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
# Primary: DigitalOcean Spaces (public, reliable)
@@ -475,6 +499,11 @@ register_toolchains(
"@rules_scala//testing:scalatest_toolchain",
)
# Apple CC toolchain for Objective-C compilation (SparklePlugin).
# Registered before LLVM so it has higher priority; Apple constraint matching
# ensures it's only used for Apple-platform targets (objc_library, macos_bundle).
register_toolchains("@local_config_apple_cc_toolchains//:all")
# Set dev_dependency so we can turn this off for swift MacOS builds
register_toolchains(
"@llvm_toolchain//:all",
+185 -236
View File
@@ -4,15 +4,12 @@
"https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
"https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
"https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
"https://bcr.bazel.build/modules/abseil-cpp/20220623.1/MODULE.bazel": "73ae41b6818d423a11fd79d95aedef1258f304448193d4db4ff90e5e7a0f076c",
"https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.0/MODULE.bazel": "98dc378d64c12a4e4741ad3362f87fb737ee6a0886b2d90c3cdbb4d93ea3e0bf",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16",
"https://bcr.bazel.build/modules/abseil-cpp/20240722.0/MODULE.bazel": "88668a07647adbdc14cb3a7cd116fb23c9dda37a90a1681590b6c9d8339a5b84",
"https://bcr.bazel.build/modules/abseil-cpp/20250127.0/MODULE.bazel": "d1086e248cda6576862b4b3fe9ad76a214e08c189af5b42557a6e1888812c5d5",
"https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1",
"https://bcr.bazel.build/modules/abseil-cpp/20250512.0/MODULE.bazel": "c4d02dd22cd87458516655a45512060246ee2a4732f1fbe948a5bd9eb614e626",
@@ -25,7 +22,7 @@
"https://bcr.bazel.build/modules/apple_support/1.13.0/MODULE.bazel": "7c8cdea7e031b7f9f67f0b497adf6d2c6a2675e9304ca93a9af6ed84eef5a524",
"https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85",
"https://bcr.bazel.build/modules/apple_support/1.17.1/MODULE.bazel": "655c922ab1209978a94ef6ca7d9d43e940cd97d9c172fb55f94d91ac53f8610b",
"https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e",
"https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1",
"https://bcr.bazel.build/modules/apple_support/1.23.0/MODULE.bazel": "317d47e3f65b580e7fb4221c160797fda48e32f07d2dfff63d754ef2316dcd25",
"https://bcr.bazel.build/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442",
"https://bcr.bazel.build/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8",
@@ -67,6 +64,7 @@
"https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a",
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
"https://bcr.bazel.build/modules/bazel_features/1.32.0/MODULE.bazel": "095d67022a58cb20f7e20e1aefecfa65257a222c18a938e2914fd257b5f1ccdc",
"https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6",
@@ -102,7 +100,6 @@
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.4/MODULE.bazel": "460aa12d01231a80cce03c548287b433b321d205b0028ae596728c35e5ee442e",
"https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/MODULE.bazel": "82494a01018bb7ef06d4a17ec4cd7a758721f10eb8b6c820a818e70d669500db",
"https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/source.json": "a2d30458fd86cf022c2b6331e652526fa08e17573b2f5034a9dbcacdf9c2583c",
"https://bcr.bazel.build/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "6ee6353f8b1a701fe2178e1d925034294971350b6d3ac37e67e5a7d463267834",
"https://bcr.bazel.build/modules/boringssl/0.0.0-20230215-5c22014/MODULE.bazel": "4b03dc0d04375fa0271174badcd202ed249870c8e895b26664fd7298abea7282",
"https://bcr.bazel.build/modules/boringssl/0.0.0-20240530-2db0eb3/MODULE.bazel": "d0405b762c5e87cd445b7015f2b8da5400ef9a8dbca0bfefa6c1cea79d528a97",
"https://bcr.bazel.build/modules/boringssl/0.20240913.0/MODULE.bazel": "fcaa7503a5213290831a91ed1eb538551cf11ac0bc3a6ad92d0fef92c5bd25fb",
@@ -110,14 +107,12 @@
"https://bcr.bazel.build/modules/boringssl/0.20241024.0/source.json": "d843092e682b84188c043ac742965d7f96e04c846c7e338187e03238674909a9",
"https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
"https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8",
"https://bcr.bazel.build/modules/c-ares/1.15.0/MODULE.bazel": "ba0a78360fdc83f02f437a9e7df0532ad1fbaa59b722f6e715c11effebaa0166",
"https://bcr.bazel.build/modules/c-ares/1.19.1/MODULE.bazel": "73bca21720772370ff91cc8e88bbbaf14897720c6473e87c1ddc0f848284c313",
"https://bcr.bazel.build/modules/c-ares/1.19.1/source.json": "56bfa95b01e4e0012e90eaa9f1bab823c48c3af4454286b889b9cc74f5098da1",
"https://bcr.bazel.build/modules/cel-spec/0.15.0/MODULE.bazel": "e1eed53d233acbdcf024b4b0bc1528116d92c29713251b5154078ab1348cb600",
"https://bcr.bazel.build/modules/cel-spec/0.15.0/source.json": "ab7dccdf21ea2261c0f809b5a5221a4d7f8b580309f285fdf1444baaca75d44a",
"https://bcr.bazel.build/modules/civetweb/1.16/MODULE.bazel": "46a38f9daeb57392e3827fce7d40926be0c802bd23cdd6bfd3a96c804de42fae",
"https://bcr.bazel.build/modules/civetweb/1.16/source.json": "ba8b9585adb8355cb51b999d57172fd05e7a762c56b8d4bac6db42c99de3beb7",
"https://bcr.bazel.build/modules/curl/8.4.0/MODULE.bazel": "0bc250aa1cb69590049383df7a9537c809591fcf876c620f5f097c58fdc9bc10",
"https://bcr.bazel.build/modules/curl/8.7.1/MODULE.bazel": "088221c35a2939c555e6e47cb31a81c15f8b59f4daa8009b1e9271a502d33485",
"https://bcr.bazel.build/modules/curl/8.8.0/MODULE.bazel": "7da3b3e79b0b4ee8f8c95d640bc6ad7b430ce66ef6e9c9d2bc29b3b5ef85f6fe",
"https://bcr.bazel.build/modules/curl/8.8.0/source.json": "d7d138b6878cf38891692fee0649ace35357fd549b425614d571786f054374d4",
@@ -142,36 +137,33 @@
"https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6",
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
"https://bcr.bazel.build/modules/google_benchmark/1.8.4/MODULE.bazel": "c6d54a11dcf64ee63545f42561eda3fd94c1b5f5ebe1357011de63ae33739d5e",
"https://bcr.bazel.build/modules/google_benchmark/1.8.5/MODULE.bazel": "9ba9b31b984022828a950e3300410977eda2e35df35584c6b0b2d0c2e52766b7",
"https://bcr.bazel.build/modules/google_benchmark/1.8.5/source.json": "2c9c685f9b496f125b9e3a9c696c549d1ed2f33b75830a2fb6ac94fab23c0398",
"https://bcr.bazel.build/modules/google_benchmark/1.8.4/source.json": "84590f7bc5a1fd99e1ef274ee16bb41c214f705e62847b42e705010dfa81fe53",
"https://bcr.bazel.build/modules/googleapis-cc/1.0.0/MODULE.bazel": "cf01757e7590c56140a4b81638ff2b3e7074769e6271720bbf738fcda25b6fc2",
"https://bcr.bazel.build/modules/googleapis-cc/1.0.0/source.json": "ab0e3a2ee9968a8848f59872fbbfa3e1f768597d71d2229e6caa319d357967c7",
"https://bcr.bazel.build/modules/googleapis-grpc-cc/1.0.0/MODULE.bazel": "3553358a9d8d96026c9e28d9fb6c268574950d0be7fa9b4c0aeaf3c37c73f2d3",
"https://bcr.bazel.build/modules/googleapis-grpc-cc/1.0.0/source.json": "fa7b79043b3c82bf74f1f2fa45af289e19b247375868d0752db2c114a1c7366c",
"https://bcr.bazel.build/modules/googleapis-python/1.0.0/MODULE.bazel": "0ccd1614a914fb524b3ac267f9c97f9a5cd5412b027f0176b81a725882ec42ff",
"https://bcr.bazel.build/modules/googleapis-python/1.0.0/source.json": "24364f075ec5e6d5e0cfc5d651bd833b119e0cea1a45d51330c93461e3586e42",
"https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/MODULE.bazel": "97c6a4d413b373d4cc97065da3de1b2166e22cbbb5f4cc9f05760bfa83619e24",
"https://bcr.bazel.build/modules/googleapis-rules-registry/1.0.0/source.json": "cf611c836a60e98e2e2ab2de8004f119e9f06878dcf4ea2d95a437b1b7a89fe9",
"https://bcr.bazel.build/modules/googleapis/0.0.0-20240326-1c8d509c5/MODULE.bazel": "a4b7e46393c1cdcc5a00e6f85524467c48c565256b22b5fae20f84ab4a999a68",
"https://bcr.bazel.build/modules/googleapis/0.0.0-20240819-fe8ba054a/MODULE.bazel": "117b7c7be7327ed5d6c482274533f2dbd78631313f607094d4625c28203cacdf",
"https://bcr.bazel.build/modules/googleapis/0.0.0-20240819-fe8ba054a/source.json": "b31fc7eb283a83f71d2e5bfc3d1c562d2994198fa1278409fbe8caec3afc1d3e",
"https://bcr.bazel.build/modules/googleapis/0.0.0-20241220-5e258e33.bcr.1/MODULE.bazel": "ee6c30f82ecd476e61f019fb1151aaab380ea419958ff274ef2f0efca7969f5c",
"https://bcr.bazel.build/modules/googleapis/0.0.0-20241220-5e258e33.bcr.1/source.json": "d6f66e3d95ec52821e994015e83ed194f8888c655068e192659e55a8987dfe77",
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
"https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108",
"https://bcr.bazel.build/modules/googletest/1.16.0/MODULE.bazel": "a175623c69e94fca4ca7acbc12031e637b0c489318cd4805606981d4d7adb34a",
"https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46",
"https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713",
"https://bcr.bazel.build/modules/grpc-java/1.62.2/MODULE.bazel": "99b8771e8c7cacb130170fed2a10c9e8fed26334a93e73b42d2953250885a158",
"https://bcr.bazel.build/modules/grpc-java/1.66.0/MODULE.bazel": "86ff26209fac846adb89db11f3714b3dc0090fb2fb81575673cc74880cda4e7e",
"https://bcr.bazel.build/modules/grpc-java/1.69.0/MODULE.bazel": "53887af6a00b3b406d70175d3d07e84ea9362016ff55ea90b9185f0227bfaf98",
"https://bcr.bazel.build/modules/grpc-java/1.78.0/MODULE.bazel": "48f790fbb95625245295df1283e0dba344a4e30b4a9a9cefbabfa92bd84f3691",
"https://bcr.bazel.build/modules/grpc-java/1.78.0/source.json": "36c3a83c5ddeb864d7a4b97801456c464077f80a8216354ac1a71f7ce8699771",
"https://bcr.bazel.build/modules/grpc-proto/0.0.0-20240627-ec30f58/MODULE.bazel": "88de79051e668a04726e9ea94a481ec6f1692086735fd6f488ab908b3b909238",
"https://bcr.bazel.build/modules/grpc-proto/0.0.0-20240627-ec30f58/source.json": "5035d379c61042930244ab59e750106d893ec440add92ec0df6a0098ca7f131d",
"https://bcr.bazel.build/modules/grpc/1.41.0/MODULE.bazel": "5bcbfc2b274dabea628f0649dc50c90cf36543b1cfc31624832538644ad1aae8",
"https://bcr.bazel.build/modules/grpc/1.56.3.bcr.1/MODULE.bazel": "cd5b1eb276b806ec5ab85032921f24acc51735a69ace781be586880af20ab33f",
"https://bcr.bazel.build/modules/grpc/1.62.1/MODULE.bazel": "2998211594b8a79a6b459c4e797cfa19f0fb8b3be3149760ec7b8c99abfd426f",
"https://bcr.bazel.build/modules/grpc/1.63.1.bcr.1/MODULE.bazel": "d7b9fef03bd175e6825237b521b18a3c29f1ac15f8aa52c8a1a0f3bd8f33d54b",
"https://bcr.bazel.build/modules/grpc/1.66.0.bcr.2/MODULE.bazel": "0fa2b0fd028ce354febf0fe90f1ed8fecfbfc33118cddd95ac0418cc283333a0",
"https://bcr.bazel.build/modules/grpc/1.66.0.bcr.3/MODULE.bazel": "f6047e89faf488f5e3e65cb2594c6f5e86992abec7487163ff6b623526e543b0",
"https://bcr.bazel.build/modules/grpc/1.69.0/MODULE.bazel": "4e26e05c9e1ef291ccbc96aad8e457b1b8abedbc141623831629da2f8168eef6",
"https://bcr.bazel.build/modules/grpc/1.70.1/MODULE.bazel": "b800cd8e3e7555c1e61cba2e02d3a2fcf0e91f66e800db286d965d3b7a6a721a",
"https://bcr.bazel.build/modules/grpc/1.71.0/MODULE.bazel": "7fcab2c05530373f1a442c362b17740dd0c75b6a2a975eec8f5bf4c70a37928a",
"https://bcr.bazel.build/modules/grpc/1.74.1/MODULE.bazel": "09523be10ba2bfd999683671d0f8f22fb5b20ec77ad89b05ef58ff19a1b65c82",
"https://bcr.bazel.build/modules/grpc/1.74.1/source.json": "8508bcf9bae1b7c647a594e13461ce192240fcdbb409c2741444322d47d01f98",
"https://bcr.bazel.build/modules/grpc/1.78.0/MODULE.bazel": "6a72f0b2fe950342fe63b231c76197df6d117a8ea1ac338dac40b053020f586a",
"https://bcr.bazel.build/modules/grpc/1.78.0/source.json": "c988abfe519ef5ace8cd27c80c0a66e68e501e5902b623722fcbf6694b355693",
"https://bcr.bazel.build/modules/helly25_bzl/0.3.1/MODULE.bazel": "3a4be20f6fc13be32ad44643b8252ef5af09eee936f1d943cd4fd7867fa92826",
"https://bcr.bazel.build/modules/helly25_bzl/0.3.1/source.json": "b129ab1828492de2c163785bbeb4065c166de52d932524b4317beb5b7f917994",
"https://bcr.bazel.build/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f",
@@ -188,18 +180,13 @@
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74",
"https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de.bcr.2/MODULE.bazel": "cc18734138dd18c912c6ce2a59186db28f85d8058c99c9f21b46ca3e0aba0ebe",
"https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de.bcr.2/source.json": "7c135f9d42bb3b045669c3c6ab3bb3c208e00b46aca4422eea64c29811a5b240",
"https://bcr.bazel.build/modules/opencensus-cpp/0.0.0-20230502-50eb5de/MODULE.bazel": "02201d2921dadb4ec90c4980eca4b2a02904eddcf6fa02f3da7594fb7b0d821c",
"https://bcr.bazel.build/modules/opencensus-proto/0.4.1.bcr.2/MODULE.bazel": "789706a714855f92c5c8cfcf1ef32bbb64dcd3b7c9066756ad7986ec59709d29",
"https://bcr.bazel.build/modules/opencensus-proto/0.4.1.bcr.2/source.json": "aadf3f53e08b72376506b7c4ea3d167010c9efb160d7d6e1e304ed646bac1b36",
"https://bcr.bazel.build/modules/opencensus-proto/0.4.1/MODULE.bazel": "4a2e8b4d0b544002502474d611a5a183aa282251e14f6a01afe841c0c1b10372",
"https://bcr.bazel.build/modules/openssl/3.3.1.bcr.1/MODULE.bazel": "49c0c07e8fb87b480bccb842cfee1b32617f11dac590f732573c69058699a3d1",
"https://bcr.bazel.build/modules/openssl/3.3.1.bcr.1/source.json": "0c0872e048bbea052a9c541fb47019481a19201ba5555a71d762ad591bf94e1f",
"https://bcr.bazel.build/modules/opentelemetry-cpp/1.14.2/MODULE.bazel": "089a5613c2a159c7dfde098dabfc61e966889c7d6a81a98422a84c51535ed17d",
"https://bcr.bazel.build/modules/opentelemetry-cpp/1.16.0/MODULE.bazel": "b7379a140f538cea3f749179a2d481ed81942cc6f7b05a6113723eb34ac3b3e7",
"https://bcr.bazel.build/modules/opentelemetry-cpp/1.19.0/MODULE.bazel": "3455326c08b28415648a3d60d8e3c811847ebdbe64474f75b25878f25585aea1",
"https://bcr.bazel.build/modules/opentelemetry-cpp/1.19.0/source.json": "4e48137e4c3ecb99401ff99876df8fa330598d7da051869bec643446e8a8ff95",
"https://bcr.bazel.build/modules/opentelemetry-proto/1.1.0/MODULE.bazel": "a49f406e99bf05ab43ed4f5b3322fbd33adfd484b6546948929d1316299b68bf",
"https://bcr.bazel.build/modules/opentelemetry-proto/1.3.1/MODULE.bazel": "0141a50e989576ee064c11ce8dd5ec89993525bd9f9a09c5618e4dacc8df9352",
"https://bcr.bazel.build/modules/opentelemetry-proto/1.4.0.bcr.1/MODULE.bazel": "5ceaf25e11170d22eded4c8032728b4a3f273765fccda32f9e94f463755c4167",
"https://bcr.bazel.build/modules/opentelemetry-proto/1.5.0/MODULE.bazel": "7543d91a53b98e7b5b37c5a0865b93bff12c1ee022b1e322cd236b968894b030",
"https://bcr.bazel.build/modules/opentelemetry-proto/1.5.0/source.json": "046b721ce203e88cdaad44d7dd17a86b7200eab9388b663b234e72e13ff7b143",
@@ -219,7 +206,6 @@
"https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc",
"https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580",
"https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96",
"https://bcr.bazel.build/modules/prometheus-cpp/1.2.4/MODULE.bazel": "0fbe5dcff66311947a3f6b86ebc6a6d9328e31a28413ca864debc4a043f371e5",
"https://bcr.bazel.build/modules/prometheus-cpp/1.3.0.bcr.1/MODULE.bazel": "116ad46e97c1d2aeb020fe2899a342a7e703574ce7c0faf7e4810f938c974a9a",
"https://bcr.bazel.build/modules/prometheus-cpp/1.3.0.bcr.1/source.json": "e813cce2d450708cfcb26e309c5172583a7440776edf354e83e6788c768e5cca",
"https://bcr.bazel.build/modules/prometheus-cpp/1.3.0/MODULE.bazel": "ce82e086bbc0b60267e970f6a54b2ca6d0f22d3eb6633e00e2cc2899c700f3d8",
@@ -227,8 +213,6 @@
"https://bcr.bazel.build/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a",
"https://bcr.bazel.build/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12",
"https://bcr.bazel.build/modules/protobuf/25.6/MODULE.bazel": "fc0ae073b47c7ede88b825ff79e64f1c058967c7a87a86cdf4abecd9e0516625",
"https://bcr.bazel.build/modules/protobuf/26.0.bcr.1/MODULE.bazel": "8f04d38c2da40a3715ff6bdce4d32c5981e6432557571482d43a62c31a24c2cf",
"https://bcr.bazel.build/modules/protobuf/26.0.bcr.2/MODULE.bazel": "62e0b84ca727bdeb55a6fe1ef180e6b191bbe548a58305ea1426c158067be534",
"https://bcr.bazel.build/modules/protobuf/26.0/MODULE.bazel": "8402da964092af40097f4a205eec2a33fd4a7748dc43632b7d1629bfd9a2b856",
"https://bcr.bazel.build/modules/protobuf/27.0-rc2/MODULE.bazel": "b2b0dbafd57b6bec0ca9b251da02e628c357dab53a097570aa7d79d020f107cf",
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
@@ -252,7 +236,6 @@
"https://bcr.bazel.build/modules/protoc-gen-validate/1.0.4/MODULE.bazel": "b8913c154b16177990f6126d2d2477d187f9ddc568e95ee3e2d50fc65d2c494a",
"https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1.bcr.1/MODULE.bazel": "4bf09676b62fa587ae07e073420a76ec8766dcce7545e5f8c68cfa8e484b5120",
"https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1.bcr.1/source.json": "c19071ebc4b53b5f1cfab9c66eefaf6e4179eb8a998970d07b1077687e777f29",
"https://bcr.bazel.build/modules/protoc-gen-validate/1.2.1/MODULE.bazel": "52b51f50533ec4fbd5d613cd093773f979ac2e035d954e02ca11de383f502505",
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
"https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34",
"https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680",
@@ -269,8 +252,8 @@
"https://bcr.bazel.build/modules/rules_android/0.6.6/source.json": "a9d8dc2d5a102dc03269a94acc886a4cab82cdcb9ccbc77b0f665d6d17a6ae09",
"https://bcr.bazel.build/modules/rules_apple/3.13.0/MODULE.bazel": "b4559a2c6281ca3165275bb36c1f0ac74666632adc5bdb680e366de7ce845f43",
"https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
"https://bcr.bazel.build/modules/rules_apple/3.16.0/source.json": "d8b5fe461272018cc07cfafce11fe369c7525330804c37eec5a82f84cd475366",
"https://bcr.bazel.build/modules/rules_apple/3.5.1/MODULE.bazel": "3d1bbf65ad3692003d36d8a29eff54d4e5c1c5f4bfb60f79e28646a924d9101c",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/MODULE.bazel": "c5c2c4adeeac5f3f2f9b7f16abfa8be7ffefa596171d0d92bed4cae9ade0a498",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/source.json": "3cb1d69c8243ffcc42ecbf84ae8b9cccd7b1e2f091b0aee5a3e9c9a45267f312",
"https://bcr.bazel.build/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162",
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
@@ -313,7 +296,6 @@
"https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae",
"https://bcr.bazel.build/modules/rules_go/0.59.0/source.json": "1df17bb7865cfc029492c30163cee891d0dd8658ea0d5bfdf252c4b6db5c1ef6",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.1.0/MODULE.bazel": "324b6478b0343a3ce7a9add8586ad75d24076d6d43d2f622990b9c1cfd8a1b15",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
"https://bcr.bazel.build/modules/rules_java/5.5.0/MODULE.bazel": "486ad1aa15cdc881af632b4b1448b0136c76025a1fe1ad1b65c5899376b83a50",
"https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39",
@@ -391,18 +373,18 @@
"https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
"https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
"https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
"https://bcr.bazel.build/modules/rules_python/0.29.0/MODULE.bazel": "2ac8cd70524b4b9ec49a0b8284c79e4cd86199296f82f6e0d5da3f783d660c82",
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
"https://bcr.bazel.build/modules/rules_python/0.32.2/MODULE.bazel": "01052470fc30b49de91fb8483d26bea6f664500cfad0b078d4605b03e3a83ed4",
"https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937",
"https://bcr.bazel.build/modules/rules_python/0.37.1/MODULE.bazel": "3faeb2d9fa0a81f8980643ee33f212308f4d93eea4b9ce6f36d0b742e71e9500",
"https://bcr.bazel.build/modules/rules_python/0.37.2/MODULE.bazel": "b5ffde91410745750b6c13be1c5dc4555ef5bc50562af4a89fd77807fdde626a",
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
"https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7",
"https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
"https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
"https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6",
"https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8",
"https://bcr.bazel.build/modules/rules_python/1.6.0/source.json": "e980f654cf66ec4928672f41fc66c4102b5ea54286acf4aecd23256c84211be6",
"https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f",
"https://bcr.bazel.build/modules/rules_python/1.6.3/source.json": "f0be74977e5604a6526c8a416cda22985093ff7d5d380d41722d7e44015cc419",
"https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251",
"https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/source.json": "37c10335f2361c337c5c1f34ed36d2da70534c23088062b33a8bdaab68aa9dea",
"https://bcr.bazel.build/modules/rules_rust/0.45.1/MODULE.bazel": "a69d0db3a958fab2c6520961e1b2287afcc8b36690fd31bbc4f6f7391397150d",
@@ -420,8 +402,10 @@
"https://bcr.bazel.build/modules/rules_shell/0.5.0/MODULE.bazel": "8c8447370594d45539f66858b602b0bb2cb2d3401a4ebb9ad25830c59c0f366d",
"https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b",
"https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c",
"https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400",
"https://bcr.bazel.build/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66",
"https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
"https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046",
"https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
"https://bcr.bazel.build/modules/rules_swift/2.4.0/source.json": "a6577f57f9febbdc015a01f2a8f3487422032f134d6c61d18ed8e8ca3b9acc7c",
"https://bcr.bazel.build/modules/stardoc/0.5.0/MODULE.bazel": "f9f1f46ba8d9c3362648eea571c6f9100680efc44913618811b58cc9c02cd678",
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
@@ -432,8 +416,8 @@
"https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7",
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
"https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468",
"https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351",
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c",
@@ -442,23 +426,18 @@
"https://bcr.bazel.build/modules/toolchains_llvm/1.6.0/source.json": "6bd3ef95a288dd2bb1582eca332af850c9a5428a23bb92cb1c57c2dfe6cb7369",
"https://bcr.bazel.build/modules/toolchains_protoc/0.5.0/MODULE.bazel": "e649dcd74790d8b186517588c827a777dfa67acfc4cbd733721c4be143ea107f",
"https://bcr.bazel.build/modules/toolchains_protoc/0.5.0/source.json": "9152bf33827a44f796f94f486252fc0128d9efc2413246ebb09a234bb628a846",
"https://bcr.bazel.build/modules/upb/0.0.0-20211020-160625a/MODULE.bazel": "6cced416be2dc5b9c05efd5b997049ba795e5e4e6fafbe1624f4587767638928",
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9",
"https://bcr.bazel.build/modules/upb/0.0.0-20230907-e7430e6/MODULE.bazel": "3a7dedadf70346e678dc059dbe44d05cbf3ab17f1ce43a1c7a42edc7cbf93fd9",
"https://bcr.bazel.build/modules/xds/0.0.0-20240423-555b57e/MODULE.bazel": "cea509976a77e34131411684ef05a1d6ad194dd71a8d5816643bc5b0af16dc0f",
"https://bcr.bazel.build/modules/xds/0.0.0-20240423-555b57e/source.json": "7227e1fcad55f3f3cab1a08691ecd753cb29cc6380a47bc650851be9f9ad6d20",
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/source.json": "2d2bad780a9f2b9195a4a370314d2c17ae95eaa745cefc2e12fbc49759b15aa3",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
"https://bcr.bazel.build/modules/zlib/1.2.13/MODULE.bazel": "aa6deb1b83c18ffecd940c4119aff9567cd0a671d7bba756741cb2ef043a29d5",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.1/MODULE.bazel": "6a9fe6e3fc865715a7be9823ce694ceb01e364c35f7a846bf0d2b34762bc066b",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806",
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198",
"https://bcr.bazel.build/modules/zlib/1.3/MODULE.bazel": "6a9c02f19a24dcedb05572b2381446e27c272cd383aed11d41d99da9e3167a72"
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
},
"selectedYankedVersions": {},
"moduleExtensions": {
@@ -1159,168 +1138,12 @@
},
"@@googleapis+//:extensions.bzl%switched_rules": {
"general": {
"bzlTransitiveDigest": "vG6fuTzXD8MMvHWZEQud0MMH7eoC4GXY0va7VrFFh04=",
"usagesDigest": "tB2/BAROtqvrfaBweAJxJpqqnx85mOx/aupy9bEK4Ss=",
"bzlTransitiveDigest": "liqpEiZfQn8ycdEspyJt6J+baY9GQOl+9/prJJz2wTA=",
"usagesDigest": "AtKnJVSfl4DFbEt1Zhzwc971VYZMgmyRYK8WMKrwA7o=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_google_googleapis_imports": {
"repoRuleId": "@@googleapis+//:repository_rules.bzl%switched_rules",
"attributes": {
"rules": {
"proto_library_with_info": [
"",
""
],
"moved_proto_library": [
"",
""
],
"java_proto_library": [
"",
""
],
"java_grpc_library": [
"",
""
],
"java_gapic_library": [
"",
""
],
"java_gapic_test": [
"",
""
],
"java_gapic_assembly_gradle_pkg": [
"",
""
],
"py_proto_library": [
"",
""
],
"py_grpc_library": [
"",
""
],
"py_gapic_library": [
"",
""
],
"py_test": [
"",
""
],
"py_gapic_assembly_pkg": [
"",
""
],
"py_import": [
"",
""
],
"go_proto_library": [
"",
""
],
"go_grpc_library": [
"",
""
],
"go_library": [
"",
""
],
"go_test": [
"",
""
],
"go_gapic_library": [
"",
""
],
"go_gapic_assembly_pkg": [
"",
""
],
"cc_proto_library": [
"",
""
],
"cc_grpc_library": [
"",
""
],
"cc_gapic_library": [
"",
""
],
"php_proto_library": [
"",
"php_proto_library"
],
"php_grpc_library": [
"",
"php_grpc_library"
],
"php_gapic_library": [
"",
"php_gapic_library"
],
"php_gapic_assembly_pkg": [
"",
"php_gapic_assembly_pkg"
],
"nodejs_gapic_library": [
"",
"typescript_gapic_library"
],
"nodejs_gapic_assembly_pkg": [
"",
"typescript_gapic_assembly_pkg"
],
"ruby_proto_library": [
"",
""
],
"ruby_grpc_library": [
"",
""
],
"ruby_ads_gapic_library": [
"",
""
],
"ruby_cloud_gapic_library": [
"",
""
],
"ruby_gapic_assembly_pkg": [
"",
""
],
"csharp_proto_library": [
"",
""
],
"csharp_grpc_library": [
"",
""
],
"csharp_gapic_library": [
"",
""
],
"csharp_gapic_assembly_pkg": [
"",
""
]
}
}
}
},
"generatedRepoSpecs": {},
"recordedRepoMappingEntries": []
}
},
@@ -1420,34 +1243,6 @@
"recordedRepoMappingEntries": []
}
},
"@@rules_apple+//apple:extensions.bzl%non_module_deps": {
"general": {
"bzlTransitiveDigest": "UsflLeiazyu2v5pvibcvOeIdDV95S25rT96h4XU1nhY=",
"usagesDigest": "M3VqFpeTCo4qmrNKGZw0dxBHvTYDrfV3cscGzlSAhQ4=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"xctestrunner": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/google/xctestrunner/archive/b7698df3d435b6491b4b4c0f9fc7a63fbed5e3a6.tar.gz"
],
"strip_prefix": "xctestrunner-b7698df3d435b6491b4b4c0f9fc7a63fbed5e3a6",
"sha256": "ae3a063c985a8633cb7eb566db21656f8db8eb9a0edb8c182312c7f0db53730d"
}
}
},
"recordedRepoMappingEntries": [
[
"rules_apple+",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_dotnet+//dotnet:extensions.bzl%dotnet": {
"general": {
"bzlTransitiveDigest": "fd+R6GHICVvxgxz6YGDAcp7EbUx26nq9MWH1unOHz9I=",
@@ -2398,7 +2193,7 @@
"@@rules_python+//python/uv:uv.bzl%uv": {
"general": {
"bzlTransitiveDigest": "8vT1ddXtljNxYD0tJkksqzeKE6xqx4Ix+tXthAppjTI=",
"usagesDigest": "p80sy6cYQuWxx5jhV3fOTu+N9EyIUFG9+F7UC/nhXic=",
"usagesDigest": "icnInV8HDGrRQf9x8RMfxWfBHgT3OgRlYovS/9POEJw=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -3733,6 +3528,160 @@
]
]
}
},
"@@rules_swift+//swift:extensions.bzl%non_module_deps": {
"general": {
"bzlTransitiveDigest": "gITmwZkwiDAjxHDTZA8AxJiD5JGajyGl/ExE3dyszB0=",
"usagesDigest": "9w18ec4dj90mX/JqpR2tBU2YVc1GU9yNxi/d7q6lLVA=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_github_apple_swift_protobuf": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-protobuf/archive/1.20.2.tar.gz"
],
"sha256": "3fb50bd4d293337f202d917b6ada22f9548a0a0aed9d9a4d791e6fbd8a246ebb",
"strip_prefix": "swift-protobuf-1.20.2/",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_protobuf/BUILD.overlay"
}
},
"com_github_grpc_grpc_swift": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/grpc/grpc-swift/archive/1.16.0.tar.gz"
],
"sha256": "58b60431d0064969f9679411264b82e40a217ae6bd34e17096d92cc4e47556a5",
"strip_prefix": "grpc-swift-1.16.0/",
"build_file": "@@rules_swift+//third_party:com_github_grpc_grpc_swift/BUILD.overlay"
}
},
"com_github_apple_swift_docc_symbolkit": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-docc-symbolkit/archive/refs/tags/swift-5.10-RELEASE.tar.gz"
],
"sha256": "de1d4b6940468ddb53b89df7aa1a81323b9712775b0e33e8254fa0f6f7469a97",
"strip_prefix": "swift-docc-symbolkit-swift-5.10-RELEASE",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_docc_symbolkit/BUILD.overlay"
}
},
"com_github_apple_swift_nio": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio/archive/2.42.0.tar.gz"
],
"sha256": "e3304bc3fb53aea74a3e54bd005ede11f6dc357117d9b1db642d03aea87194a0",
"strip_prefix": "swift-nio-2.42.0/",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_nio/BUILD.overlay"
}
},
"com_github_apple_swift_nio_http2": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio-http2/archive/1.26.0.tar.gz"
],
"sha256": "f0edfc9d6a7be1d587e5b403f2d04264bdfae59aac1d74f7d974a9022c6d2b25",
"strip_prefix": "swift-nio-http2-1.26.0/",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_nio_http2/BUILD.overlay"
}
},
"com_github_apple_swift_nio_transport_services": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio-transport-services/archive/1.15.0.tar.gz"
],
"sha256": "f3498dafa633751a52b9b7f741f7ac30c42bcbeb3b9edca6d447e0da8e693262",
"strip_prefix": "swift-nio-transport-services-1.15.0/",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_nio_transport_services/BUILD.overlay"
}
},
"com_github_apple_swift_nio_extras": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio-extras/archive/1.4.0.tar.gz"
],
"sha256": "4684b52951d9d9937bb3e8ccd6b5daedd777021ef2519ea2f18c4c922843b52b",
"strip_prefix": "swift-nio-extras-1.4.0/",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_nio_extras/BUILD.overlay"
}
},
"com_github_apple_swift_log": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-log/archive/1.4.4.tar.gz"
],
"sha256": "48fe66426c784c0c20031f15dc17faf9f4c9037c192bfac2f643f65cb2321ba0",
"strip_prefix": "swift-log-1.4.4/",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_log/BUILD.overlay"
}
},
"com_github_apple_swift_nio_ssl": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio-ssl/archive/2.23.0.tar.gz"
],
"sha256": "4787c63f61dd04d99e498adc3d1a628193387e41efddf8de19b8db04544d016d",
"strip_prefix": "swift-nio-ssl-2.23.0/",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_nio_ssl/BUILD.overlay"
}
},
"com_github_apple_swift_collections": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-collections/archive/1.0.4.tar.gz"
],
"sha256": "d9e4c8a91c60fb9c92a04caccbb10ded42f4cb47b26a212bc6b39cc390a4b096",
"strip_prefix": "swift-collections-1.0.4/",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_collections/BUILD.overlay"
}
},
"com_github_apple_swift_atomics": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-atomics/archive/1.1.0.tar.gz"
],
"sha256": "1bee7f469f7e8dc49f11cfa4da07182fbc79eab000ec2c17bfdce468c5d276fb",
"strip_prefix": "swift-atomics-1.1.0/",
"build_file": "@@rules_swift+//third_party:com_github_apple_swift_atomics/BUILD.overlay"
}
},
"build_bazel_rules_swift_index_import": {
"repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive",
"attributes": {
"build_file": "@@rules_swift+//third_party:build_bazel_rules_swift_index_import/BUILD.overlay",
"canonical_id": "index-import-5.8",
"urls": [
"https://github.com/MobileNativeFoundation/index-import/releases/download/5.8.0.1/index-import.tar.gz"
],
"sha256": "28c1ffa39d99e74ed70623899b207b41f79214c498c603915aef55972a851a15"
}
},
"build_bazel_rules_swift_local_config": {
"repoRuleId": "@@rules_swift+//swift/internal:swift_autoconfiguration.bzl%swift_autoconfiguration",
"attributes": {}
}
},
"recordedRepoMappingEntries": [
[
"rules_swift+",
"bazel_tools",
"bazel_tools"
]
]
}
}
},
"facts": {
+44 -2
View File
@@ -40,6 +40,19 @@ if [ -f "$INFO_PLIST" ]; then
/usr/libexec/PlistBuddy -c "Set :ITSAppUsesNonExemptEncryption false" "$INFO_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :ITSAppUsesNonExemptEncryption bool false" "$INFO_PLIST"
echo "Set ITSAppUsesNonExemptEncryption=false in Info.plist"
# Remove UIApplicationSceneManifest if present.
# Unity 6 generates this key, which opts the app into the iOS scene-based
# lifecycle. This breaks Application.deepLinkActivated because iOS routes
# deep link URLs through UISceneDelegate instead of UIApplicationDelegate,
# and Unity doesn't implement the scene delegate path.
# See: https://issuetracker.unity3d.com/issues/in-135632
if /usr/libexec/PlistBuddy -c "Print :UIApplicationSceneManifest" "$INFO_PLIST" 2>/dev/null; then
echo "Found UIApplicationSceneManifest in Info.plist — removing to fix deep link handling"
/usr/libexec/PlistBuddy -c "Delete :UIApplicationSceneManifest" "$INFO_PLIST"
else
echo "No UIApplicationSceneManifest in Info.plist (deep links should work)"
fi
fi
# Unity always generates "Unity-iPhone" as the main app scheme
@@ -88,6 +101,16 @@ EOF
echo "Exporting IPA..."
# Restore keychain search list before export.
# xcodebuild archive can reset the user keychain search list during long
# builds, dropping the temporary CI keychain that holds the signing cert.
# Re-add it here so xcodebuild -exportArchive can find the certificate.
if [ -n "${KEYCHAIN_NAME:-}" ]; then
echo "Restoring keychain search list (adding $KEYCHAIN_NAME)"
security list-keychains -d user -s "$KEYCHAIN_NAME" login.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
fi
# Debug: List available keychains and signing identities
echo "=== Debug: Available keychains ==="
security list-keychains -d user
@@ -97,11 +120,24 @@ echo "=== Debug: Export options plist ==="
cat "$EXPORT_OPTIONS_PLIST"
echo "=== End debug ==="
# Export IPA (removed -allowProvisioningUpdates as we're using manual signing)
# Export IPA with App Store Connect API key authentication.
# Without the API key, xcodebuild falls back to the Xcode-Token in the
# keychain, which expires periodically and breaks headless CI builds.
EXPORT_AUTH_ARGS=()
if [ -n "${APP_STORE_CONNECT_API_KEY_PATH:-}" ] && [ -n "${APP_STORE_CONNECT_API_KEY_ID:-}" ] && [ -n "${APP_STORE_CONNECT_API_ISSUER_ID:-}" ]; then
echo "Using App Store Connect API key for export authentication"
EXPORT_AUTH_ARGS=(
-authenticationKeyPath "$APP_STORE_CONNECT_API_KEY_PATH"
-authenticationKeyID "$APP_STORE_CONNECT_API_KEY_ID"
-authenticationKeyIssuerID "$APP_STORE_CONNECT_API_ISSUER_ID"
)
fi
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST"
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
"${EXPORT_AUTH_ARGS[@]}"
# Find and rename the IPA to a consistent name
IPA_FILE=$(find "$EXPORT_PATH" -name "*.ipa" | head -1)
@@ -111,3 +147,9 @@ fi
echo "Export complete: $EXPORT_PATH/eagle0.ipa"
ls -la "$EXPORT_PATH"
# Clean up DerivedData for Unity-iPhone builds.
# Each build creates a new ~3.6GB folder (Unity-iPhone-<random>) that
# accumulates and wastes disk space on the build runner.
echo "Cleaning up Unity-iPhone DerivedData..."
find ~/Library/Developer/Xcode/DerivedData -maxdepth 1 -name 'Unity-iPhone-*' -type d -exec rm -rf {} + 2>/dev/null || true
@@ -1,49 +0,0 @@
#!/usr/bin/env bash
# Build iOS Addressables only (no player build)
# 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
BUILD_BASE="${EAGLE0_BUILD_DIR:-/tmp/eagle0}"
WORKSPACE=$(pwd)
echo "Building protos"
./scripts/build_protos.sh
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
LOG_PATH=${1:-"${BUILD_BASE}/editor_ios_addressables.log"}
echo "Building iOS Addressables"
mkdir -p "$(dirname "$LOG_PATH")"
# Build Addressables for iOS target
# Uses BuildiOSAddressables which explicitly switches build target
# Capture exit code to show log on failure
set +e
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
-nographics \
-batchmode \
-quit \
-executeMethod BuildScript.BuildiOSAddressables \
-logFile "$LOG_PATH" \
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
UNITY_EXIT_CODE=$?
set -e
if [ $UNITY_EXIT_CODE -ne 0 ]; then
echo ""
echo "Unity build failed with exit code $UNITY_EXIT_CODE"
echo "=== Unity Editor Log (last 200 lines) ==="
tail -200 "$LOG_PATH" || echo "Could not read log file at $LOG_PATH"
echo "=== End of Unity Editor Log ==="
exit $UNITY_EXIT_CODE
fi
echo "iOS Addressables build complete"
echo "Bundles should be in: $WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/iOS/"
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# Fetch LFS files from Gitea mirror with retry.
#
# Gitea's mirror-sync API is async — the webhook fires on push but the
# actual sync may still be in progress when CI starts. Retry with backoff
# to bridge the gap.
#
# Usage:
# ./ci/github_actions/fetch_lfs.sh # full pull
# ./ci/github_actions/fetch_lfs.sh --include="path/to/*" # selective pull
set -euo pipefail
MAX_ATTEMPTS=5
RETRY_DELAY=15
git lfs install
echo "LFS objects before pull:"
git lfs ls-files | wc -l
for i in $(seq 1 $MAX_ATTEMPTS); do
if git lfs pull "$@"; then
echo "LFS objects after pull:"
git lfs ls-files | wc -l
exit 0
fi
if [ "$i" -lt "$MAX_ATTEMPTS" ]; then
echo "LFS pull attempt $i/$MAX_ATTEMPTS failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
fi
done
echo "LFS pull failed after $MAX_ATTEMPTS attempts"
exit 1
+281
View File
@@ -0,0 +1,281 @@
# Allied Victory: Battle Resolution Flow
How allied victories work end-to-end, from Shardok tactical resolution through Eagle strategic
processing to player-facing aftermath decisions.
## 1. Battle Setup (Eagle -> Shardok)
When Eagle sends a battle to Shardok via `RequestBattlesAction`, each player gets specific victory
conditions:
| Player | Victory Conditions |
|-------------|-----------------------------------------------------------------------|
| Attacker(s) | `HoldsCriticalTiles`, `LastPlayerStanding`, `LastAllianceStanding` |
| Defender | `LastPlayerStanding`, `WinAfterMaxRounds` |
This is set in `RequestBattlesAction.shardokPlayers`.
Alliance information is sent separately in `PlayerSetupInfo.allies`. Eagle reads each player's
`factionRelationships` and includes all non-HOSTILE factions as allies
(`ShardokInterfaceGrpcClient.scala:75-80`).
## 2. Victory Condition Evaluation (Shardok)
Victory conditions are checked in `UpdateGameStatusAction.cpp` in priority order:
### Check 1: LastPlayerStanding (any time)
If exactly **one** player has surviving units and has `LastPlayerStanding`:
- That player is the sole winner.
- This applies to solo victories only (1 survivor).
### Check 2: LastAllianceStanding (any time)
If **multiple** players survive, check if they form a winning alliance:
- ALL survivors must have `LastAllianceStanding` condition
- ALL survivors must be **mutually** allied (every pair checks both directions)
- If yes: all survivors are winners
This triggers for AssaultProvince battles when allied attackers eliminate all defenders. Since the
defender does **not** have `LastAllianceStanding`, the check correctly requires that only the
allied attackers remain alive.
### Check 3: WinAfterMaxRounds (end-of-round only)
If the round counter exceeds `max_rounds`:
- Find the player with `WinAfterMaxRounds` (always the defender)
- That player wins, even if they have no surviving units
- Eagle validates exactly 1 player has this condition (`internalRequire` in `RequestBattlesAction`)
### Check 4: HoldsCriticalTiles (end-of-round only)
**Single-player castle control**: If one player with `HoldsCriticalTiles` occupies ALL critical
tiles with hero-bearing units, that player wins. Additionally, any other player who:
- Has `HoldsCriticalTiles`
- Is **mutually** allied with the castle holder
is also added to `winning_shardok_ids` as a co-winner.
**Allied castle control**: If multiple players collectively occupy all critical tiles with
hero-bearing units, AND:
- All occupants have `HoldsCriticalTiles`
- All occupants are mutually allied
Then all occupants are winners together.
## 3. EndGameCondition Assignment Per Player (Shardok -> Eagle)
Shardok assigns each player a `Victory` or `Loss`. All winners receive `Victory`; there is no
`AllyVictory` distinction. The `EndGameCondition` proto reserves fields 2 (`ally_victory`) and
3 (`draw`) for backwards compatibility but Eagle rejects both.
`GameOverResponsePopulator` validates that any player allied to a winner is also in
`winning_shardok_ids`. If an ally is missing from the winners list, it throws an internal error.
**Known issue**: This validation is too strict for `LastPlayerStanding`. When one allied attacker
is the sole survivor, their dead co-attacker is allied to the winner but legitimately not in
`winning_shardok_ids` (they're dead). The throw should be changed to `Loss` for dead allies, or
the check should only apply when the ally has surviving units.
| Condition | Criterion |
|-----------|-----------|
| `Victory(type)` | Player is in `winning_shardok_ids` |
| `Loss(type)` | Player is not in `winning_shardok_ids` |
The `type` is the VictoryCondition that caused the game to end (e.g., `HoldsCriticalTiles`).
### When do multiple players get Victory?
In standard AssaultProvince battles:
1. **Allied attackers jointly hold all castles** -> both get `Victory(HoldsCriticalTiles)`.
2. **One attacker holds all castles alone, ally has `HoldsCriticalTiles` and is mutually allied**
-> both get `Victory(HoldsCriticalTiles)`. The ally is added to `winning_shardok_ids` by
`UpdateGameStatusAction`.
3. **Allied attackers eliminate all defenders** -> both get `Victory(LastAllianceStanding)`.
This triggers because both have `LastAllianceStanding` and are mutually allied.
4. **One attacker holds all castles, co-attacker is NOT allied** -> only the castle holder
gets `Victory(HoldsCriticalTiles)`; the non-allied co-attacker gets `Loss`.
## 4. Eagle Processes Battle Results
`ResolveBattleAction.scala` receives `BattleResolution` containing each player's
`EndGameCondition` and resolved units.
### Winner/Loser Partition
Players are split by `isVictory` (line 383-386):
- `winningResolvedPlayers`: Victory
- `losingResolvedPlayers`: Loss
### The `unitReturned` Function (line 765-788)
Determines which units go home vs stay at the battle province:
| Unit Status | Returned? | Notes |
|---------------|-----------|-------|
| `Fled` | Always | Sent to their army's flee province |
| `NeverEntered`| Conditional | Only if: attacker AND has flee province AND did NOT win (`!isVictory`) |
| `Captured` | Never | |
| `Normal` | Never | |
| `Retreated` | Never | |
| `Outlawed` | Never | Becomes unaffiliated hero |
Key point: `NeverEntered` units from **winning** factions stay at the battle province rather than
being sent home. This is important for bounced co-attackers who are allied with the actual winner.
### Withdrawn/Fled Unit Routing (line 690-706)
For units that ARE returned, `armyFromResolvedArmy` builds a returning army with
`fleeProvinceId = None`. If the army has no `fleeProvinceId`, it becomes "shattered" (units
become outlaws in the battle province). If the original army had a `fleeProvinceId` set, returned
units are sent there as incoming armies.
### Battle Resolution Branching (line 397-552)
Three branches based on who won:
#### Branch A: Defender Won
Triggered when: `winningShardokPlayers.exists(_.isDefender)`
- Executes `ProvinceHeldAction`
- All non-defender, non-returned, non-outlawed units are **captured**
- Province stays with the defending faction
#### Branch B: Single Attacker Won
Triggered when: exactly 1 attacking winner
- Executes `ProvinceConqueredAction` immediately
- Winning attacker's non-fled units occupy the province
- Losing defenders' non-fled units are captured
- Province transfers to the attacker
#### Branch C: Multiple Allied Attackers Won
Triggered when: 2+ attacking winners
- Executes `MultiVictorBattleSetupAction`
- Creates `PendingConquestInfo` with aftermath claimants
- Province enters the **Battle Aftermath** phase
## 5. Battle Aftermath (Multi-Victor Only)
When multiple allied attackers win, the province enters an aftermath decision phase where players
decide who keeps it.
### Claimant Setup
Each attacking winner becomes an `AftermathClaimant` with:
- `units`: their non-fled, non-outlawed units still at the battle province
- `armySize`: total troop count of those units
- `broughtGold` / `broughtFood`: supplies their armies carried
Claimants are **sorted by army size descending** (largest army decides first).
### Decision Phase
Claimants are presented with a choice **one at a time**, in order:
**If no one has chosen "Keep" yet:**
- **Keep Province**: claim the province for your faction
- **Withdraw To**: pick an adjacent province to march your army to, optionally carrying up to
the gold/food you brought
**If someone already chose "Keep":**
- **Withdraw To** is the only option (can't have two keepers)
**Last undecided claimant:**
- If no one has chosen "Keep" yet, the last claimant **automatically keeps** (no command shown,
resolved by `AutoResolveBattleAftermathAction`)
- This guarantees someone always claims the province
### Finalization
Once all claimants have decided (`FinalizeAftermathAction`):
1. The keeper's units run through `ProvinceConqueredAction` -- province transfers to their faction
2. Withdrawing claimants' units become incoming armies to their chosen adjacent province,
arriving next round, carrying the specified supplies
3. `PendingConquestInfo` is cleared from the province
## 6. Summary: Who Gets What
### Two allied attackers jointly hold all castles
- Shardok: Both get `Victory(HoldsCriticalTiles)` (both in `winning_shardok_ids`)
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction
- Players: Largest army picks first: Keep or Withdraw. Last player auto-keeps if no one chose Keep.
### One attacker holds all castles, allied attacker doesn't
- Shardok: Castle-holder gets `Victory(HoldsCriticalTiles)`. Mutually-allied co-attacker with
`HoldsCriticalTiles` is added to `winning_shardok_ids` -> also gets `Victory(HoldsCriticalTiles)`.
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction -> Battle Aftermath.
- Players: Largest army picks first: Keep or Withdraw.
### Allied attackers eliminate all defenders
- Shardok: Both attackers have `LastAllianceStanding` and are mutually allied -> both get
`Victory(LastAllianceStanding)`.
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction -> Battle Aftermath.
- Players: Largest army picks first: Keep or Withdraw.
### Non-allied co-attackers: one holds all castles
- Shardok: Castle-holder gets `Victory(HoldsCriticalTiles)`, non-allied co-attacker gets
`Loss(HoldsCriticalTiles)`.
- Eagle: Single attacker won -> ProvinceConqueredAction. Non-allied co-attacker's units are
captured along with the defender's.
### One allied attacker is sole survivor (LastPlayerStanding)
- Shardok: Sole survivor gets `Victory(LastPlayerStanding)`. Dead ally should get `Loss`.
- **BUG**: `GameOverResponsePopulator` currently throws because dead ally is allied to winner
but not in `winning_shardok_ids`. Needs fix — see known issue in Section 3.
- Eagle (once fixed): Single attacker won -> ProvinceConqueredAction. Dead ally's units were
already destroyed in battle.
### Single attacker wins (any condition, no co-attackers)
- No alliance considerations. ProvinceConqueredAction immediately.
### Defender wins (WinAfterMaxRounds or LastPlayerStanding)
- ProvinceHeldAction. All non-returned attacker units are captured.
## 7. Edge Cases and Known Issues
### BUG: Dead ally causes crash via LastPlayerStanding
When one allied attacker is the sole survivor (both the co-attacker and defender are dead),
`LastPlayerStanding` fires and only the survivor is in `winning_shardok_ids`. The
`GameOverResponsePopulator` then sees the dead co-attacker is allied to the winner but not in
`winning_shardok_ids`, and throws `ShardokInternalErrorException`. Fix: dead allies should
receive `Loss`, not trigger a validation error.
### Non-allied co-attackers eliminate all defenders
Two non-allied attackers who both survive after eliminating the defender cannot win via
`LastAllianceStanding` (they aren't mutually allied). The game continues until max rounds,
at which point the defender wins via `WinAfterMaxRounds`. They must capture all critical tiles.
### Non-allied co-attackers: one holds all castles
Only the castle holder wins. The non-allied co-attacker gets `Loss` and their units are captured
along with the defender's. The mutual-alliance check in `UpdateGameStatusAction` prevents
non-allied players from being added to `winning_shardok_ids`.
## Key File References
| Component | File |
|-----------|------|
| Victory condition checking | `src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.cpp` |
| Per-player EndGameCondition | `src/main/cpp/net/eagle0/shardok/server/GameOverResponsePopulator.cpp` |
| Battle setup (victory conditions) | `src/main/scala/.../library/actions/impl/action/RequestBattlesAction.scala` |
| Alliance communication | `src/main/scala/.../shardok_interface/ShardokInterfaceGrpcClient.scala:75-80` |
| Battle result processing | `src/main/scala/.../library/actions/impl/action/ResolveBattleAction.scala` |
| Unit return logic | `ResolveBattleAction.scala:765-788` (`unitReturned`) |
| Multi-victor setup | `src/main/scala/.../library/actions/impl/action/MultiVictorBattleSetupAction.scala` |
| Aftermath availability | `src/main/scala/.../library/actions/availability/AvailableBattleAftermathDecisionCommandFactory.scala` |
| Aftermath command | `src/main/scala/.../library/actions/impl/command/BattleAftermathDecisionCommand.scala` |
| Auto-resolve last claimant | `src/main/scala/.../library/actions/impl/action/AutoResolveBattleAftermathAction.scala` |
| Finalize aftermath | `src/main/scala/.../library/actions/impl/action/FinalizeAftermathAction.scala` |
| EndGameCondition enum | `src/main/scala/.../model/state/shardok_battle/EndGameCondition.scala` |
+25 -1
View File
@@ -12,7 +12,7 @@ This document catalogs all media assets in the Unity project for licensing revie
|----------|-------|-------|
| Images | 10,637 | Mostly PNG icons and UI sprites |
| Audio | 1,778 | 26 music tracks + 1,752 sound effects |
| 3D Models | 52 | Bridge pack only |
| 3D Models | 66+ | Bridge pack, Animal pack deluxe, Honey Badger |
| Fonts | 16 | TTF files |
---
@@ -55,6 +55,30 @@ These are commercial Unity Asset Store purchases tied to your account:
- **Contents:** Bridge construction pieces
- **License:** Unity Asset Store
### Animal pack deluxe
- **Location:** `Assets/Animal pack deluxe/`
- **Publisher:** janpec
- **Asset Store Link:** https://assetstore.unity.com/packages/3d/characters/animals/animal-pack-deluxe-99702
- **Contents:** 26 rigged and animated 3D animal models (bear, boar, crocodile, wolf, frog, crab, rabbit, scorpion, snake, stag, deer, rat, goat, pig, etc.) with idle, walk, run, eat, attack, and die animations
- **Used for:** Beast province effects on the strategic map (AnimalEffect)
- **License:** Unity Asset Store
### 2D Monster Pack: Basic Bundle (+PSB)
- **Location:** `Assets/DungeonMonsters2D/`
- **Publisher:** SP1
- **Asset Store Link:** https://assetstore.unity.com/packages/2d/characters/2d-monster-pack-basic-bundle-psb-328637
- **Contents:** 2D animated monster sprites with PSB (Photoshop) source files. Characters: SkeletonWarrior, SkeletonArcher, SkeletonMage, Zombie, ZombieWarrior, Demon, Succubus, Imp, Spider, Rat, Vampire, DragonRed, Butcher
- **Used for:** Beast province effects (MonsterEffect, DragonEffect) and human-type beast effects on the strategic map
- **License:** Unity Asset Store
### Honey Badger 3D Model
- **Location:** `Assets/HoneyBadger/`
- **Creator:** WildMesh 3D (@WildMesh_3D)
- **Source:** https://sketchfab.com/3d-models/realistic-honey-badger-3d-model-09751ed5383c4afb924cfb6485d313f1
- **Contents:** Rigged 3D honey badger model (FBX) with 63 animations and PBR texture
- **Used for:** Honey badger beast province effect (AnimalEffect)
- **License:** Purchased via Sketchfab/Patreon
### Fantasy Interface Sounds
- **Location:** `Assets/Fantasy Interface Sounds/`
- **Count:** 320 WAV files
+246
View File
@@ -0,0 +1,246 @@
# Adding Beast-Type-Specific Effects
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`.
## Architecture
```
ProvinceBeastsController
├── GetBeastName(province) → reads BeastsEvent.BeastInfo.SingularName
├── GetEffectPrefab(beastName) → switch on name → returns prefab
└── SpawnBeastsEffect(provinceId, beastName) → instantiates at centroid
```
**Key files:**
| File | Purpose |
|------|---------|
| `Assets/Eagle/ProvinceBeastsController.cs` | Routes beast names to prefabs, positions effects |
| `Assets/Eagle/BeastsEffect.cs` | Generic effect (circling birds via particle system) |
| `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:
public GameObject wolfEffectPrefab;
// Add case in GetEffectPrefab(), keyed by singularName from beasts.tsv:
private GameObject GetEffectPrefab(string beastName) {
switch (beastName) {
case "dragon": return dragonEffectPrefab;
case "wolf": return wolfEffectPrefab;
default: return beastsEffectPrefab;
}
}
```
### 4. Wire up in the scene
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 |
|---|---|---|---|
| dragon | DragonEffect | 3dFoin Dragon | dragon_skin.FBX (w/ DragonMapAnims controller) |
| skeleton | MonsterEffect | DungeonMonsters2D | SkeletonWarrior, SkeletonArcher, SkeletonMage |
| zombie | MonsterEffect | DungeonMonsters2D | Zombie, ZombieWarrior |
| rat | MonsterEffect + AnimalEffect | DungeonMonsters2D + Animal Pack Deluxe | Rat (2D), Rat (3D) |
| spider | MonsterEffect | DungeonMonsters2D | Spider |
| demon | MonsterEffect | DungeonMonsters2D | Demon, Succubus |
| orc | MonsterEffect | DungeonMonsters2D | Imp |
| vampire | MonsterEffect | DungeonMonsters2D | Vampire |
| knight types (6 names) | AnimalEffect | Polytope Studio | PT_Male_Knight_01/02, PT_Female_Knight_01/02 (w/ HumanMapAnims controller) |
| soldier types (8 names) | AnimalEffect | Polytope Studio | PT_Male_Soldier_01/02, PT_Female_Soldier_01/02 (w/ HumanMapAnims controller) |
| archer types (3 names) | AnimalEffect | Polytope Studio | PT_Male_Archer_01/02, PT_Female_Archer_01/02 (w/ HumanMapAnims controller) |
| militia types (8 names) | AnimalEffect | Polytope Studio | PT_Male_Militia_01/02, PT_Female_Militia_01/02 (w/ HumanMapAnims controller) |
| peasant types (9 names) | AnimalEffect | Polytope Studio | PT_Male_Peasant_01, PT_Female_Peasant_01_a (w/ HumanMapAnims controller) |
| clown | AnimalEffect | Clown Pack | Clown, Fat Clown (w/ per-prefab controller overrides) |
| giant | AnimalEffect | Polytope Studio | PT_Male/Female_Peasant at 3x scale (w/ HumanMapAnims controller) |
| ogre, troll | AnimalEffect | Orc-Ogre Pack | OrcOgre_Animated (w/ OgreMapAnims controller) |
| bear | AnimalEffect | Animal Pack Deluxe | Brown_bear |
| boar | AnimalEffect | Animal Pack Deluxe | Wild_boar |
| crocodile | AnimalEffect | Animal Pack Deluxe | Crocodile |
| snake | AnimalEffect | Animal Pack Deluxe | Viper |
| crab | AnimalEffect | Animal Pack Deluxe | Crab |
| frog | AnimalEffect | Animal Pack Deluxe | Common_frog, Common_frog_v2, Common_frog_v3 |
| rabbit | AnimalEffect | Animal Pack Deluxe | Wild_rabbit, Wild_rabbit_v2 |
| salamander | AnimalEffect | Animal Pack Deluxe | Fire_salamander |
| scorpion | AnimalEffect | Animal Pack Deluxe | Scorpion, Yellow_fattail_scorpion |
| stag | AnimalEffect | Animal Pack Deluxe | Deer, Deer_male |
| wild goat | AnimalEffect | Animal Pack Deluxe | Goat, Ibex |
| wild pig | AnimalEffect | Animal Pack Deluxe | Iron_age_pig, Iron_age_pig_v2 |
| wolf, wolverine | AnimalEffect | Animal Pack Deluxe | Wolf |
| honey badger | AnimalEffect | HoneyBadger (Sketchfab) | HoneyBadger (w/ HoneyBadgerMapAnims controller) |
| raccoon | AnimalEffect | Raccoon (Sketchfab) | Raccoon (w/ RaccoonMapAnims controller) |
| tiger | AnimalEffect | ithappy Animals FREE | Tiger_001 (w/ TigerMapAnims controller) |
| elephant | AnimalEffect | Animal Pack Deluxe v2 | African_elephant, Indian_elephant (w/ ElephantMapAnims controller) |
| mammoth | AnimalEffect | Animal Pack Deluxe v2 | African_elephant, Indian_elephant (w/ ElephantMapAnims controller) |
| hippopotamus | AnimalEffect | Africa Animals Pack Low Poly V2 | Hippopotamus (w/ HippopotamusMapAnims controller) |
| lion | AnimalEffect | Africa Animals Pack Low Poly V1 | Lion, Lioness (w/ LionMapAnims controller) |
| velociraptor | AnimalEffect | Dino Pack Low Poly V1 | VelociraptorColor1, VelociraptorColor2 (w/ VelociraptorMapAnims controller) |
| black panther | AnimalEffect | Africa Animals Pack Low Poly V1 | BlackPanther (w/ BlackPantherMapAnims controller) |
| rhinoceros | AnimalEffect | Africa Animals Pack Low Poly V1 | Rhinoceros (w/ RhinocerosMapAnims controller) |
| gorilla | AnimalEffect | Africa Animals Pack Low Poly V2 | Gorilla (w/ GorillaMapAnims controller) |
| hyena | AnimalEffect | Africa Animals Pack Low Poly V2 | Hyena (w/ HyenaMapAnims controller) |
| leopard | AnimalEffect | Africa Animals Pack Low Poly V2 | Leopard (w/ LeopardMapAnims controller) |
| warthog | AnimalEffect | Africa Animals Pack Low Poly V2 | Phacochoerus (w/ WarthogMapAnims controller) |
| emu | AnimalEffect | Australia Animals Pack V1 | Emu (w/ EmuMapAnims controller) |
| kangaroo | AnimalEffect | Australia Animals Pack V1 | Kangaroo (w/ KangarooMapAnims controller) |
| tasmanian devil | AnimalEffect | Australia Animals Pack V1 | TasmanianDevil (w/ TasmanianDevilMapAnims controller) |
| 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) |
|---|---|---|
| **Knight** (heavy armor) | Knight M/F x2 | cultist, heretic, cannibal, terrorist, cossack, desperado (6) |
| **Soldier** (medium armor) | Soldier M/F x2 | bandit, brigand, robber, highwayman, marauder, dacoit, freebooter, mobster (8) |
| **Archer** (light armor, ranged) | Archer M/F x2 | thief, pirate, buccaneer (3) |
| **Militia** (light armed) | Militia M/F x2 | hooligan, street tough, ruffian, scalawag, ne'er-do-well, crook, miscreant, asshole (8) |
| **Peasant** (unarmed) | Peasant M/F | agitator, instigator, rabble-rouser, separatist, particularist, nihilist, proud boy, rebel, traitor (9) |
Asset packs: Polytope Studio - Lowpoly Medieval Characters (Soldiers, Knights, Militia, Archers, Peasants)
| **Butcher** (armed loner) | DungeonMonsters2D Butcher | psychopath (1) |
### 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)
- Africa Animals Pack Low Poly V2 (Hippopotamus, Antelope, Gorilla, Hyena, Leopard, Phacochoerus/Warthog)
- Dino Pack Low Poly V1 (Velociraptor, Brontosaurus, Pteranodon, Stegosaurus, T-Rex, Triceratops)
- Australia Animals Pack V1 (Chlamydosaurus/Frilled Lizard, Echidna, Emu, Kangaroo, Koala, Platypus, Tasmanian Devil)
### Per-animal setup steps
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 |
| Echidna | echidna beast | Small, spiny |
| Chlamydosaurus (Frilled Lizard) | lizard beast | Has threat display animation |
| Platypus (Ornitorinco) | platypus beast | Semi-aquatic; unique |
(Emu, Kangaroo, and Tasmanian Devil already have effects.)
None of the remaining animals above are currently in `beasts.tsv`.
## Tips
- Keep effects lightweight; multiple provinces may have beasts simultaneously
- Use `ParticleSystemScalingMode.Hierarchy` so effects scale with the map zoom
- Set appropriate `sortingOrder` on renderers (existing effects use 102-104)
- For animated prefabs, set `AnimatorCullingMode.AlwaysAnimate` since UI elements may be considered offscreen by the culling system
- For 3D models (AnimalEffect), set `renderQueue=4000` and `ZTest=Always` on materials to prevent clipping behind the map
+19
View File
@@ -0,0 +1,19 @@
# Two-Stage Animation Implementation Pattern
Actions with server-determined success/failure outcomes use a two-stage system:
1. **Attempt phase**: Animation + sound plays immediately when the command is issued
2. **Result phase**: A distinct success or failure animation + sound plays when the server responds
## Adding a New Two-Stage Action
To add distinct success/failure animations for an action (using `ExtinguishFire` as an example):
1. **AnimationType enum** (`ShardokGameController.cs`): Add `ExtinguishFireFailed`
2. **AnimationTypeForAction()**: Map `ActionType.ExtinguishFireFailed` to new type
3. **AttemptAnimationType()**: Map `ExtinguishFireFailed` back to `ExtinguishFire`
4. **ActionTypeForSound()**: Map new type to `ActionType.ExtinguishFireFailed`
5. **PlayAnimation()**: Add dispatch case calling the new animator method
6. **Animator**: Add `AnimateExtinguishFailed()` method
7. **SoundManager**: Ensure result sound is mapped (usually already done)
See FearAnimator and FleeAnimator for reference implementations.
+1 -1
View File
@@ -12,7 +12,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
golang.org/x/sys v0.28.0
golang.org/x/sys v0.30.0
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.36.3
)
+2 -2
View File
@@ -43,8 +43,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+4 -8
View File
@@ -2,9 +2,6 @@
#
# Build the SparklePlugin native library for Unity using Bazel
#
# The SparklePlugin is built in a separate Bazel workspace (sparkle_workspace/)
# to avoid rules_swift version conflicts between grpc and rules_apple.
#
# Usage: build_sparkle_plugin.sh [output_dir]
set -euo pipefail
@@ -14,14 +11,13 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUTPUT_DIR="${1:-$PROJECT_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/macOS}"
echo "=== Building SparklePlugin with Bazel (from sparkle_workspace) ==="
echo "=== Building SparklePlugin with Bazel ==="
# Build in the separate sparkle_workspace to avoid dependency conflicts
cd "$PROJECT_ROOT/sparkle_workspace"
bazel build //:SparklePlugin
cd "$PROJECT_ROOT"
bazel build //src/main/objc/net/eagle0/clients/unity/sparkle:SparklePlugin
# Get the zip path from bazel
ZIP_PATH=$(bazel cquery --output=files //:SparklePlugin 2>/dev/null)
ZIP_PATH=$(bazel cquery --output=files //src/main/objc/net/eagle0/clients/unity/sparkle:SparklePlugin 2>/dev/null)
echo "=== Extracting SparklePlugin.bundle ==="
mkdir -p "$OUTPUT_DIR"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Keeps Bazel mactools builds in sync with the installed Xcode version.
#
# Only needed on runners that do mactools builds (Mac/Sparkle). Non-Mac builds
# don't run this script; their apple_cc_autoconf stays cached because none of
# its environ vars change, so they're unaffected by Xcode version changes.
#
# 1. Writes .bazelrc.xcode with mactools-scoped flags:
# - action_env for remote cache key invalidation
# - DEVELOPER_DIR pointing to the active Xcode
#
# 2. Runs `bazel clean --expunge` when the version actually changes, because
# local_config_apple_cc (a cached repository rule) bakes in the old version
# and can only be refreshed by clearing the output base.
#
# .bazelrc imports the generated file via: try-import %workspace%/.bazelrc.xcode
set -euo pipefail
XCODE_BUILD_VERSION=$(xcodebuild -version 2>/dev/null | awk '/Build version/ {print $3}')
if [ -z "$XCODE_BUILD_VERSION" ]; then
echo "Warning: Could not detect Xcode build version, skipping"
exit 0
fi
DEVELOPER_DIR=$(xcode-select -p 2>/dev/null)
BAZELRC_XCODE=".bazelrc.xcode"
EXPECTED_LINE="common:mactools --action_env=XCODE_BUILD_VERSION=${XCODE_BUILD_VERSION}"
# Check if the file already has the right version (first line is the version marker)
if [ -f "$BAZELRC_XCODE" ]; then
CURRENT_FIRST_LINE=$(head -1 "$BAZELRC_XCODE")
if [ "$CURRENT_FIRST_LINE" = "$EXPECTED_LINE" ]; then
exit 0
fi
fi
# Xcode version changed (or first run) — expunge local cache to clear
# stale local_config_apple_cc, then write the new config
OLD_VERSION="unknown"
if [ -f "$BAZELRC_XCODE" ]; then
OLD_VERSION=$(sed -n 's/.*XCODE_BUILD_VERSION=//p' "$BAZELRC_XCODE" | head -1)
fi
echo "Xcode build version changed: ${OLD_VERSION} -> ${XCODE_BUILD_VERSION}"
echo "Running bazel clean --expunge to clear stale toolchain config..."
bazel clean --expunge 2>/dev/null || true
cat > "$BAZELRC_XCODE" << EOF
${EXPECTED_LINE}
common:mactools --repo_env=DEVELOPER_DIR=${DEVELOPER_DIR}
EOF
echo "Updated ${BAZELRC_XCODE} — next mactools build will rebuild with Xcode ${XCODE_BUILD_VERSION}"
-1
View File
@@ -1 +0,0 @@
7.7.1
-2
View File
@@ -1,2 +0,0 @@
# Bazel symlinks
bazel-*
-28
View File
@@ -1,28 +0,0 @@
module(name = "sparkle_workspace")
# Minimal dependencies for building SparklePlugin
# This workspace is isolated from the main workspace to avoid
# rules_swift version conflicts between grpc and rules_apple
bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_apple_support")
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
# Register Apple CC toolchain for Objective-C compilation
apple_cc_configure = use_extension(
"@build_bazel_apple_support//crosstool:setup.bzl",
"apple_cc_configure_extension",
)
use_repo(apple_cc_configure, "local_config_apple_cc")
# Sparkle framework for macOS auto-updates
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
SPARKLE_VERSION = "2.6.4"
http_archive(
name = "sparkle",
build_file = "//:external/BUILD.sparkle",
sha256 = "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b",
strip_prefix = "",
url = "https://github.com/sparkle-project/Sparkle/releases/download/%s/Sparkle-%s.tar.xz" % (SPARKLE_VERSION, SPARKLE_VERSION),
)
-451
View File
@@ -1,451 +0,0 @@
{
"lockFileVersion": 13,
"registryFileHashes": {
"https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
"https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
"https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
"https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da",
"https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896",
"https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85",
"https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1",
"https://bcr.bazel.build/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442",
"https://bcr.bazel.build/modules/apple_support/1.23.1/source.json": "d888b44312eb0ad2c21a91d026753f330caa48a25c9b2102fae75eb2b0dcfdd2",
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
"https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
"https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
"https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc",
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
"https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
"https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
"https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
"https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
"https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
"https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953",
"https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
"https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8",
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4",
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d",
"https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74",
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9",
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
"https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29",
"https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
"https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
"https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
"https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814",
"https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d",
"https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc",
"https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/source.json": "c16a6488fb279ef578da7098e605082d72ed85fc8d843eaae81e7d27d0f4625d",
"https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
"https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022",
"https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
"https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4",
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/MODULE.bazel": "c5c2c4adeeac5f3f2f9b7f16abfa8be7ffefa596171d0d92bed4cae9ade0a498",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/source.json": "3cb1d69c8243ffcc42ecbf84ae8b9cccd7b1e2f091b0aee5a3e9c9a45267f312",
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
"https://bcr.bazel.build/modules/rules_cc/0.0.11/MODULE.bazel": "9f249c5624a4788067b96b8b896be10c7e8b4375dc46f6d8e1e51100113e0992",
"https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
"https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
"https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
"https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
"https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
"https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
"https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0",
"https://bcr.bazel.build/modules/rules_cc/0.1.2/source.json": "53fcb09b5816c83ca60d9d7493faf3bfaf410dfc2f15deb52d6ddd146b8d43f0",
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
"https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
"https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
"https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
"https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
"https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1",
"https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
"https://bcr.bazel.build/modules/rules_java/8.5.1/source.json": "db1a77d81b059e0f84985db67a22f3f579a529a86b7997605be3d214a0abe38e",
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
"https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
"https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
"https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
"https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
"https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
"https://bcr.bazel.build/modules/rules_proto/6.0.2/source.json": "17a2e195f56cb28d6bbf763e49973d13890487c6945311ed141e196fb660426d",
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
"https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7",
"https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
"https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
"https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
"https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
"https://bcr.bazel.build/modules/rules_python/1.3.0/source.json": "25932f917cd279c7baefa6cb1d3fa8750a7a29de522024449b19af6eab51f4a0",
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
"https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
"https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3",
"https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
"https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
"https://bcr.bazel.build/modules/rules_swift/2.4.0/source.json": "a6577f57f9febbdc015a01f2a8f3487422032f134d6c61d18ed8e8ca3b9acc7c",
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
"https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d",
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
},
"selectedYankedVersions": {},
"moduleExtensions": {
"@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": {
"general": {
"bzlTransitiveDigest": "C4xqrMy1wN4iuTN6Z2eCm94S5XingHhD6uwrIXvCxVI=",
"usagesDigest": "pwHZ+26iLgQdwvdZeA5wnAjKnNI3y6XO2VbhOTeo5h8=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"compatibility_proxy": {
"bzlFile": "@@rules_java~//java:rules_java_deps.bzl",
"ruleClassName": "_compatibility_proxy_repo_rule",
"attributes": {}
}
},
"recordedRepoMappingEntries": [
[
"rules_java~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_kotlin~//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
"general": {
"bzlTransitiveDigest": "eecmTsmdIQveoA97hPtH3/Ej/kugbdCI24bhXIXaly8=",
"usagesDigest": "aJF6fLy82rR95Ff5CZPAqxNoFgOMLMN5ImfBS0nhnkg=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_github_jetbrains_kotlin_git": {
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
"ruleClassName": "kotlin_compiler_git_repository",
"attributes": {
"urls": [
"https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip"
],
"sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88"
}
},
"com_github_jetbrains_kotlin": {
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
"ruleClassName": "kotlin_capabilities_repository",
"attributes": {
"git_repository_name": "com_github_jetbrains_kotlin_git",
"compiler_version": "1.9.23"
}
},
"com_github_google_ksp": {
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:ksp.bzl",
"ruleClassName": "ksp_compiler_plugin_repository",
"attributes": {
"urls": [
"https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip"
],
"sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d",
"strip_version": "1.9.23-1.0.20"
}
},
"com_github_pinterest_ktlint": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_file",
"attributes": {
"sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985",
"urls": [
"https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint"
],
"executable": true
}
},
"rules_android": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
"strip_prefix": "rules_android-0.1.1",
"urls": [
"https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"
]
}
}
},
"recordedRepoMappingEntries": [
[
"rules_kotlin~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_python~//python/uv:uv.bzl%uv": {
"general": {
"bzlTransitiveDigest": "Xpqjnjzy6zZ90Es9Wa888ZLHhn7IsNGbph/e6qoxzw8=",
"usagesDigest": "qI5PVlIum/YAnGJg5oXGHzDkMFWt2aNSUZY4G8PBbic=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"uv": {
"bzlFile": "@@rules_python~//python/uv/private:uv_toolchains_repo.bzl",
"ruleClassName": "uv_toolchains_repo",
"attributes": {
"toolchain_type": "'@@rules_python~//python/uv:uv_toolchain_type'",
"toolchain_names": [
"none"
],
"toolchain_implementations": {
"none": "'@@rules_python~//python:none'"
},
"toolchain_compatible_with": {
"none": [
"@platforms//:incompatible"
]
},
"toolchain_target_settings": {}
}
}
},
"recordedRepoMappingEntries": [
[
"rules_python~",
"platforms",
"platforms"
]
]
}
},
"@@rules_swift~//swift:extensions.bzl%non_module_deps": {
"general": {
"bzlTransitiveDigest": "l+Zu+SMObRQy3DG2LEw0eGVPkYRnyVj+M1QyR5AAFmM=",
"usagesDigest": "l2vIL7SL4tJqHIVLWd78Y/ym+r0II64lmvoX8o+0Bb0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_github_apple_swift_protobuf": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-protobuf/archive/1.20.2.tar.gz"
],
"sha256": "3fb50bd4d293337f202d917b6ada22f9548a0a0aed9d9a4d791e6fbd8a246ebb",
"strip_prefix": "swift-protobuf-1.20.2/",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_protobuf/BUILD.overlay"
}
},
"com_github_grpc_grpc_swift": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/grpc/grpc-swift/archive/1.16.0.tar.gz"
],
"sha256": "58b60431d0064969f9679411264b82e40a217ae6bd34e17096d92cc4e47556a5",
"strip_prefix": "grpc-swift-1.16.0/",
"build_file": "@@rules_swift~//third_party:com_github_grpc_grpc_swift/BUILD.overlay"
}
},
"com_github_apple_swift_docc_symbolkit": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-docc-symbolkit/archive/refs/tags/swift-5.10-RELEASE.tar.gz"
],
"sha256": "de1d4b6940468ddb53b89df7aa1a81323b9712775b0e33e8254fa0f6f7469a97",
"strip_prefix": "swift-docc-symbolkit-swift-5.10-RELEASE",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_docc_symbolkit/BUILD.overlay"
}
},
"com_github_apple_swift_nio": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio/archive/2.42.0.tar.gz"
],
"sha256": "e3304bc3fb53aea74a3e54bd005ede11f6dc357117d9b1db642d03aea87194a0",
"strip_prefix": "swift-nio-2.42.0/",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio/BUILD.overlay"
}
},
"com_github_apple_swift_nio_http2": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio-http2/archive/1.26.0.tar.gz"
],
"sha256": "f0edfc9d6a7be1d587e5b403f2d04264bdfae59aac1d74f7d974a9022c6d2b25",
"strip_prefix": "swift-nio-http2-1.26.0/",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_http2/BUILD.overlay"
}
},
"com_github_apple_swift_nio_transport_services": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio-transport-services/archive/1.15.0.tar.gz"
],
"sha256": "f3498dafa633751a52b9b7f741f7ac30c42bcbeb3b9edca6d447e0da8e693262",
"strip_prefix": "swift-nio-transport-services-1.15.0/",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_transport_services/BUILD.overlay"
}
},
"com_github_apple_swift_nio_extras": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio-extras/archive/1.4.0.tar.gz"
],
"sha256": "4684b52951d9d9937bb3e8ccd6b5daedd777021ef2519ea2f18c4c922843b52b",
"strip_prefix": "swift-nio-extras-1.4.0/",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_extras/BUILD.overlay"
}
},
"com_github_apple_swift_log": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-log/archive/1.4.4.tar.gz"
],
"sha256": "48fe66426c784c0c20031f15dc17faf9f4c9037c192bfac2f643f65cb2321ba0",
"strip_prefix": "swift-log-1.4.4/",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_log/BUILD.overlay"
}
},
"com_github_apple_swift_nio_ssl": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-nio-ssl/archive/2.23.0.tar.gz"
],
"sha256": "4787c63f61dd04d99e498adc3d1a628193387e41efddf8de19b8db04544d016d",
"strip_prefix": "swift-nio-ssl-2.23.0/",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_ssl/BUILD.overlay"
}
},
"com_github_apple_swift_collections": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-collections/archive/1.0.4.tar.gz"
],
"sha256": "d9e4c8a91c60fb9c92a04caccbb10ded42f4cb47b26a212bc6b39cc390a4b096",
"strip_prefix": "swift-collections-1.0.4/",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_collections/BUILD.overlay"
}
},
"com_github_apple_swift_atomics": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/apple/swift-atomics/archive/1.1.0.tar.gz"
],
"sha256": "1bee7f469f7e8dc49f11cfa4da07182fbc79eab000ec2c17bfdce468c5d276fb",
"strip_prefix": "swift-atomics-1.1.0/",
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_atomics/BUILD.overlay"
}
},
"build_bazel_rules_swift_index_import": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file": "@@rules_swift~//third_party:build_bazel_rules_swift_index_import/BUILD.overlay",
"canonical_id": "index-import-5.8",
"urls": [
"https://github.com/MobileNativeFoundation/index-import/releases/download/5.8.0.1/index-import.tar.gz"
],
"sha256": "28c1ffa39d99e74ed70623899b207b41f79214c498c603915aef55972a851a15"
}
},
"build_bazel_rules_swift_local_config": {
"bzlFile": "@@rules_swift~//swift/internal:swift_autoconfiguration.bzl",
"ruleClassName": "swift_autoconfiguration",
"attributes": {}
}
},
"recordedRepoMappingEntries": [
[
"rules_swift~",
"bazel_tools",
"bazel_tools"
]
]
}
}
}
}
-3
View File
@@ -1,3 +0,0 @@
# This file marks sparkle_workspace as a separate Bazel workspace.
# It prevents Bazel from looking up to the parent directory's workspace.
# The actual dependencies are defined in MODULE.bazel (bzlmod).
@@ -13,6 +13,7 @@
#include <cstring>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
@@ -147,6 +148,9 @@ public:
static auto FromPath(const string& path) -> byte_vector {
std::ifstream inputFileStream(path, std::ios::binary | std::ios::ate);
if (!inputFileStream.is_open()) {
throw std::runtime_error("Failed to open file: " + path);
}
const std::streamsize size = inputFileStream.tellg();
inputFileStream.seekg(0, std::ios::beg);
byte_vector serializedGame((size_t(size)));
@@ -32,6 +32,32 @@ CoordsSet AICommandFilter::BuildEnemyLocations(const GameStateW& gameState, Play
return enemyLocations;
}
std::vector<size_t> AICommandFilter::FilterLoopingCommands(
const CommandListSPtr& commands,
const GameStateW& gameState) {
std::vector<size_t> filteredIndices;
filteredIndices.reserve(commands->size());
for (size_t i = 0; i < commands->size(); ++i) {
const auto& cmd = (*commands)[i];
bool shouldFilter = false;
if (cmd->GetCommandType() == CommandType::METEOR_TARGET_COMMAND ||
cmd->GetCommandType() == CommandType::METEOR_CANCEL_COMMAND) {
const int unitId = cmd->GetActorUnitId();
const Unit* unit = gameState->units()->Get(unitId);
if (unit->attached_hero().profession_info().cast_target().row() > -1) {
shouldFilter = true;
}
}
if (!shouldFilter) { filteredIndices.push_back(i); }
}
return filteredIndices;
}
std::vector<size_t> AICommandFilter::FilterCommands(
const CommandListSPtr& commands,
PlayerId pid,
@@ -138,6 +164,17 @@ bool AICommandFilter::IsWastefulAction(
break;
}
case CommandType::METEOR_TARGET_COMMAND:
case CommandType::METEOR_CANCEL_COMMAND: {
// Filter out re-target/cancel when the mage already has a target.
// Re-targeting exists for human misclick correction; the AI commits
// to its first target choice and doesn't need to reconsider.
const int unitId = cmd.GetActorUnitId();
const Unit* unit = gameState->units()->Get(unitId);
if (unit->attached_hero().profession_info().cast_target().row() > -1) { return true; }
break;
}
case CommandType::START_FIRE_COMMAND: {
// Fire spell filtering - be very restrictive for attackers
// Fire only affects adjacent tiles and lasts multiple rounds
@@ -44,6 +44,18 @@ public:
const APDCache& apdCache,
const BattalionTypeGetter& battalionTypeLookup);
/**
* Lightweight filter for the root of the search tree.
* Only removes commands that are genuinely never useful (e.g. meteor
* re-targeting when a target is already set), as opposed to the full
* heuristic filter which aggressively prunes for lookahead performance.
* The full filter could miss good moves at the root that only look
* bad locally but prove worthwhile with deeper search.
*/
static std::vector<size_t> FilterLoopingCommands(
const CommandListSPtr& commands,
const GameStateW& gameState);
private:
// Helper to build enemy locations once for efficiency
static CoordsSet BuildEnemyLocations(const GameStateW& gameState, PlayerId pid);
@@ -11,6 +11,7 @@
#include "AIAttackerStrategySelector.hpp"
#include "AICommandEvaluator.hpp"
#include "AICommandFilter.hpp"
#include "TranspositionTable.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/score/AIScoreCalculator.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
@@ -59,6 +60,12 @@ auto IterativeDeepeningAI::IterativeSearch(
return result;
}
// Lightweight root filter: only remove genuinely never-useful commands
// (e.g. meteor re-targeting). The full aggressive filter is reserved for
// lookahead where pruning is a performance optimization, not a decision.
const std::vector<size_t> rootFilteredIndices =
AICommandFilter::FilterLoopingCommands(commands, state);
// Check if we're in SET_UP phase and enforce maximum depth limit
bool isSetupPhase =
(state->status()->state() ==
@@ -90,7 +97,8 @@ auto IterativeDeepeningAI::IterativeSearch(
std::vector<size_t> sortedIndices = GetCommandsSortedByPreviousDepth(
currentDepth,
scoresByDepth,
highestDepthCompleted);
highestDepthCompleted,
rootFilteredIndices);
size_t evaluatedCount = 0;
bool allEvaluated = true;
@@ -344,15 +352,15 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
const size_t currentDepth,
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<size_t>& highestDepthCompleted) -> std::vector<size_t> {
std::vector<size_t> indices(scoresByDepth.size());
std::iota(indices.begin(), indices.end(), 0);
const std::vector<size_t>& highestDepthCompleted,
const std::vector<size_t>& filteredIndices) -> std::vector<size_t> {
if (currentDepth == 1) {
// For depth 1, return natural order
return indices;
// For depth 1, return filtered indices in natural order
return filteredIndices;
}
std::vector<size_t> indices = filteredIndices;
// Sort by score at previous depth
const size_t prevDepth = currentDepth - 1;
std::ranges::sort(indices, [&](const size_t a, const size_t b) {
@@ -101,7 +101,8 @@ private:
[[nodiscard]] static std::vector<size_t> GetCommandsSortedByPreviousDepth(
size_t currentDepth,
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
const std::vector<size_t>& highestDepthCompleted);
const std::vector<size_t>& highestDepthCompleted,
const std::vector<size_t>& filteredIndices);
[[nodiscard]] static SearchResult SelectBestResult(
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
@@ -8,6 +8,8 @@
#include "ShardokGameController.hpp"
#include <execinfo.h>
#include <algorithm>
#include <iterator>
#include <ranges>
@@ -153,28 +155,48 @@ void ShardokGameController::DoAIThread() {
continue;
}
// Phase 2: AI thinks (NO LOCK - this is the slow part)
const auto results = aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
try {
// Phase 2: AI thinks (NO LOCK - this is the slow part)
const auto results = aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
// Phase 3: Post the command (brief lock)
{
unique_lock lk(masterLock);
// Phase 3: Post the command (brief lock)
{
unique_lock lk(masterLock);
// Verify state hasn't changed while we were thinking
if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
// State changed (e.g., human posted command) - re-evaluate
printf("AI: State changed while thinking, re-evaluating\n");
continue;
// Verify state hasn't changed while we were thinking
if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
// State changed (e.g., human posted command) - re-evaluate
printf("AI: State changed while thinking, re-evaluating\n");
continue;
}
if (engine->GameIsOver()) {
aiThreadKeepGoing = false;
continue;
}
engine->PostCommand(playerId, results.chosenIndex);
LockedNotifyClients();
aiThreadKeepGoing = !engine->GameIsOver();
}
} catch (const std::exception &e) {
fprintf(stderr,
"AI thread exception in game %s, player %d, round %d: %s\n",
cachedGameId.c_str(),
playerId,
gsv.current_round(),
e.what());
if (engine->GameIsOver()) {
aiThreadKeepGoing = false;
continue;
}
void *backtraceArray[20];
const int backtraceSize = backtrace(backtraceArray, 20);
backtrace_symbols_fd(backtraceArray, backtraceSize, STDERR_FILENO);
engine->PostCommand(playerId, results.chosenIndex);
LockedNotifyClients();
aiThreadKeepGoing = !engine->GameIsOver();
aiThreadErrorMessage = "AI error in game " + cachedGameId + ", player " +
std::to_string(playerId) + ", round " +
std::to_string(gsv.current_round()) + ": " + e.what();
aiThreadFailed.store(true);
updateCondition.notify_all();
break;
}
}
printf("Exiting AI thread.\n");
@@ -374,31 +396,39 @@ void ShardokGameController::UnregisterSubscriber(const StreamSubscriber *subscri
auto ShardokGameController::WaitForUpdatesAndPush(
std::shared_ptr<StreamSubscriber> subscriber,
int64_t startingActionId) -> bool {
int64_t startingActionId) -> WaitResult {
int64_t lastPushedActionId = startingActionId;
while (subscriber->IsActive()) {
bool gameOver = false;
bool tutorialReset = false;
GameOverInfo gameOverInfo{};
{
unique_lock<mutex> guard(masterLock);
// Wait for updates or game over
// Wait for updates, game over, or AI thread failure
updateCondition.wait(guard, [this, lastPushedActionId] {
return engine->GetUnfilteredHistoryCount() >
static_cast<size_t>(lastPushedActionId) ||
engine->GameIsOver();
engine->GameIsOver() || aiThreadFailed.load();
});
if (!subscriber->IsActive()) { return false; }
if (!subscriber->IsActive()) { return WaitResult::DISCONNECTED; }
if (aiThreadFailed.load()) { return WaitResult::DISCONNECTED; }
gameOver = engine->GameIsOver();
if (gameOver) {
gameOverInfo.gameStatus = fb::ToProto(engine->GetGameStatus());
gameOverInfo.playerInfos = engine->GetPlayerInfos();
gameOverInfo.endGameUnits = engine->EndGameUnits();
// Check if this is a tutorial battle where the defender lost
tutorialReset = engine->IsTutorialBattleEnabled() && engine->DidDefenderLose();
if (!tutorialReset) {
gameOverInfo.gameStatus = fb::ToProto(engine->GetGameStatus());
gameOverInfo.playerInfos = engine->GetPlayerInfos();
gameOverInfo.endGameUnits = engine->EndGameUnits();
}
}
}
// Lock released - GetUpdates will acquire its own lock
@@ -418,14 +448,20 @@ auto ShardokGameController::WaitForUpdatesAndPush(
updates.currentGameState);
}
// Tutorial battle reset: defender lost, signal reset instead of game over
if (tutorialReset) {
subscriber->OnBattleReset();
return WaitResult::BATTLE_RESET;
}
// Now send gameOver notification after all updates have been sent
if (gameOver) {
subscriber->OnGameOver(gameOverInfo);
return true;
return WaitResult::GAME_OVER;
}
}
return false; // Subscriber disconnected
return WaitResult::DISCONNECTED; // Subscriber disconnected
}
} // namespace shardok
@@ -9,6 +9,7 @@
#ifndef ShardokGameController_hpp
#define ShardokGameController_hpp
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
@@ -55,6 +56,13 @@ struct OnePlayerUpdates {
availableCommands(acs) {}
};
/// Result of WaitForUpdatesAndPush indicating how the wait ended
enum class WaitResult {
GAME_OVER, // Game ended normally
DISCONNECTED, // Subscriber disconnected
BATTLE_RESET // Tutorial battle reset (defender lost)
};
/// Interface for subscribers that receive streaming updates from a game
class StreamSubscriber {
public:
@@ -70,6 +78,9 @@ public:
/// Called when the game ends
virtual void OnGameOver(const GameOverInfo& info) = 0;
/// Called when a tutorial battle resets (defender lost, battle will restart)
virtual void OnBattleReset() = 0;
/// Returns true if this subscriber is still active and should receive updates
[[nodiscard]] virtual auto IsActive() const -> bool = 0;
};
@@ -100,6 +111,10 @@ private:
const string mapName;
const string logFilePath;
// AI thread error state - written once by AI thread before setting atomic flag
std::atomic<bool> aiThreadFailed{false};
std::string aiThreadErrorMessage;
vector<shared_ptr<ShardokAIClient>> aiClients;
std::thread aiThread;
@@ -169,6 +184,9 @@ public:
[[nodiscard]] auto GetLogFilePath() const -> string { return logFilePath; }
[[nodiscard]] auto HasAIThreadError() const -> bool { return aiThreadFailed.load(); }
[[nodiscard]] auto GetAIThreadError() const -> std::string { return aiThreadErrorMessage; }
/// Register a subscriber to receive streaming updates for this game.
/// The subscriber will receive updates until it becomes inactive or is unregistered.
void RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber);
@@ -177,11 +195,10 @@ public:
void UnregisterSubscriber(const StreamSubscriber* subscriber);
/// Wait for game updates, pushing them to the given subscriber.
/// Blocks until the game ends or the subscriber becomes inactive.
/// Returns true if the game ended normally, false if subscriber disconnected.
/// Blocks until the game ends, subscriber disconnects, or tutorial battle resets.
auto WaitForUpdatesAndPush(
std::shared_ptr<StreamSubscriber> subscriber,
int64_t startingActionId) -> bool;
int64_t startingActionId) -> WaitResult;
};
} // namespace shardok
@@ -29,6 +29,17 @@ class AvailableCommandsFactoryImpl : public AvailableCommandsFactory {
private:
const SettingsGetter settings;
static auto CannotBecomeOutlaw(const GameStateW &gameState, PlayerId pid) -> bool {
if (pid == UNCONTROLLED_PLAYER_ID) return false;
const auto &player = std::find_if(
begin(*gameState->player_infos()),
end(*gameState->player_infos()),
[pid](const PlayerInfoFb *pi) { return pi->player_id() == pid; });
return (player != end(*gameState->player_infos()) && player->cannot_become_outlaw());
}
static auto IsAttacker(const GameStateW &gameState, PlayerId pid) -> bool {
if (pid == UNCONTROLLED_PLAYER_ID) return false;
@@ -98,6 +109,7 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
const bool hasHero = unit->has_attached_hero();
const auto allyPids = AlliedPids(gameState, unit->player_id());
const bool isAttacker = IsAttacker(gameState, unit->player_id());
const bool cannotBecomeOutlaw = CannotBecomeOutlaw(gameState, unit->player_id());
const bool unitMovedIntoZoc = unit->has_moved_in_zoc();
const ActionPoints remainingActionPoints = unit->remaining_action_points();
@@ -134,6 +146,7 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
.units = gameState->units(),
.allyPids = allyPids,
.isAttacker = isAttacker,
.cannotBecomeOutlaw = cannotBecomeOutlaw,
.unitMovedIntoZoc = unitMovedIntoZoc};
for (const auto &commandFactory : commandFactories) {
@@ -219,6 +219,27 @@ public:
}
[[nodiscard]] inline auto GameIsOver() const -> bool { return GameIsOver(GetGameStatus()); }
[[nodiscard]] auto IsTutorialBattleEnabled() const -> bool {
return tutorialController_.IsEnabled();
}
/// Returns true if the game is over and the defender lost (not in winning_shardok_ids).
[[nodiscard]] auto DidDefenderLose() const -> bool {
if (!GameIsOver()) return false;
const auto *status = GetGameStatus();
const auto *winIds = status->winning_shardok_ids();
for (const auto *pi : *GetCurrentGameState()->player_infos()) {
if (pi->is_defender()) {
if (!winIds) return true;
for (const auto wid : *winIds) {
if (wid == pi->player_id()) return false;
}
return true;
}
}
return false;
}
};
} // namespace shardok
@@ -29,7 +29,6 @@ using Unit = net::eagle0::shardok::storage::fb::Unit;
inline auto GetMutableUnit(GameState *state, UnitId unitId) -> Unit * {
return const_cast<Unit *>(state->mutable_units()->GetMutableObject(unitId));
}
using net::eagle0::shardok::storage::fb::DrawType;
using net::eagle0::shardok::storage::fb::VictoryCondition;
using net::eagle0::shardok::storage::fb::VictoryType;
@@ -259,23 +258,6 @@ void MutatingApplyResult(
->mutable_end_game_condition()
->mutate_victory_type(VictoryType::VictoryType_UNKNOWN_VICTORY_TYPE);
break;
case net::eagle0::shardok::common::EndGameCondition::kAllyVictory:
mutatingGameState->mutable_status()
->mutable_end_game_condition()
->mutate_victory_type(VictoryType::VictoryType_ALLY_VICTORY);
mutatingGameState->mutable_status()
->mutable_end_game_condition()
->mutate_victory_details(static_cast<VictoryCondition>(
result.game_status().end_game_condition().ally_victory()));
break;
case net::eagle0::shardok::common::EndGameCondition::kDraw:
mutatingGameState->mutable_status()
->mutable_end_game_condition()
->mutate_victory_type(VictoryType::VictoryType_DRAW);
mutatingGameState->mutable_status()
->mutable_end_game_condition()
->mutate_draw_details(static_cast<DrawType>(
result.game_status().end_game_condition().draw()));
case net::eagle0::shardok::common::EndGameCondition::kLoss:
mutatingGameState->mutable_status()
->mutable_end_game_condition()
@@ -211,21 +211,13 @@ auto UpdateGameStatusAction::InternalExecute(
}
}
results.emplace_back();
results[0].set_type(net::eagle0::shardok::common::GAME_OVER);
results[0].mutable_game_status()->set_state(GameStatusProto::DRAW);
results[0].mutable_game_status()->clear_winning_shardok_ids();
results[0].mutable_game_status()->mutable_end_game_condition()->set_draw(
net::eagle0::shardok::common::DRAW_AFTER_MAX_ROUNDS);
results[0].mutable_game_status()->set_description(
"Draw! No victory after " + std::to_string(settingsGetter.Backing().max_rounds()) +
" rounds");
return results;
throw ShardokInternalErrorException(
"No player has WIN_AFTER_MAX_ROUNDS but max rounds exceeded");
}
// check for castle occupation
if (!criticalTileLocations.empty()) {
// First check: single player holding all critical tiles
for (const auto* pi : *gameState->player_infos()) {
if (HasVictoryCondition(
pi,
@@ -261,6 +253,30 @@ auto UpdateGameStatusAction::InternalExecute(
results[0].mutable_game_status()->set_state(
GameStatusProto::State::GameStatus_State_VICTORY);
results[0].mutable_game_status()->add_winning_shardok_ids(pi->player_id());
// Also add mutually-allied players with HoldsCriticalTiles
for (const auto* otherPi : *gameState->player_infos()) {
if (otherPi->player_id() == pi->player_id()) continue;
if (!HasVictoryCondition(
otherPi,
net::eagle0::shardok::storage::fb::
VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES))
continue;
bool mutuallyAllied =
std::ranges::any_of(
*pi->allies(),
[otherPi](const auto* ap) {
return ap->player_id() == otherPi->player_id();
}) &&
std::ranges::any_of(*otherPi->allies(), [pi](const auto* ap) {
return ap->player_id() == pi->player_id();
});
if (mutuallyAllied) {
results[0].mutable_game_status()->add_winning_shardok_ids(
otherPi->player_id());
}
}
results[0].mutable_game_status()->mutable_end_game_condition()->set_victory(
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
results[0].mutable_game_status()->set_description(
@@ -270,6 +286,71 @@ auto UpdateGameStatusAction::InternalExecute(
}
}
}
// Second check: allied players collectively holding all critical tiles
unordered_set<PlayerId> occupantPlayerIds{};
bool allTilesOccupiedWithHeroes = true;
for (const auto& criticalTile : criticalTileLocations) {
const auto* possibleOccupant = currentState.GetOccupant(criticalTile);
if (!possibleOccupant || !possibleOccupant->has_attached_hero()) {
allTilesOccupiedWithHeroes = false;
break;
}
occupantPlayerIds.insert(possibleOccupant->player_id());
}
if (allTilesOccupiedWithHeroes && occupantPlayerIds.size() > 1) {
// All occupants must have the HOLDS_CRITICAL_TILES victory condition
bool allHaveCondition = true;
for (const PlayerId pid : occupantPlayerIds) {
const auto* pi = GetPlayerInfo(pid);
if (!HasVictoryCondition(
pi,
net::eagle0::shardok::storage::fb::
VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES)) {
allHaveCondition = false;
break;
}
}
if (allHaveCondition) {
// Check mutual alliance among all occupants
bool allAllied = true;
for (const PlayerId pid1 : occupantPlayerIds) {
const auto* p1Info = GetPlayerInfo(pid1);
for (const PlayerId pid2 : occupantPlayerIds) {
if (pid1 == pid2) continue;
if (!std::ranges::any_of(
*p1Info->allies(),
[pid2](const net::eagle0::shardok::storage::fb::AlliedPlayer*
alliedPlayer) {
return alliedPlayer->player_id() == pid2;
})) {
allAllied = false;
break;
}
}
if (!allAllied) break;
}
if (allAllied) {
results.emplace_back();
results[0].set_type(net::eagle0::shardok::common::GAME_OVER);
results[0].mutable_game_status()->set_state(
GameStatusProto::State::GameStatus_State_VICTORY);
*results[0].mutable_game_status()->mutable_winning_shardok_ids() = {
std::begin(occupantPlayerIds),
std::end(occupantPlayerIds)};
results[0].mutable_game_status()->mutable_end_game_condition()->set_victory(
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
results[0].mutable_game_status()->set_description(
"Alliance victory! Allied attackers collectively control all "
"critical tiles");
ResolveHiddenLosers(results[0]);
return results;
}
}
}
}
// If we got all the way through, keep the status as it is
@@ -10,6 +10,7 @@ cc_library(
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
],
deps = [
":blow_bridge_command_factory",
":brave_water_command_factory",
":build_bridge_command_factory",
":challenge_duel_command_factory",
@@ -38,6 +39,25 @@ cc_library(
],
)
cc_library(
name = "blow_bridge_command_factory",
srcs = ["BlowBridgeCommandFactory.cpp"],
hdrs = ["BlowBridgeCommandFactory.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/library:__subpackages__",
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
],
deps = [
":command_factory",
"//src/main/cpp/net/eagle0/shardok/library:shardok_command",
"//src/main/cpp/net/eagle0/shardok/library/commands:blow_bridge_command",
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
"//src/main/cpp/net/eagle0/shardok/library/util:combat_utils",
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
],
)
cc_library(
name = "brave_water_command_factory",
srcs = [
@@ -0,0 +1,104 @@
//
// Created by Claude on 3/14/26.
//
#include "BlowBridgeCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/commands/BlowBridgeCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
namespace shardok {
[[nodiscard]] auto GetBlowBridgeOdds(
const SettingsGetter& settings,
double strength,
double agility,
double intelligence,
bool hasForestAccess) -> PercentileRollOdds;
void BlowBridgeCommandFactory::AddAvailableBlowBridgeCommands(
CommandList& commands,
const Unit* unit,
const ActionPoints remainingActionPoints,
const Coords& position,
const HexMap* hexMap,
const Units* units,
const vector<PlayerId>& allyPids) const {
const PlayerId playerId = unit->player_id();
if (!unit->has_attached_hero()) return;
const auto& hero = unit->attached_hero();
if (hero.profession_info().profession() !=
net::eagle0::shardok::storage::fb::Profession_ENGINEER)
return;
// Must be on a bridge tile
const auto* currentTerrain = GetTerrain(hexMap, position);
if (!currentTerrain->modifier().bridge().present()) return;
const ActionCost cost =
settings.ActionCostFor(settings.Backing().blow_bridge_action_point_cost());
if (!cost.IsPossible(remainingActionPoints)) return;
const bool nearbyForest = HasForestAccess(position, units, hexMap, allyPids, playerId);
const auto odds = GetBlowBridgeOdds(
settings,
hero.strength(),
hero.agility(),
hero.wisdom(),
nearbyForest);
for (const Coords& adjCoords : HexMapUtils::GetAdjacentCoords(hexMap, position)) {
if (IsTraversible(*GetTerrain(hexMap, adjCoords)) && !Occupant(units, adjCoords)) {
commands.push_back(std::make_shared<BlowBridgeCommand>(
settings.ActionCostFor(settings.Backing().blow_bridge_action_point_cost()),
unit->player_id(),
unit->unit_id(),
adjCoords,
position,
*currentTerrain,
nearbyForest,
settings.Backing().blow_bridge_agility_xp(),
settings.Backing().blow_bridge_strength_xp(),
settings.Backing().minimum_knowledge_for_profession(),
odds));
}
}
}
void BlowBridgeCommandFactory::AddAvailableCommands(
CommandList& commands,
const CommandParams& params) const {
AddAvailableBlowBridgeCommands(
commands,
params.unit,
params.remainingActionPoints,
params.position,
params.hexMap,
params.units,
params.allyPids);
}
auto GetBlowBridgeOdds(
const SettingsGetter& settings,
const double strength,
const double agility,
const double intelligence,
const bool hasForestAccess) -> PercentileRollOdds {
const int16_t base = settings.Backing().blow_bridge_base_odds();
constexpr int16_t terrainFactor = 0;
constexpr int16_t weatherFactor = 0;
constexpr int16_t windFactor = 0;
const std::vector stats = {strength, agility, intelligence};
std::vector<OtherFactor> otherFactors;
if (hasForestAccess) {
otherFactors.push_back(
MakeOtherFactor(settings.Backing().blow_bridge_forest_bonus(), "forest nearby"));
}
return MakeOdds(base, terrainFactor, weatherFactor, windFactor, stats, otherFactors);
}
} // namespace shardok
@@ -0,0 +1,37 @@
//
// Created by Claude on 3/14/26.
//
#ifndef EAGLE0_BLOWBRIDGECOMMANDFACTORY_HPP
#define EAGLE0_BLOWBRIDGECOMMANDFACTORY_HPP
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/CommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
namespace shardok {
class BlowBridgeCommandFactory : public CommandFactory {
private:
const SettingsGetter settings;
public:
explicit BlowBridgeCommandFactory(const SettingsGetter& getter) : settings(getter){};
void AddAvailableBlowBridgeCommands(
CommandList& commands,
const Unit* unit,
ActionPoints remainingActionPoints,
const Coords& position,
const HexMap* hexMap,
const Units* units,
const vector<PlayerId>& allyPids) const;
void AddAvailableCommands(CommandList& commands, const CommandParams& params) const override;
};
} // namespace shardok
#endif // EAGLE0_BLOWBRIDGECOMMANDFACTORY_HPP
@@ -4,6 +4,7 @@
#include "CommandFactoriesList.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/BlowBridgeCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/BraveWaterCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/BuildBridgeCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/ChallengeDuelCommandFactory.hpp"
@@ -41,6 +42,7 @@ auto MakeFactories(const SettingsGetter& settings) -> vector<shared_ptr<const Co
return {make_shared<MeteorTargetCommandFactory>(settings),
make_shared<ControlCommandFactory>(settings),
make_shared<ArcheryCommandFactory>(settings),
make_shared<BlowBridgeCommandFactory>(settings),
make_shared<BraveWaterCommandFactory>(settings),
make_shared<BuildBridgeCommandFactory>(settings),
make_shared<ChallengeDuelCommandFactory>(settings),
@@ -35,6 +35,7 @@ public:
const Units* units;
const vector<PlayerId>& allyPids;
const bool isAttacker;
const bool cannotBecomeOutlaw;
const bool unitMovedIntoZoc;
};
@@ -25,7 +25,8 @@ void FleeCommandFactory::AddAvailableFleeCommands(
const Units *allUnits,
const HexMap *map,
const vector<PlayerId> &allyPids,
const ActionPoints remainingActionPoints) const {
const ActionPoints remainingActionPoints,
const bool cannotBecomeOutlaw) const {
if (!settings.ActionCostFor(settings.Backing().flee_action_point_cost())
.IsPossible(remainingActionPoints))
return;
@@ -83,7 +84,7 @@ void FleeCommandFactory::AddAvailableFleeCommands(
unit->player_id(),
unit->unit_id(),
odds));
} else {
} else if (!cannotBecomeOutlaw) {
commands.push_back(std::make_shared<BecomeOutlawCommand>(
settings.ActionCostFor(settings.Backing().flee_action_point_cost()),
unit->player_id(),
@@ -100,7 +101,8 @@ void FleeCommandFactory::AddAvailableCommands(
params.units,
params.hexMap,
params.allyPids,
params.remainingActionPoints);
params.remainingActionPoints,
params.cannotBecomeOutlaw);
}
} // namespace shardok
@@ -27,7 +27,8 @@ public:
const Units *allUnits,
const HexMap *map,
const vector<PlayerId> &allyPids,
ActionPoints remainingActionPoints) const;
ActionPoints remainingActionPoints,
bool cannotBecomeOutlaw) const;
void AddAvailableCommands(CommandList &commands, const CommandParams &params) const override;
@@ -21,13 +21,18 @@ auto MeteorTargetCommandFactory::AddAvailableMeteorTargetCommands(
if (unit->attached_hero().profession_info().meteor_cast_state() !=
net::eagle0::shardok::storage::fb::MultiroundMagicState_TARGET)
return;
if (unit->attached_hero().profession_info().cast_target().row() > -1) return;
// If the mage already has a target, these commands are optional (re-targeting).
// If no target yet, the player must choose before ending their turn.
const bool isRetarget = unit->attached_hero().profession_info().cast_target().row() > -1;
const bool requiredToEndTurn = !isRetarget;
existingCommands.emplace_back(std::make_shared<MeteorCancelCommand>(
settings.ActionCostFor(settings.Backing().meteor_cancel_action_point_cost()),
settings,
unit->player_id(),
unit->unit_id()));
unit->unit_id(),
requiredToEndTurn));
const Coords &unitLocation = unit->location();
for (const Coords &meteorCoords : CoordsInMeteorRange(map, unitLocation, settings)) {
@@ -37,7 +42,8 @@ auto MeteorTargetCommandFactory::AddAvailableMeteorTargetCommands(
unit->player_id(),
unit->unit_id(),
meteorCoords,
settings.Backing().minimum_knowledge_for_profession()));
settings.Backing().minimum_knowledge_for_profession(),
requiredToEndTurn));
}
}
void MeteorTargetCommandFactory::AddAvailableCommands(
@@ -49,10 +49,10 @@ auto ReduceCommandFactory::AddAvailableReduceCommands(
const auto *terrain = GetTerrain(map, reduceCoords);
// Allow reducing if there's a bridge or castle with integrity > 0, OR an enemy
// Allow reducing if there's a castle with integrity > 0, OR an enemy
// occupant. We already continued in the case of friendly occupants.
if (terrain->modifier().bridge().present() ||
(terrain->modifier().castle().present() &&
// Note: bridges are no longer valid reduce targets (use Blow Bridge instead).
if ((terrain->modifier().castle().present() &&
terrain->modifier().castle().integrity() > 0.0) ||
enemyOccupant) {
existingCommands.push_back(std::make_shared<ReduceCommand>(
@@ -84,6 +84,25 @@ void ArcheryCommandFactory::AddAvailableArcheryCommands(
targetCoords += HexMapUtils::GetAdjacentCoords(hexMap, position);
}
// Wind-assisted range 3 for longbowmen
if (battalionType->alwaysArcheryCapable &&
weather->wind().speed_in_mph() >=
settings.Backing().longbow_wind_bonus_range_min_speed()) {
const int windDir = (int)weather->wind().direction();
for (const Coords &farCoords : TilesWithExactDistance(hexMap, position, 3)) {
auto dirTo = DirectionsTo(hexMap, position, farCoords);
int relMain = ((int)dirTo.main - windDir + 6) % 6;
bool inCone = (relMain == 0 || relMain == 1 || relMain == 5);
if (!inCone && dirTo.usesSecondary) {
int relSec = ((int)dirTo.secondary - windDir + 6) % 6;
inCone = (relSec == 0 || relSec == 1 || relSec == 5);
}
if (inCone && !HexMapUtils::LineIsBlockedByMountains(hexMap, position, farCoords)) {
targetCoords.Add(farCoords);
}
}
}
for (const Coords &archeryCoords : targetCoords) {
const auto *const enemyOccupant =
KnownEnemyOccupant(unit->player_id(), units, allyPids, archeryCoords);
@@ -39,6 +39,28 @@ cc_library(
],
)
cc_library(
name = "blow_bridge_command",
srcs = ["BlowBridgeCommand.cpp"],
hdrs = ["BlowBridgeCommand.hpp"],
copts = COPTS,
visibility = [
"//src/main/cpp/net/eagle0/shardok/library:__subpackages__",
"//src/test/cpp/net/eagle0/shardok/ai:__subpackages__",
"//src/test/cpp/net/eagle0/shardok/library:__subpackages__",
],
deps = [
"//src/main/cpp/net/eagle0/shardok/library:action_cost",
"//src/main/cpp/net/eagle0/shardok/library:shardok_command",
"//src/main/cpp/net/eagle0/shardok/library/actions:action_requires_hero_exception",
"//src/main/cpp/net/eagle0/shardok/library/map:tile_modifier_with_coords",
"//src/main/cpp/net/eagle0/shardok/library/unit",
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
"//src/main/cpp/net/eagle0/shardok/library/view_filters:odds_filter",
"//src/main/protobuf/net/eagle0/shardok/common:terrain_cc_proto",
],
)
cc_library(
name = "build_bridge_command",
srcs = ["BuildBridgeCommand.cpp"],
@@ -0,0 +1,82 @@
//
// Created by Claude on 3/14/26.
//
#include "BlowBridgeCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/actions/ActionRequiresHeroException.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/TileModifierWithCoords.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/OddsFilter.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/tile_modifier.pb.h"
namespace shardok {
using net::eagle0::shardok::common::ActionType;
auto BlowBridgeCommand::InternalExecuteWithRoll(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator,
const std::optional<int32_t> roll) const -> vector<ActionResult> {
const auto successRoll = 101 - roll.value_or(generator->Percentile());
return ExecuteWithRoll(currentState, successRoll);
}
auto BlowBridgeCommand::ExecuteWithRoll(const GameStateW& currentState, double roll) const
-> vector<ActionResult> {
const auto* actor = currentState->units()->Get(actorId);
if (!actor->has_attached_hero()) { throw ActionRequiresHeroException("blow bridge"); }
auto actorAfter = *actor;
MutatingSpendActionPoints(&actorAfter, cost);
MutatingBumpAgilityXp(&actorAfter, agilityXp);
MutatingBumpStrengthXp(&actorAfter, strengthXp);
MutatingBumpAllKnowledgeToMinimum(&actorAfter, minimumKnowledgeAfter);
// Always move engineer to target tile
actorAfter.mutable_location() = target;
ActionResult result{};
result.mutable_player()->set_value(GetPlayerId());
result.mutable_actor()->set_value(actorAfter.unit_id());
*result.mutable_target_coords() = ToCoordsProto(target);
result.mutable_roll()->set_value(roll);
*result.mutable_odds() = successOdds;
if (PercentileRollSucceeds(successOdds, roll)) {
result.set_type(ActionType::BLOW_BRIDGE);
auto newTileModifier = targetTerrain.modifier();
SetBridge(newTileModifier, 0.0);
*result.add_changed_tile_modifiers() = MakeTmc(bridgeCoords, newTileModifier);
} else {
result.set_type(ActionType::BLOW_BRIDGE_FAILED);
}
AddChangedUnit(result, actorAfter);
return {result};
}
auto BlowBridgeCommand::GetCommandProto() const -> CommandProto {
CommandProto proto{};
proto.set_player(GetPlayerId());
proto.set_type(net::eagle0::shardok::common::BLOW_BRIDGE_COMMAND);
proto.mutable_actor()->set_value(actorId);
*proto.mutable_target() = ToCoordsProto(target);
proto.mutable_roll_request()->set_roll_type(net::eagle0::shardok::api::ROLL_TYPE_D100);
proto.mutable_roll_request()->set_command_type(
net::eagle0::shardok::common::BLOW_BRIDGE_COMMAND);
proto.mutable_roll_request()->set_acting_unit_id(actorId);
*proto.mutable_odds() = OddsFilteredForPlayer(successOdds, GetPlayerId());
return proto;
}
auto BlowBridgeCommand::GetOddsPercentile() const -> int32_t {
return OddsFilteredForPlayer(successOdds, GetPlayerId()).success_chance();
}
} // namespace shardok
@@ -0,0 +1,82 @@
//
// Created by Claude on 3/14/26.
//
#ifndef EAGLE0_BLOWBRIDGECOMMAND_HPP
#define EAGLE0_BLOWBRIDGECOMMAND_HPP
#include <utility>
#include "src/main/cpp/net/eagle0/shardok/library/ActionCost.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
namespace shardok {
using Terrain = net::eagle0::shardok::storage::fb::Terrain;
using Unit = net::eagle0::shardok::storage::fb::Unit;
class BlowBridgeCommand : public ShardokCommand {
protected:
[[nodiscard]] auto InternalExecuteWithRoll(
const GameStateW& currentState,
const std::shared_ptr<RandomGenerator>& generator,
std::optional<int32_t> roll) const -> vector<ActionResult> override;
const ActionCost cost;
const UnitId actorId;
const Coords target;
const Coords bridgeCoords;
const Terrain targetTerrain;
const bool hasNearbyForest;
const int agilityXp;
const int strengthXp;
int minimumKnowledgeAfter;
const PercentileRollOdds successOdds;
public:
BlowBridgeCommand(
const ActionCost& cost,
PlayerId playerId,
UnitId actorId,
Coords target,
Coords bridgeCoords,
const Terrain& targetTerrain,
bool hasNearbyForest,
int agilityXp,
int strengthXp,
int opponentKnowledgeIncrease,
PercentileRollOdds successOdds)
: ShardokCommand(playerId),
cost(cost),
actorId(actorId),
target(std::move(target)),
bridgeCoords(std::move(bridgeCoords)),
targetTerrain(std::move(targetTerrain)),
hasNearbyForest(hasNearbyForest),
agilityXp(agilityXp),
strengthXp(strengthXp),
minimumKnowledgeAfter(opponentKnowledgeIncrease),
successOdds(std::move(successOdds)){};
auto ExecuteWithRoll(const GameStateW& currentState, double roll) const -> vector<ActionResult>;
~BlowBridgeCommand() override = default;
[[nodiscard]] auto GetCommandType() const -> CommandType override {
return net::eagle0::shardok::common::BLOW_BRIDGE_COMMAND;
}
[[nodiscard]] auto CanUseClientRoll() const -> bool override { return true; }
[[nodiscard]] auto GetCommandProto() const -> CommandProto override;
[[nodiscard]] auto HasOdds() const -> bool override { return true; }
[[nodiscard]] auto GetOddsPercentile() const -> int32_t override;
[[nodiscard]] int GetActorUnitId() const override { return actorId; }
[[nodiscard]] MapIndex GetTargetRow() const override { return target.row(); }
[[nodiscard]] MapIndex GetTargetColumn() const override { return target.column(); }
};
} // namespace shardok
#endif // EAGLE0_BLOWBRIDGECOMMAND_HPP
@@ -16,11 +16,13 @@ MeteorCancelCommand::MeteorCancelCommand(
const ActionCost& cost,
const SettingsGetter& settingsGetter,
const PlayerId playerId,
const UnitId casterId)
const UnitId casterId,
const bool requiredToEndTurn)
: ShardokCommand(playerId),
cost(cost),
settings(settingsGetter),
casterId(casterId) {}
casterId(casterId),
requiredToEndTurn(requiredToEndTurn) {}
auto MeteorCancelCommand::InternalExecute(
const GameStateW& currentState,
@@ -21,13 +21,15 @@ protected:
const ActionCost cost;
const SettingsGetter settings;
const UnitId casterId;
const bool requiredToEndTurn;
public:
MeteorCancelCommand(
const ActionCost &cost,
const SettingsGetter &settingsGetter,
PlayerId playerId,
UnitId casterId);
UnitId casterId,
bool requiredToEndTurn = true);
~MeteorCancelCommand() override = default;
@@ -36,7 +38,7 @@ public:
}
auto GetCommandProto() const -> CommandProto override;
[[nodiscard]] auto IsRequiredToEndTurn() const -> bool override { return true; }
[[nodiscard]] auto IsRequiredToEndTurn() const -> bool override { return requiredToEndTurn; }
[[nodiscard]] auto CanDoWithLowMorale() const -> bool override { return true; }
[[nodiscard]] auto CanDoWithLowVigor() const -> bool override { return true; }
@@ -41,13 +41,15 @@ MeteorTargetCommand::MeteorTargetCommand(
const PlayerId playerId,
const UnitId casterId,
Coords target,
const int minimumKnowledge)
const int minimumKnowledge,
const bool requiredToEndTurn)
: ShardokCommand(playerId),
cost(cost),
settings(settingsGetter),
casterId(casterId),
target(std::move(target)),
minimumKnowledge(minimumKnowledge) {}
minimumKnowledge(minimumKnowledge),
requiredToEndTurn(requiredToEndTurn) {}
auto MeteorTargetCommand::GetCommandProto() const -> CommandProto {
CommandProto proto{};
@@ -24,6 +24,7 @@ protected:
const UnitId casterId;
const Coords target;
const int minimumKnowledge;
const bool requiredToEndTurn;
public:
MeteorTargetCommand(
@@ -32,7 +33,8 @@ public:
PlayerId playerId,
UnitId casterId,
Coords target,
int minimumKnowledge);
int minimumKnowledge,
bool requiredToEndTurn = true);
~MeteorTargetCommand() override = default;
@@ -41,7 +43,7 @@ public:
}
auto GetCommandProto() const -> CommandProto override;
[[nodiscard]] auto IsRequiredToEndTurn() const -> bool override { return true; }
[[nodiscard]] auto IsRequiredToEndTurn() const -> bool override { return requiredToEndTurn; }
[[nodiscard]] auto CanDoWithLowMorale() const -> bool override { return true; }
[[nodiscard]] auto CanDoWithLowVigor() const -> bool override { return true; }
@@ -62,6 +62,7 @@ auto FromPlayerInfoProto(flatbuffers::FlatBufferBuilder& fbb, const PlayerInfoPr
pib.add_allies(alliesOffset);
pib.add_victory_conditions(vcOffset);
pib.add_starting_food(piProto.starting_food());
pib.add_cannot_become_outlaw(piProto.cannot_become_outlaw());
return pib.Finish();
}
@@ -239,14 +240,12 @@ auto ToProto(const GameStatus* fbStatus) -> GameStatusProto {
fbStatus->end_game_condition()->victory_details()));
break;
case net::eagle0::shardok::storage::fb::VictoryType_ALLY_VICTORY:
protoStatus.mutable_end_game_condition()->set_ally_victory(
protoStatus.mutable_end_game_condition()->set_victory(
static_cast<net::eagle0::shardok::common::VictoryCondition>(
fbStatus->end_game_condition()->victory_details()));
break;
case net::eagle0::shardok::storage::fb::VictoryType_DRAW:
protoStatus.mutable_end_game_condition()->set_draw(
static_cast<net::eagle0::shardok::common::DrawType>(
fbStatus->end_game_condition()->draw_details()));
throw InvalidProtoException("Draw victory type is no longer supported");
break;
case net::eagle0::shardok::storage::fb::VictoryType_LOSS:
protoStatus.mutable_end_game_condition()->set_loss(
@@ -278,6 +277,7 @@ auto ToPlayerInfoProto(const PlayerInfo* fbPI) -> PlayerInfoProto {
}
piProto.set_is_ai(fbPI->is_ai());
piProto.set_eagle_faction_id(fbPI->eagle_faction_id());
piProto.set_cannot_become_outlaw(fbPI->cannot_become_outlaw());
return piProto;
}
@@ -260,7 +260,13 @@ auto TutorialBattleController::ExecuteReinforcementsAction(
}
}
// Place PENDING_REINFORCEMENT units directly at available positions as NORMAL_UNIT
// Build set of eagle hero IDs for this specific reinforcement event
std::set<int32_t> expectedHeroIds;
for (const auto& commonUnit : reinforcementsAction.units()) {
if (commonUnit.has_hero()) { expectedHeroIds.insert(commonUnit.hero().eagle_hero_id()); }
}
// Place only the PENDING_REINFORCEMENT units that belong to this event
ActionResult reinforcementsResult{};
reinforcementsResult.set_type(ActionType::TUTORIAL_REINFORCEMENTS_ARRIVED);
reinforcementsResult.mutable_player()->set_value(playerId);
@@ -268,6 +274,10 @@ auto TutorialBattleController::ExecuteReinforcementsAction(
size_t posIndex = 0;
for (const auto* unit : *state->units()) {
if (unit->status() != UnitStatusFB::UnitStatus_PENDING_REINFORCEMENT) { continue; }
if (!unit->has_attached_hero() ||
!expectedHeroIds.contains(unit->attached_hero().eagle_hero_id())) {
continue;
}
if (posIndex >= availablePositions.size()) { break; }
net::eagle0::shardok::storage::fb::Unit changedUnit = *unit;
@@ -188,22 +188,6 @@ auto GameStateGuesser::GuessedState(
gameStateView.status().end_game_condition().victory()));
break;
case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kAllyVictory:
endGameCondition.mutate_victory_type(
net::eagle0::shardok::storage::fb::VictoryType_ALLY_VICTORY);
endGameCondition.mutate_victory_details(
static_cast<net::eagle0::shardok::storage::fb::VictoryCondition>(
gameStateView.status().end_game_condition().ally_victory()));
break;
case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kDraw:
endGameCondition.mutate_victory_type(
net::eagle0::shardok::storage::fb::VictoryType_DRAW);
endGameCondition.mutate_draw_details(
static_cast<net::eagle0::shardok::storage::fb::DrawType>(
gameStateView.status().end_game_condition().draw()));
break;
case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kLoss:
endGameCondition.mutate_victory_type(
net::eagle0::shardok::storage::fb::VictoryType_LOSS);
@@ -47,6 +47,25 @@ cc_library(
],
)
cc_library(
name = "game_over_response_populator",
srcs = ["GameOverResponsePopulator.cpp"],
hdrs = ["GameOverResponsePopulator.hpp"],
copts = COPTS,
visibility = ["//visibility:public"],
deps = [
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
"//src/main/cpp/net/eagle0/shardok/library:shardok_exception",
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
"//src/main/protobuf/net/eagle0/common:common_unit_cc_proto",
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
"//src/main/protobuf/net/eagle0/common:victory_condition_cc_proto",
"//src/main/protobuf/net/eagle0/shardok/common:game_status_cc_proto",
"//src/main/protobuf/net/eagle0/shardok/common:player_info_cc_proto",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
],
)
cc_library(
name = "eagle_interface_grpc_server",
srcs = ["EagleInterfaceGrpcServer.cpp"],
@@ -54,6 +73,7 @@ cc_library(
copts = COPTS,
visibility = ["//src/main/cpp/net/eagle0/shardok:__subpackages__"],
deps = [
":game_over_response_populator",
":games_manager",
":token_auth",
"//src/main/cpp/net/eagle0/common:unit_conversions",
@@ -19,6 +19,7 @@
#include "src/main/cpp/net/eagle0/common/UnitConversions.hpp"
#include "src/main/cpp/net/eagle0/shardok/controller/ShardokGameController.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/server/GameOverResponsePopulator.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
#include "src/main/protobuf/net/eagle0/common/victory_condition.pb.h"
@@ -45,6 +46,16 @@ public:
auto EagleInterfaceImpl::ControllerForGame(const GameId &gameId, const GameSetupInfo &setupInfo)
-> std::shared_ptr<ShardokGameController> {
auto controller = gamesManager->GetController(gameId);
// If game exists but Eagle is requesting a fresh start with no prior history
// (happens after admin rewind), replace the old game
if (controller != nullptr && setupInfo.has_new_game_request() &&
setupInfo.known_result_count() == 0) {
printf("Replacing existing game %s with fresh game (rewind detected)\n", gameId.c_str());
gamesManager->RemoveController(gameId);
controller = nullptr;
}
if (controller == nullptr) {
if (setupInfo.current_game_state().empty()) {
StartGame(setupInfo.new_game_request());
@@ -63,22 +74,6 @@ auto EagleInterfaceImpl::ControllerForGame(const GameId &gameId, const GameSetup
return controller;
}
static auto PopulateGameOverResponse(
const string &gameId,
const GameStatus &gameStatus,
const vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
const vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool;
static auto IncludeUnitProtoInReturn(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool;
static auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
static auto FromInternalStatus(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
-> net::eagle0::common::UnitStatus;
const std::string &kShardokGameRequestExtension = *(new string(".e0gr"));
EagleInterfaceImpl::EagleInterfaceImpl(
@@ -143,7 +138,8 @@ auto ConvertPlayerInfo(
pi.food(),
victoryConditions,
alliedPids,
shardokUnits);
shardokUnits,
pi.cannot_become_outlaw());
}
void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
@@ -175,7 +171,21 @@ void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
playersInfo.push_back(ConvertPlayerInfo(request.user_infos(), (PlayerId)i));
}
// FIXME: validate sanity of the victory conditions
// Validate that exactly one player has WIN_AFTER_MAX_ROUNDS
int winAfterMaxRoundsCount = 0;
for (const auto &psi : playersInfo) {
for (const auto &vc : psi.playerInfo.victory_conditions()) {
if (vc == net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS) {
winAfterMaxRoundsCount++;
}
}
}
if (winAfterMaxRoundsCount != 1) {
throw NewGameException(
StatusCode::INVALID_ARGUMENT,
"Exactly one player must have WIN_AFTER_MAX_ROUNDS victory condition, found " +
std::to_string(winAfterMaxRoundsCount));
}
// Grab the nonempty gameId
const string &gameId = request.game_id();
@@ -351,177 +361,6 @@ auto EagleInterfaceImpl::GetHexMapNames(
return Status::OK;
}
auto PopulateGameOverResponse(
const string &gameId,
const GameStatus &gameStatus,
const vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
const vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool {
const auto &endCondition = gameStatus.end_game_condition();
int32_t winningFactionId = -1;
for (const auto &rpi : resolvedPlayerInfos) {
// for (int shardokPid = 0; shardokPid < request.user_infos_size(); shardokPid++) {
auto shardokPid = rpi.player_id();
const int32_t eagleFactionId = rpi.eagle_faction_id();
// const net::eagle0::common::PlayerSetupInfo &playerSetupInfo =
// request.user_infos(shardokPid);
auto *resolutionInfo = gameOverResponse->add_user_infos();
// Set the endgame condition (win/lose/draw) appropriately
resolutionInfo->set_eagle_faction_id(eagleFactionId);
if (endCondition.condition_case() == EndGameCondition::kDraw) {
// Everyone gets a draw
resolutionInfo->mutable_end_game_condition()->set_draw(
net::eagle0::common::DrawType(endCondition.draw()));
} else if (std::ranges::contains(gameStatus.winning_shardok_ids(), shardokPid)) {
// The winning player gets the victory set
resolutionInfo->mutable_end_game_condition()->set_victory(
net::eagle0::common::VictoryCondition(endCondition.victory()));
winningFactionId = eagleFactionId;
} else if (std::ranges::any_of(
gameStatus.winning_shardok_ids(),
[&rpi, shardokPid](const PlayerId /*pid*/) {
return std::ranges::any_of(
rpi.allies(),
[shardokPid](const net::eagle0::shardok::common::AlliedPlayer
&ap) {
return ap.player_id() == shardokPid;
});
})) {
// Allies of the winning player get an allied victory set
resolutionInfo->mutable_end_game_condition()->set_ally_victory(
net::eagle0::common::VictoryCondition(endCondition.victory()));
} else {
resolutionInfo->mutable_end_game_condition()->set_loss(
net::eagle0::common::VictoryCondition(endCondition.victory()));
}
// copy in all the resolvedUnits that correspond to this player
for (const auto &internalRu : resolvedUnits) {
const auto unit = (Unit *)internalRu.unit_bytes().data();
if (!unit->has_attached_hero()) continue;
if (!IncludeUnitProtoInReturn(internalRu.status())) continue;
if (unit->player_id() != shardokPid) continue;
auto *ru = resolutionInfo->add_units();
*ru->mutable_unit() = FromUnitFb(*unit);
ru->set_status(FromInternalStatus(internalRu.status()));
}
}
printf("Finishing game %s: ", gameId.c_str());
if (endCondition.condition_case() == EndGameCondition::kDraw) {
printf("It's a draw!\n");
} else {
printf("%d wins\n", winningFactionId);
}
return true;
}
auto IncludeUnitProtoInReturn(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool {
switch (internalStatus) {
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT: return true;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
return false;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
case net::eagle0::shardok::storage::
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
case net::eagle0::shardok::storage::
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
throw ShardokInternalErrorException("Bad unit status on resolved unit");
}
return true;
}
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit {
net::eagle0::common::CommonUnit commonUnit{};
commonUnit.set_eagle_player_id(unit.eagle_player_id());
commonUnit.mutable_hero()->set_eagle_hero_id(unit.attached_hero().eagle_hero_id());
commonUnit.mutable_hero()->set_profession(
(net::eagle0::common::CommonProfession)unit.attached_hero()
.profession_info()
.profession());
commonUnit.mutable_hero()->set_strength(unit.attached_hero().strength());
commonUnit.mutable_hero()->set_strength_xp(unit.attached_hero().strength_xp());
commonUnit.mutable_hero()->set_agility(unit.attached_hero().agility());
commonUnit.mutable_hero()->set_agility_xp(unit.attached_hero().agility_xp());
commonUnit.mutable_hero()->set_constitution(unit.attached_hero().constitution());
commonUnit.mutable_hero()->set_constitution_xp(unit.attached_hero().constitution_xp());
commonUnit.mutable_hero()->set_charisma(unit.attached_hero().charisma());
commonUnit.mutable_hero()->set_charisma_xp(unit.attached_hero().charisma_xp());
commonUnit.mutable_hero()->set_wisdom(unit.attached_hero().wisdom());
commonUnit.mutable_hero()->set_wisdom_xp(unit.attached_hero().wisdom_xp());
commonUnit.mutable_hero()->set_integrity(unit.attached_hero().integrity());
commonUnit.mutable_hero()->set_ambition(unit.attached_hero().ambition());
commonUnit.mutable_hero()->set_gregariousness(unit.attached_hero().gregariousness());
commonUnit.mutable_hero()->set_bravery(unit.attached_hero().bravery());
commonUnit.mutable_hero()->set_vigor(unit.attached_hero().vigor());
commonUnit.mutable_hero()->set_spent_vigor(unit.attached_hero().spent_vigor());
commonUnit.mutable_battalion()->set_eagle_battalion_id(unit.battalion().eagle_battalion_id());
commonUnit.mutable_battalion()->set_size(unit.battalion().size());
commonUnit.mutable_battalion()->set_type(
static_cast<net::eagle0::common::CommonBattalionTypeId>(unit.battalion().type()));
commonUnit.mutable_battalion()->set_armament(unit.battalion().armament());
commonUnit.mutable_battalion()->set_training(unit.battalion().training());
return commonUnit;
}
auto FromInternalStatus(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
-> net::eagle0::common::UnitStatus {
switch (internalStatus) {
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
return net::eagle0::common::UnitStatus::CAPTURED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
return net::eagle0::common::UnitStatus::NORMAL_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
return net::eagle0::common::UnitStatus::FLED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
return net::eagle0::common::UnitStatus::RETREATED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
return net::eagle0::common::UnitStatus::NEVER_ENTERED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT:
return net::eagle0::common::UnitStatus::OUTLAWED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
case net::eagle0::shardok::storage::
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
case net::eagle0::shardok::storage::
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
throw ShardokInternalErrorException("Bad unit status on resolved unit");
}
throw ShardokInternalErrorException("Bad unit status on resolved unit");
}
auto EagleInterfaceImpl::SubscribeToGame(
ServerContext *context,
const GameSubscriptionRequest *request,
@@ -532,7 +371,16 @@ auto EagleInterfaceImpl::SubscribeToGame(
shared_ptr<ShardokGameController> controller;
try {
controller = ControllerForGame(request->game_id(), request->game_setup_info());
} catch (NewGameException &e) { return e.GetStatus(); }
} catch (NewGameException &e) { return e.GetStatus(); } catch (ShardokClientErrorException &e) {
std::cerr << "Bad request from client: " << e.what() << std::endl;
return Status(StatusCode::INVALID_ARGUMENT, e.what());
} catch (ShardokInternalErrorException &e) {
std::cerr << "Internal error: " << e.what() << std::endl;
return Status(StatusCode::INTERNAL, e.what());
} catch (std::exception &e) {
std::cerr << "Unknown exception in SubscribeToGame: " << e.what() << std::endl;
return Status(StatusCode::INTERNAL, e.what());
}
if (!controller) { return Status(StatusCode::NOT_FOUND, "Game not found"); }
@@ -624,6 +472,16 @@ auto EagleInterfaceImpl::SubscribeToGame(
active_ = false;
}
void OnBattleReset() override {
if (!active_) return;
GameStatusResponse response;
response.set_game_id(gameId_);
response.mutable_battle_reset_response();
if (!writer_->Write(response)) { active_ = false; }
}
[[nodiscard]] auto IsActive() const -> bool override {
return active_ && !context_->IsCancelled();
}
@@ -634,13 +492,63 @@ auto EagleInterfaceImpl::SubscribeToGame(
controller->RegisterSubscriber(subscriber);
// Wait for updates and push them until game ends or subscriber disconnects
// Use countAfterInitialResponse to avoid re-sending results already in initial response
bool gameEnded = controller->WaitForUpdatesAndPush(subscriber, countAfterInitialResponse);
// Wait for updates and push them until game ends or subscriber disconnects.
// For tutorial battles, a BATTLE_RESET result means the defender lost and we
// should restart the game from the original request.
int64_t actionCount = countAfterInitialResponse;
WaitResult result = WaitResult::GAME_OVER;
while (true) {
result = controller->WaitForUpdatesAndPush(subscriber, actionCount);
if (result != WaitResult::BATTLE_RESET) break;
printf("SubscribeToGame: Tutorial battle reset, restarting game %s\n",
controller->GetGameId().c_str());
// Save the serialized request before removing the controller
const std::string savedRequest = controller->SerializedRequest();
const GameId gameId = controller->GetGameId();
controller->UnregisterSubscriber(subscriber.get());
gamesManager->RemoveController(gameId);
// Deserialize and restart the game from the original request
NewGameRequest newGameRequest;
newGameRequest.ParseFromString(savedRequest);
StartGame(newGameRequest);
controller = gamesManager->GetController(gameId);
if (!controller) {
printf("SubscribeToGame: Failed to restart game after reset\n");
return Status(StatusCode::INTERNAL, "Failed to restart tutorial battle");
}
// Send initial state from the new game
GameStatusResponse resetInitialResponse;
PopulateGameStatusResponse(controller, 0, &resetInitialResponse);
if (!writer->Write(resetInitialResponse)) break;
// Reset action count based on the new game's state
actionCount =
resetInitialResponse.has_game_update_response()
? resetInitialResponse.game_update_response().total_action_result_count()
: 0;
// Re-register subscriber with the new controller
controller->RegisterSubscriber(subscriber);
}
controller->UnregisterSubscriber(subscriber.get());
if (gameEnded) {
if (controller->HasAIThreadError()) {
std::cerr << "SubscribeToGame: AI thread error: " << controller->GetAIThreadError()
<< std::endl;
return Status(StatusCode::INTERNAL, controller->GetAIThreadError());
}
if (result == WaitResult::GAME_OVER) {
printf("SubscribeToGame: Game ended normally\n");
} else {
printf("SubscribeToGame: Subscriber disconnected\n");
@@ -0,0 +1,176 @@
#include "GameOverResponsePopulator.hpp"
#include <algorithm>
#include <ranges>
#include <string>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokException.hpp"
#include "src/main/protobuf/net/eagle0/common/victory_condition.pb.h"
namespace shardok {
using net::eagle0::common::VictoryCondition;
using std::string;
using std::vector;
auto PopulateGameOverResponse(
const string &gameId,
const net::eagle0::shardok::common::GameStatus &gameStatus,
const vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
const vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool {
const auto &endCondition = gameStatus.end_game_condition();
int32_t winningFactionId = -1;
for (const auto &rpi : resolvedPlayerInfos) {
auto shardokPid = rpi.player_id();
const int32_t eagleFactionId = rpi.eagle_faction_id();
auto *resolutionInfo = gameOverResponse->add_user_infos();
// Set the endgame condition (win/lose) appropriately
resolutionInfo->set_eagle_faction_id(eagleFactionId);
if (std::ranges::contains(gameStatus.winning_shardok_ids(), shardokPid)) {
// The winning player gets the victory set
resolutionInfo->mutable_end_game_condition()->set_victory(
net::eagle0::common::VictoryCondition(endCondition.victory()));
winningFactionId = eagleFactionId;
} else if (std::ranges::any_of(
gameStatus.winning_shardok_ids(),
[&rpi](const PlayerId pid) {
return std::ranges::any_of(
rpi.allies(),
[pid](const net::eagle0::shardok::common::AlliedPlayer &ap) {
return ap.player_id() == pid;
});
})) {
// Allied to a winner — their side won, so they get victory too
resolutionInfo->mutable_end_game_condition()->set_victory(
net::eagle0::common::VictoryCondition(endCondition.victory()));
} else {
resolutionInfo->mutable_end_game_condition()->set_loss(
net::eagle0::common::VictoryCondition(endCondition.victory()));
}
// copy in all the resolvedUnits that correspond to this player
for (const auto &internalRu : resolvedUnits) {
const auto unit = (Unit *)internalRu.unit_bytes().data();
if (!unit->has_attached_hero()) continue;
if (!IncludeUnitProtoInReturn(internalRu.status())) continue;
if (unit->player_id() != shardokPid) continue;
auto *ru = resolutionInfo->add_units();
*ru->mutable_unit() = FromUnitFb(*unit);
ru->set_status(FromInternalStatus(internalRu.status()));
}
}
printf("Finishing game %s: %d wins\n", gameId.c_str(), winningFactionId);
return true;
}
auto IncludeUnitProtoInReturn(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool {
switch (internalStatus) {
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT: return true;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
return false;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
case net::eagle0::shardok::storage::
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
case net::eagle0::shardok::storage::
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
throw ShardokInternalErrorException("Bad unit status on resolved unit");
}
return true;
}
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit {
net::eagle0::common::CommonUnit commonUnit{};
commonUnit.set_eagle_player_id(unit.eagle_player_id());
commonUnit.mutable_hero()->set_eagle_hero_id(unit.attached_hero().eagle_hero_id());
commonUnit.mutable_hero()->set_profession(
(net::eagle0::common::CommonProfession)unit.attached_hero()
.profession_info()
.profession());
commonUnit.mutable_hero()->set_strength(unit.attached_hero().strength());
commonUnit.mutable_hero()->set_strength_xp(unit.attached_hero().strength_xp());
commonUnit.mutable_hero()->set_agility(unit.attached_hero().agility());
commonUnit.mutable_hero()->set_agility_xp(unit.attached_hero().agility_xp());
commonUnit.mutable_hero()->set_constitution(unit.attached_hero().constitution());
commonUnit.mutable_hero()->set_constitution_xp(unit.attached_hero().constitution_xp());
commonUnit.mutable_hero()->set_charisma(unit.attached_hero().charisma());
commonUnit.mutable_hero()->set_charisma_xp(unit.attached_hero().charisma_xp());
commonUnit.mutable_hero()->set_wisdom(unit.attached_hero().wisdom());
commonUnit.mutable_hero()->set_wisdom_xp(unit.attached_hero().wisdom_xp());
commonUnit.mutable_hero()->set_integrity(unit.attached_hero().integrity());
commonUnit.mutable_hero()->set_ambition(unit.attached_hero().ambition());
commonUnit.mutable_hero()->set_gregariousness(unit.attached_hero().gregariousness());
commonUnit.mutable_hero()->set_bravery(unit.attached_hero().bravery());
commonUnit.mutable_hero()->set_vigor(unit.attached_hero().vigor());
commonUnit.mutable_hero()->set_spent_vigor(unit.attached_hero().spent_vigor());
commonUnit.mutable_battalion()->set_eagle_battalion_id(unit.battalion().eagle_battalion_id());
commonUnit.mutable_battalion()->set_size(unit.battalion().size());
commonUnit.mutable_battalion()->set_type(
static_cast<net::eagle0::common::CommonBattalionTypeId>(unit.battalion().type()));
commonUnit.mutable_battalion()->set_armament(unit.battalion().armament());
commonUnit.mutable_battalion()->set_training(unit.battalion().training());
return commonUnit;
}
auto FromInternalStatus(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
-> net::eagle0::common::UnitStatus {
switch (internalStatus) {
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
return net::eagle0::common::UnitStatus::CAPTURED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
return net::eagle0::common::UnitStatus::NORMAL_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
return net::eagle0::common::UnitStatus::FLED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
return net::eagle0::common::UnitStatus::RETREATED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
return net::eagle0::common::UnitStatus::NEVER_ENTERED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT:
return net::eagle0::common::UnitStatus::OUTLAWED_UNIT;
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
case net::eagle0::shardok::storage::
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
case net::eagle0::shardok::storage::
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
throw ShardokInternalErrorException("Bad unit status on resolved unit");
}
throw ShardokInternalErrorException("Bad unit status on resolved unit");
}
} // namespace shardok
@@ -0,0 +1,36 @@
#ifndef GameOverResponsePopulator_hpp
#define GameOverResponsePopulator_hpp
#include <string>
#include <vector>
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
#include "src/main/protobuf/net/eagle0/common/common_unit.pb.h"
#include "src/main/protobuf/net/eagle0/common/shardok_internal_interface.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/common/game_status.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
namespace shardok {
using net::eagle0::shardok::storage::fb::Unit;
auto PopulateGameOverResponse(
const std::string &gameId,
const net::eagle0::shardok::common::GameStatus &gameStatus,
const std::vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
const std::vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool;
auto IncludeUnitProtoInReturn(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool;
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
auto FromInternalStatus(
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
-> net::eagle0::common::UnitStatus;
} // namespace shardok
#endif /* GameOverResponsePopulator_hpp */
@@ -57,6 +57,11 @@ auto ShardokGamesManager::GetController(const GameId &gameId)
return existing->second;
}
void ShardokGamesManager::RemoveController(const GameId &gameId) {
std::unique_lock<std::shared_mutex> lk(runningControllersLock);
runningControllers.erase(gameId);
}
auto ShardokGamesManager::UnlockedCreateFromGameStateBytes(
const byte_vector &gameStateBytes,
int32_t startingHistoryCount,
@@ -198,8 +203,15 @@ auto SetUpController(
}
}
// For tutorial battles, override settings so duels are always accepted
auto effectiveSettings = gameSettings;
if (tutorialConfig.enabled()) {
effectiveSettings = std::make_shared<GameSettings>(*gameSettings);
effectiveSettings->GetSetter().SetInt(kSettingsKeys.duelBaseAcceptOdds, 200);
}
unique_ptr<ShardokEngine> engine = std::make_unique<ShardokEngine>(
gameSettings,
effectiveSettings,
std::move(unplacedUnits),
customMapBytes.empty() ? LoadMap(mapName) : LoadMapFromBytes(customMapBytes),
month,
@@ -47,7 +47,8 @@ public:
const vector<VictoryConditionProto> &victoryConditions,
const vector<PlayerId> &alliedPids,
const int32_t eagleFactionId,
const bool isAi) -> PlayerInfoProto {
const bool isAi,
const bool cannotBecomeOutlaw) -> PlayerInfoProto {
PlayerInfoProto pi;
pi.set_player_id(pid);
pi.set_is_defender(def);
@@ -56,6 +57,7 @@ public:
for (const PlayerId alliedPid : alliedPids) { pi.add_allies()->set_player_id(alliedPid); }
pi.set_eagle_faction_id(eagleFactionId);
pi.set_is_ai(isAi);
pi.set_cannot_become_outlaw(cannotBecomeOutlaw);
return pi;
}
@@ -68,7 +70,8 @@ public:
const int food,
const vector<VictoryConditionProto> &victoryConditions,
const vector<PlayerId> &alliedPids,
vector<Unit> us)
vector<Unit> us,
const bool cannotBecomeOutlaw)
: playerInfo(MakePlayerInfo(
pid,
def,
@@ -76,7 +79,8 @@ public:
victoryConditions,
alliedPids,
eagleFactionId,
isAi)),
isAi,
cannotBecomeOutlaw)),
units(std::move(us)) {}
};
@@ -97,6 +101,8 @@ public:
auto GetController(const GameId &gameId) -> std::shared_ptr<ShardokGameController>;
void RemoveController(const GameId &gameId);
auto UnlockedCreateSpecifiedGame(
const GameId &gameId,
const vector<PlayerInfoWithUnits> &playerSetupInfos,
@@ -41,4 +41,12 @@ sysinfo.txt
mono_crash.*
# Local server data
ServerData/
ServerData/
# Unity FBX import artifacts (auto-generated duplicates of textures/materials)
*.fbm/
*.fbm.meta
Assets/HoneyBadger/Materials/
Assets/HoneyBadger/Materials.meta
Assets/HoneyBadger/M_Honey_Badger.mat
Assets/HoneyBadger/M_Honey_Badger.mat.meta
@@ -13,4 +13,8 @@ MonoBehaviour:
m_Name: AddressableAssetGroupSortSettings
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.AddressableAssetGroupSortSettings
sortOrder:
- b7e3a1c4d5f6928a0b1c2d3e4f5a6b7c
- e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
- f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6
- a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
- 4458439bbfafa478b905417eff3c5741
@@ -61,6 +61,10 @@ MonoBehaviour:
m_overridePlayerVersion: '[UnityEditor.PlayerSettings.bundleVersion]'
m_GroupAssets:
- {fileID: 11400000, guid: e57932396135345fe98cb0c17cfc1d9d, type: 2}
- {fileID: 11400000, guid: aa899846fdf64bddb09e7af6c73d3bfa, type: 2}
- {fileID: 11400000, guid: f8a2b3c4d5e6f7089a1b2c3d4e5f6a7b, type: 2}
- {fileID: 11400000, guid: e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6, type: 2}
- {fileID: 11400000, guid: f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7, type: 2}
m_BuildSettings:
m_LogResourceManagerExceptions: 1
m_BundleBuildPath: Temp/com.unity.addressables/AssetBundles
@@ -100,6 +104,29 @@ MonoBehaviour:
m_LabelTable:
m_LabelNames:
- default
- beast-effects
- hex-castle
- hex-forest
- hex-forest-full-snow
- hex-forest-partial-snow
- hex-hills
- hex-hills-full-snow
- hex-hills-partial-snow
- hex-ice
- hex-mountain
- hex-mountain-snow
- hex-ocean
- hex-plains
- hex-plains-full-snow
- hex-plains-partial-snow
- hex-river
- hex-swamp
- hex-thawing-ice
- hex-winter-castle
- hex-winter-forest
- hex-winter-hills
- hex-winter-mountain
- hex-winter-plains
- music-autumn
- music-battle
- music-spring
@@ -107,6 +134,10 @@ MonoBehaviour:
- music-travel
- music-victory
- music-winter
- sfx-combat
- tactical-bridge-constructed
- tactical-bridge-permanent
- tactical-fire
m_SchemaTemplates: []
m_GroupTemplateObjects:
- {fileID: 11400000, guid: b712027ecfc3d47d1a2e0b33ee77c970, type: 2}
@@ -0,0 +1,311 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
m_Name: Beast Effects
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.AddressableAssetGroup
m_GroupName: Beast Effects
m_GUID: b7e3a1c4d5f6928a0b1c2d3e4f5a6b7c
m_SerializeEntries:
- m_GUID: 03fd458f13534496b0eb92f162f7ce5f
m_Address: Assets/Eagle/Effects/LeopardEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 06da0d48df3fc4cabb095b2bbc803307
m_Address: Assets/Eagle/Effects/SpiderEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0c9aa291ec03459290d22dbbb0cd95ac
m_Address: Assets/Eagle/Effects/WildGoatEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 18aa417cee344c3fbffdcc91e6fd43f5
m_Address: Assets/Eagle/Effects/LionEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 19b67a77bb2da43f9bd72a801f63109f
m_Address: Assets/Eagle/Effects/RatEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1c093deb39df94fbca5faa9f015a2331
m_Address: Assets/Eagle/Effects/BeastsEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1d7aec11b391453ebfe35a8f583402e0
m_Address: Assets/Eagle/Effects/FrogEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 22544e03efcd549b7945b13227eda25b
m_Address: Assets/Eagle/Effects/DemonEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 259d7f63b86154a41850db255b9e7c87
m_Address: Assets/Eagle/Effects/SkeletonEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 33d0f131e30ba466daed422c25ecfd45
m_Address: Assets/Eagle/Effects/ButcherEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3c2a18fce1d449bab74c89612b707c3f
m_Address: Assets/Eagle/Effects/HyenaEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3dea4965da3644dc38efcfadf7247812
m_Address: Assets/Eagle/Effects/OrcEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4be580a24d1d4ce3baff4fe4c8323498
m_Address: Assets/Eagle/Effects/PeasantEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 53e3e2ac7f7334bdeaa3a3608835bb2a
m_Address: Assets/Eagle/Effects/SnakeEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 56b0ba99866443ee8afdc6267320b5b0
m_Address: Assets/Eagle/Effects/StagEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 57c9c29cba614bc1ac6357b3357471c5
m_Address: Assets/Eagle/Effects/ElephantEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5865a698f1e404c8c923b10c218e3d99
m_Address: Assets/Eagle/Effects/BearEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5bc3a25d850c4ee98ce500b8e36e5e72
m_Address: Assets/Eagle/Effects/RhinocerosEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 60dcf531b61b143cfb2e87c519fac2f7
m_Address: Assets/Eagle/Effects/ZombieEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 67b96421193d84127b509e9cbd38dea4
m_Address: Assets/Eagle/Effects/BoarEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6c9c00cb423a5469ab2754670303b578
m_Address: Assets/Eagle/Effects/DragonEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6f1fb8d0b51543dd900afb7b19ca78ce
m_Address: Assets/Eagle/Effects/CrabEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 70a840086c9c4e6e80c6257eebc5a4e0
m_Address: Assets/Eagle/Effects/ArcherEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 79be05674fd8c4592afd392bc933a94a
m_Address: Assets/Eagle/Effects/RaccoonEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7dd589d0a0a04aaba2d7bae09ac2eee9
m_Address: Assets/Eagle/Effects/HippopotamusEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7f3a8b2c1d4e5f6a9b0c1d2e3f4a5b6c
m_Address: Assets/Eagle/Effects/OgreEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7ff85a8d11264353a1d99ecc0c9434a8
m_Address: Assets/Eagle/Effects/WarthogEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8e8f6f335c1243f4adaff7671fdc8f96
m_Address: Assets/Eagle/Effects/RabbitEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8f4f4c81c6f4f43d7a8a7705a2e8c827
m_Address: Assets/Eagle/Effects/HoneyBadgerEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9d4aac80ed1064b1da97b0c2da35375c
m_Address: Assets/Eagle/Effects/WolfEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9e75de33a5fd4739a256803a2401f207
m_Address: Assets/Eagle/Effects/KnightEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a085cb31c5f941a0b556b41d26095606
m_Address: Assets/Eagle/Effects/TasmanianDevilEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a728345b1fa74d63aa663e8e91451280
m_Address: Assets/Eagle/Effects/GorillaEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ac80a968b9a4486f93ba638fc4a5635a
m_Address: Assets/Eagle/Effects/KangarooEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b3910b25697e4e9a9f8151f41365175e
m_Address: Assets/Eagle/Effects/SalamanderEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b5d8f2a4c6e1930785f4b2d7a9e3c6f1
m_Address: Assets/Eagle/Effects/TigerEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: babbd2190f224e23bd4096d7bee215dd
m_Address: Assets/Eagle/Effects/VelociraptorEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c168eb3d59d6147ae90a6b6ce18b8627
m_Address: Assets/Eagle/Effects/ClownEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c994b958199c49d9ac81649fff6c4466
m_Address: Assets/Eagle/Effects/ScorpionEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c41416ab59544c898fe780f2625a45e8
m_Address: Assets/Eagle/Effects/HippogryphEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: cfe71c4bf1ca4d959895c806e89237d0
m_Address: Assets/Eagle/Effects/BlackPantherEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: cfee0ee5fb3247489f134c8b56c9c624
m_Address: Assets/Eagle/Effects/WildPigEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d4e7a1b3c5f8926074e3d1a9b6c2f8e5
m_Address: Assets/Eagle/Effects/GiantEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d7a726f04b1147659c3e98e93efd3d0e
m_Address: Assets/Eagle/Effects/SoldierEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e6e96fdb4b8d45349175545a8ca20620
m_Address: Assets/Eagle/Effects/EmuEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ebb57129016f47f8921dd89ef0af65e7
m_Address: Assets/Eagle/Effects/MilitiaEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ec1077fb62f34407b4457b0eb662e3ed
m_Address: Assets/Eagle/Effects/VampireEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f7868077fb93b49dd876e9ee457b2f79
m_Address: Assets/Eagle/Effects/CrocodileEffect.prefab
m_ReadOnly: 0
m_SerializedLabels:
- beast-effects
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: bffc3d84835754ce69a78a771fc85002, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: 9f4d7a2d75b949ed9b23d8cdce64f5a3, type: 2}
- {fileID: 11400000, guid: 55c4dbefaf9f489eb87477e8be24558f, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: aa899846fdf64bddb09e7af6c73d3bfa
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,731 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
m_Name: Hex Tiles
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.AddressableAssetGroup
m_GroupName: Hex Tiles
m_GUID: e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
m_SerializeEntries:
- m_GUID: 044723fb45720f445bc879c27830312f
m_Address: Assets/Hex Tiles/Winter/MountainSnow/hexMountainSnowCave00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0659693fd618c29459a38fb5f87db2ff
m_Address: Assets/Hex Tiles/Winter/PlainsFullSnow/hexSnowField01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0a727f19f459f2548b66c213b218970b
m_Address: Assets/Hex Tiles/Winter/HillsFullSnow/hexHillsColdSnowCovered02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0c2b586abf4e73f44b7bfb1e12e1a17d
m_Address: Assets/Hex Tiles/Winter/ForestPartialSnow/hexForestPineSnowTransition03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0ca21d4977ad29a4b811f13279d350fb
m_Address: Assets/Hex Tiles/Winter/HillsPartialSnow/hexHillsColdSnowTransitionCave00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0ca718c5d9817db45b4eb366839d46f8
m_Address: Assets/Hex Tiles/Winter/Hills/hexHillsCold03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-hills
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0d554013fdb222b4abfcecec3451a6d1
m_Address: Assets/Hex Tiles/Castle/hexMarshCastleRuins00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0dc38b5f4b39658458a7cc60c66ea7c3
m_Address: Assets/Hex Tiles/Mountain/hexMountainCave00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 126ce036e072c684d91a0d7b6de98d0a
m_Address: Assets/Hex Tiles/Forest/hexForestBroadleafForester00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 142c8b443c69b7d4a85fa09583bf9b8b
m_Address: Assets/Hex Tiles/Winter/Plains/hexDirtCold03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 142f54e386f41f349aee4b43944fef36
m_Address: Assets/Hex Tiles/Plains/hexDirt03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 159f2c9c5e018fe4a8a1ffaaf708e9a1
m_Address: Assets/Hex Tiles/Winter/ForestPartialSnow/hexForestPineSnowTransition01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 15b02911c45836a4b8a8c152ba4efb0a
m_Address: Assets/Hex Tiles/ThawingIce/hexOceanIceBergs03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-thawing-ice
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1b7999dd006d48646acd8dd7091281ab
m_Address: Assets/Hex Tiles/Winter/ForestPartialSnow/hexForestPineSnowTransition02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1dc0982d61478ea48958f5284aca407b
m_Address: Assets/Hex Tiles/Castle/hexDirtCastle00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 20dd0501b96ef474c87fb3961c7384ba
m_Address: Assets/Hex Tiles/Winter/HillsFullSnow/hexHillsColdSnowCovered00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 222aae9b94ca9b748a0dd182a2ec47f3
m_Address: Assets/Hex Tiles/Forest/hexForestBroadleaf00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2357431a2f3d74968841c787076eb4f1
m_Address: Assets/Hex Tiles/Winter/Castle/hexMountainFortress00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 242f9e2d40993184b86a5f6612cdf9ff
m_Address: Assets/Hex Tiles/Plains/hexPlains00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 25227b9be5940bb4f9ad3e364b75c03b
m_Address: Assets/Hex Tiles/Winter/HillsPartialSnow/hexHillsColdSnowTransition00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 287398353e291dc49b41b3a8c9fc68b0
m_Address: Assets/Hex Tiles/Winter/Plains/hexDirtCold00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2dfdf104f9636a1438c05f8d56bebbfc
m_Address: Assets/Hex Tiles/Winter/Hills/hexHillsCold01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-hills
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2ebd9aa864bd1b44b8d69a29e95a7042
m_Address: Assets/Hex Tiles/Plains/hexScrublands03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2edb23c572ce88e46a7ce0761186bb2b
m_Address: Assets/Hex Tiles/Winter/MountainSnow/hexMountainSnow03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 30a58a43a543d96499f8ddec24c3b68a
m_Address: Assets/Hex Tiles/Plains/hexPlains01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 348aaecda7dc730409a47ee97507de4b
m_Address: Assets/Hex Tiles/Plains/hexPlains03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 36e28e9a7cc78af43a888f18418460c8
m_Address: Assets/Hex Tiles/Ice/hexPlainsColdSnowCoveredPond00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-ice
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 394f3b43fd4840d43aefb9d89bcb7f99
m_Address: Assets/Hex Tiles/Winter/ForestFullSnow/hexForestPineSnowCovered02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3a8668307a2df7c4ca4b3297b13d43d7
m_Address: Assets/Hex Tiles/Castle/hexPlainsCastle00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3c09622509d98b8429ce8655a374a69e
m_Address: Assets/Hex Tiles/Plains/hexScrublands00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3db91d7ceab395541a01cf284b246374
m_Address: Assets/Hex Tiles/Swamp/hexMarsh02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-swamp
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3ecb578a1427c814495c76f9f06aacaa
m_Address: Assets/Hex Tiles/Winter/PlainsFullSnow/hexSnowField00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 427c33bd4c21f3d419142c6acf2dc74e
m_Address: Assets/Hex Tiles/Ice/hexPlainsColdSnowCovered00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-ice
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 464c392f8cea6b4418032a2396872c57
m_Address: Assets/Hex Tiles/Plains/hexDirt01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 478f81a4eaff7b94587117c799469761
m_Address: Assets/Hex Tiles/Winter/HillsPartialSnow/hexHillsColdSnowTransition03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 48f7d88a0e1de164084679af70de3921
m_Address: Assets/Hex Tiles/Castle/hexPlainsCastle00_green.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4cf49ef0171fa014c93cd8ee9fa5b070
m_Address: Assets/Hex Tiles/Winter/Forest/hexForestPine02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4e30581d44e8de74b8cef8615a827296
m_Address: Assets/Hex Tiles/River/hexOcean01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-river
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4fd806597af36dc418ff94aba10c90a6
m_Address: Assets/Hex Tiles/Plains/hexDirt00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 507cddba3b6244c42a01691f16437057
m_Address: Assets/Hex Tiles/Ice/hexPlainsColdSnowCovered01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-ice
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 54b244acd42c8014ba7d2bce2878813a
m_Address: Assets/Hex Tiles/Winter/ForestFullSnow/hexForestPineSnowCovered01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 54db42620c434d245ad358deb6074623
m_Address: Assets/Hex Tiles/Winter/HillsFullSnow/hexHillsColdSnowCovered01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 57a883222952fc545b0b20c785dab3eb
m_Address: Assets/Hex Tiles/Castle/hexMountainFortress00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5a25225203fe60f419098ec25f4bd63e
m_Address: Assets/Hex Tiles/Winter/Plains/hexPlainsCold03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5ad37662d488fc14a9c5c854022b9e06
m_Address: Assets/Hex Tiles/Forest/hexForestBroadleaf03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5b435480152a64072aa3e5074400f43b
m_Address: Assets/Hex Tiles/Winter/Mountain/hexMountain00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5def67db41110a24fbb3e5cca9569377
m_Address: Assets/Hex Tiles/Swamp/hexMarsh03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-swamp
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5f2031dee052c4dbebaa2a8009927f41
m_Address: Assets/Hex Tiles/Winter/Castle/hexMountainCastle00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5f80ab8898b7b0743850c4ee3cbe87ff
m_Address: Assets/Hex Tiles/Forest/hexForestBroadleaf02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6788042007d1a8e40868e02293a4d34a
m_Address: Assets/Hex Tiles/Winter/Hills/hexHillsCold00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-hills
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 682be8ec61cc93647a66884775462e27
m_Address: Assets/Hex Tiles/Winter/ForestFullSnow/hexForestPineSnowCovered03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6982d72ce07e4a84f95578ccf94ed73b
m_Address: Assets/Hex Tiles/Winter/MountainSnow/hexMountainSnowCave01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6c614b019a2398643a16e651c5a05e75
m_Address: Assets/Hex Tiles/Winter/ForestFullSnow/hexForestPineSnowCoveredClearing00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6cec2c36e84a1e9439609728d3bf42da
m_Address: Assets/Hex Tiles/Winter/PlainsPartialSnow/hexPlainsColdSnowTransition02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 703d03304b902d2409e05058fb241b97
m_Address: Assets/Hex Tiles/Winter/Plains/hexDirtCold02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 74d0636b98695ee4cbbfdb48f2ed3b21
m_Address: Assets/Hex Tiles/Winter/PlainsPartialSnow/hexPlainsColdSnowTransitionPond00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7b4560f1902d54da1a7eed9dd958ca56
m_Address: Assets/Hex Tiles/Winter/Mountain/hexMountain03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7b67d17dfc98d5848af25c32dd1051cd
m_Address: Assets/Hex Tiles/River/hexOcean03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-river
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7bc0e87bc47dd21468fb04e60af44207
m_Address: Assets/Hex Tiles/Winter/HillsPartialSnow/hexHillsColdSnowTransition01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 802241df539ac3a43b720ca0f9321915
m_Address: Assets/Hex Tiles/Winter/ForestPartialSnow/hexForestPineSnowTransition00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 815b6513bb139da43812570ecdd9b9fe
m_Address: Assets/Hex Tiles/Castle/hexPlainsCastle00_blue.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 83733db5e78b0a8478114b45c4a54f31
m_Address: Assets/Hex Tiles/ThawingIce/hexOceanIceBergs02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-thawing-ice
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 85980b2265ff45344b1513252dd5d6ef
m_Address: Assets/Hex Tiles/Ocean/hexOcean00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-ocean
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8a8dacd9ee5f6b848b27a5129f6f4238
m_Address: Assets/Hex Tiles/Winter/Plains/hexPlainsCold00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8abfc7f22352b6a4d87a7d9763d6c27b
m_Address: Assets/Hex Tiles/Winter/Hills/hexHillsColdCave00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-hills
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8b1abd64669c6ac47a92c3bb04a026c2
m_Address: Assets/Hex Tiles/Winter/Plains/hexPlainsCold02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8be08a2e1863c7f43a0abfb06179c682
m_Address: Assets/Hex Tiles/Castle/hexMountainDwarfFortress00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 912605f6db2a9234bb7feafc3a0655b5
m_Address: Assets/Hex Tiles/Plains/hexDirt02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 91aab815eba2a704e97aa7ff2be26ef8
m_Address: Assets/Hex Tiles/Winter/PlainsPartialSnow/hexPlainsColdSnowTransition00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 926371c6928788a40a04edc29f911931
m_Address: Assets/Hex Tiles/Hills/hexHills03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 92e9b8d12c2b3cf409874d0c5d862b26
m_Address: Assets/Hex Tiles/Winter/Plains/hexDirtCold01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 930869bc0c618d446a525c196a58129d
m_Address: Assets/Hex Tiles/Mountain/hexMountain01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 93312d4222ad61446be900d97031204b
m_Address: Assets/Hex Tiles/Forest/hexForestBroadleaf01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9537a3e60336d914f889c96773ec204f
m_Address: Assets/Hex Tiles/Winter/ForestFullSnow/hexForestPineSnowCovered00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 96001be02960c3f4d9c77b399fc5cd58
m_Address: Assets/Hex Tiles/Winter/PlainsPartialSnow/hexPlainsColdSnowTransition03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 962993f07e510f74ca65d36de42dcafa
m_Address: Assets/Hex Tiles/Winter/HillsFullSnow/hexHillsColdSnowCoveredCave00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9695be27a28e22b47af841545bf58f95
m_Address: Assets/Hex Tiles/Winter/MountainSnow/hexMountainSnow01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 994e2eeeb8c163c4aa41b134e73d0ea5
m_Address: Assets/Hex Tiles/Swamp/hexMarsh01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-swamp
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 999118f269ad1a94bb8439efd6c11e8f
m_Address: Assets/Hex Tiles/Hills/hexHills02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 99a934cad7542ed41819255942968a1d
m_Address: Assets/Hex Tiles/Hills/hexHills01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9abe58259b12c274385553b94ca3b706
m_Address: Assets/Hex Tiles/Winter/Forest/hexForestPine00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9bb20f0a522c6e44c8b18f7a8e5ccba2
m_Address: Assets/Hex Tiles/Winter/MountainSnow/hexMountainSnowCave02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9c01e0b6f0d489945aaed9fa35a602d8
m_Address: Assets/Hex Tiles/Plains/hexPlains02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9f2e9e51d64083349847cc81d03e1d96
m_Address: Assets/Hex Tiles/Winter/MountainSnow/hexMountainSnow00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a00eb4fdd0412a7479dd32a392bfab29
m_Address: Assets/Hex Tiles/Winter/Plains/hexPlainsCold01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a1a4802555e68594f8f222281cf25910
m_Address: Assets/Hex Tiles/Castle/hexPlainsWalledCity00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a1e0821356939824cbf62c3491463fe2
m_Address: Assets/Hex Tiles/Winter/MountainSnow/hexMountainSnow02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a574bb28797e67646805085db5d5c8b0
m_Address: Assets/Hex Tiles/Mountain/hexMountain02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a83dc6b61e9ec4d1c9394d23392d22ab
m_Address: Assets/Hex Tiles/Winter/Mountain/hexMountainCave00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: aa4da2ce1a6f8e94ab8b03da8ca83ff2
m_Address: Assets/Hex Tiles/Castle/hexDirtWalledCity00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: aa6cf94b55cfc4f7f99c96f0ddfa54a7
m_Address: Assets/Hex Tiles/Winter/Mountain/hexMountain02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ab70bf8144a18754abd362d192e839a6
m_Address: Assets/Hex Tiles/Plains/hexScrublands01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b182e2c192e8ebe44a08ed807c66e42c
m_Address: Assets/Hex Tiles/Winter/HillsPartialSnow/hexHillsColdSnowTransition02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b2a74a4a0a89bc04cb935bf78b9b6af4
m_Address: Assets/Hex Tiles/Plains/hexScrublands02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b6e872dbce137d842a2fdb5d58fc9b91
m_Address: Assets/Hex Tiles/Ice/hexPlainsColdSnowCovered02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-ice
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bbe8b026b8a5b4b8baa9a15a063132bf
m_Address: Assets/Hex Tiles/Winter/Mountain/hexMountain01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: beeeac2b04c45d141930d1ecbab37f33
m_Address: Assets/Hex Tiles/Winter/Forest/hexForestPine03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bf6abd5babf09854d9d2c9f56a17df7f
m_Address: Assets/Hex Tiles/Mountain/hexMountainCave01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c03d7b7abe5f59f4291f0920a780f088
m_Address: Assets/Hex Tiles/ThawingIce/hexOceanIceBergs00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-thawing-ice
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c0cc93cce6b27744e85f7ccc6b3db00a
m_Address: Assets/Hex Tiles/Mountain/hexMountain00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c5eddc2322a2329438f370a635f47223
m_Address: Assets/Hex Tiles/Winter/Hills/hexHillsCold02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-hills
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c71963927a62933459606789f53c0a98
m_Address: Assets/Hex Tiles/Winter/PlainsFullSnow/hexSnowField03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c8f1796d2e32f4830bbff97cc7299584
m_Address: Assets/Hex Tiles/Winter/Mountain/hexMountainCave01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: cd02e9c75e59137488f9c9a9de504361
m_Address: Assets/Hex Tiles/Winter/ForestPartialSnow/hexForestPineSnowTransitionClearing00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d09aa48433c41cf4b99a84d9820e9979
m_Address: Assets/Hex Tiles/Swamp/hexMarsh00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-swamp
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d3e4776064cbc6146878357184615ad0
m_Address: Assets/Hex Tiles/Winter/PlainsPartialSnow/hexPlainsColdSnowTransition01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains-partial-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d80dd964f2ad6d24ea7cd8bf6c8cfdeb
m_Address: Assets/Hex Tiles/Forest/hexForestBroadleafForester01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e3753902ee88aa94d82c2b27d0fd26e0
m_Address: Assets/Hex Tiles/Winter/MountainSnow/hexMountainSnowCave03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e4997a0e2994c6d42bdbf42209172883
m_Address: Assets/Hex Tiles/Winter/HillsFullSnow/hexHillsColdSnowCovered03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e51d401b0ad3bfa458c17e39fa1d0fe2
m_Address: Assets/Hex Tiles/Ocean/hexOcean02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-ocean
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e63b4b157c3cca24db2143b4df4f67b0
m_Address: Assets/Hex Tiles/Winter/Forest/hexForestPineClearing00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e687450362cef744684faaf8d4a7032d
m_Address: Assets/Hex Tiles/ThawingIce/hexOceanIceBergs01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-thawing-ice
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e792d3719cbd10b4e8b4f296beffd5c9
m_Address: Assets/Hex Tiles/Winter/PlainsFullSnow/hexSnowField02.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-plains-full-snow
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e811ff7d176db66489a5e51e09524082
m_Address: Assets/Hex Tiles/Mountain/hexMountain03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-mountain
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ed5516dfaa0865342a3c193cdc3347ae
m_Address: Assets/Hex Tiles/Castle/hexMountainCastle00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-castle
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: eef22687bc5f0fc4b8e4599f29affaad
m_Address: Assets/Hex Tiles/Ice/hexPlainsColdSnowCovered03.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-ice
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fad09dbc7076adf4db4b4ae1aff1139c
m_Address: Assets/Hex Tiles/Winter/Forest/hexForestPine01.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-winter-forest
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ff82e10631e5ed542880eebb5bcfed3a
m_Address: Assets/Hex Tiles/Hills/hexHills00.png
m_ReadOnly: 0
m_SerializedLabels:
- hex-hills
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: bffc3d84835754ce69a78a771fc85002, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4, type: 2}
- {fileID: 11400000, guid: b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f8a2b3c4d5e6f7089a1b2c3d4e5f6a7b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Beast Effects_BundledAssetGroupSchema
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.BundledAssetGroupSchema
m_Group: {fileID: 11400000, guid: aa899846fdf64bddb09e7af6c73d3bfa, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
m_InternalIdNamingMode: 0
m_CacheClearBehavior: 0
m_IncludeInBuild: 1
m_BundledAssetProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
m_StripDownloadOptions: 0
m_ForceUniqueProvider: 0
m_UseAssetBundleCache: 1
m_UseAssetBundleCrc: 1
m_UseAssetBundleCrcForCachedBundles: 1
m_UseUWRForLocalBundles: 0
m_Timeout: 0
m_ChunkedTransfer: 0
m_RedirectLimit: -1
m_RetryCount: 0
m_BuildPath:
m_Id: 2af7f42f684fd4296a199c9e60624f6f
m_LoadPath:
m_Id: 55b13501972b540abb7332a5bad98342
m_BundleMode: 0
m_AssetBundleProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
m_UseDefaultSchemaSettings: 0
m_SelectedPathPairIndex: 0
m_BundleNaming: 0
m_AssetLoadMode: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9f4d7a2d75b949ed9b23d8cdce64f5a3
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Beast Effects_ContentUpdateGroupSchema
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.ContentUpdateGroupSchema
m_Group: {fileID: 11400000, guid: aa899846fdf64bddb09e7af6c73d3bfa, type: 2}
m_StaticContent: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 55c4dbefaf9f489eb87477e8be24558f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Hex Tiles_BundledAssetGroupSchema
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.BundledAssetGroupSchema
m_Group: {fileID: 11400000, guid: f8a2b3c4d5e6f7089a1b2c3d4e5f6a7b, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
m_InternalIdNamingMode: 0
m_CacheClearBehavior: 0
m_IncludeInBuild: 1
m_BundledAssetProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
m_StripDownloadOptions: 0
m_ForceUniqueProvider: 0
m_UseAssetBundleCache: 1
m_UseAssetBundleCrc: 1
m_UseAssetBundleCrcForCachedBundles: 1
m_UseUWRForLocalBundles: 0
m_Timeout: 0
m_ChunkedTransfer: 0
m_RedirectLimit: -1
m_RetryCount: 0
m_BuildPath:
m_Id: 2af7f42f684fd4296a199c9e60624f6f
m_LoadPath:
m_Id: 55b13501972b540abb7332a5bad98342
m_BundleMode: 0
m_AssetBundleProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
m_UseDefaultSchemaSettings: 0
m_SelectedPathPairIndex: 0
m_BundleNaming: 0
m_AssetLoadMode: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Hex Tiles_ContentUpdateGroupSchema
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.ContentUpdateGroupSchema
m_Group: {fileID: 11400000, guid: f8a2b3c4d5e6f7089a1b2c3d4e5f6a7b, type: 2}
m_StaticContent: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ac8d5c261438f4bd68e68c2f37fd4ea7
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Sound Effects_BundledAssetGroupSchema
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.BundledAssetGroupSchema
m_Group: {fileID: 11400000, guid: f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
m_InternalIdNamingMode: 0
m_CacheClearBehavior: 0
m_IncludeInBuild: 1
m_BundledAssetProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
m_StripDownloadOptions: 0
m_ForceUniqueProvider: 0
m_UseAssetBundleCache: 1
m_UseAssetBundleCrc: 1
m_UseAssetBundleCrcForCachedBundles: 1
m_UseUWRForLocalBundles: 0
m_Timeout: 0
m_ChunkedTransfer: 0
m_RedirectLimit: -1
m_RetryCount: 0
m_BuildPath:
m_Id: 2af7f42f684fd4296a199c9e60624f6f
m_LoadPath:
m_Id: 55b13501972b540abb7332a5bad98342
m_BundleMode: 0
m_AssetBundleProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
m_UseDefaultSchemaSettings: 0
m_SelectedPathPairIndex: 0
m_BundleNaming: 0
m_AssetLoadMode: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Sound Effects_ContentUpdateGroupSchema
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.ContentUpdateGroupSchema
m_Group: {fileID: 11400000, guid: f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7, type: 2}
m_StaticContent: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e5d17a21594effb4e9591490b009e7aa, type: 3}
m_Name: Tactical Assets_BundledAssetGroupSchema
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.BundledAssetGroupSchema
m_Group: {fileID: 11400000, guid: e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6, type: 2}
m_InternalBundleIdMode: 1
m_Compression: 1
m_IncludeAddressInCatalog: 1
m_IncludeGUIDInCatalog: 1
m_IncludeLabelsInCatalog: 1
m_InternalIdNamingMode: 0
m_CacheClearBehavior: 0
m_IncludeInBuild: 1
m_BundledAssetProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
m_StripDownloadOptions: 0
m_ForceUniqueProvider: 0
m_UseAssetBundleCache: 1
m_UseAssetBundleCrc: 1
m_UseAssetBundleCrcForCachedBundles: 1
m_UseUWRForLocalBundles: 0
m_Timeout: 0
m_ChunkedTransfer: 0
m_RedirectLimit: -1
m_RetryCount: 0
m_BuildPath:
m_Id: 2af7f42f684fd4296a199c9e60624f6f
m_LoadPath:
m_Id: 55b13501972b540abb7332a5bad98342
m_BundleMode: 0
m_AssetBundleProviderType:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
m_UseDefaultSchemaSettings: 0
m_SelectedPathPairIndex: 0
m_BundleNaming: 0
m_AssetLoadMode: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5834b5087d578d24c926ce20cd31e6d6, type: 3}
m_Name: Tactical Assets_ContentUpdateGroupSchema
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.ContentUpdateGroupSchema
m_Group: {fileID: 11400000, guid: e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6, type: 2}
m_StaticContent: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,467 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
m_Name: Sound Effects
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.AddressableAssetGroup
m_GroupName: Sound Effects
m_GUID: f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6
m_SerializeEntries:
- m_GUID: 0b253d5f05dd344728f60f2d81fb9363
m_Address: Assets/Shardok/soundEffects/archery.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 0deae9bdb045eff4d95fe05a60ea5185
m_Address: Assets/Medieval Combat Sounds/Weapons/Weapon Draw Metal 5.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 187809f233dd149efbaf7d21918e45a3
m_Address: Assets/Shardok/soundEffects/repair_failed.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 207d1c2948a494738ae0cefc69cf8eaf
m_Address: Assets/Shardok/soundEffects/melee.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 23168a0bd1f9949a8bdac2551a72ea78
m_Address: Assets/Shardok/soundEffects/undead_break_control.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 23d505a858948614bb825caca37094c0
m_Address: Assets/Magic Spells Sound Effects LITE/General Spell/Negative Effect 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 24fabc88cd2a549e4847f3e2d0b5e31d
m_Address: Assets/Shardok/soundEffects/fire_start_failure.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 261478c4105a6894eb07080d90ab1170
m_Address: Assets/Magic Spells Sound Effects LITE/General Spell/Negative Effect 14.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2f8d73a5b4a2b45c2ab94ade33b4d252
m_Address: Assets/Shardok/soundEffects/build_bridge.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3059dbcfcd54d4cb39e3ac902c8f187e
m_Address: Assets/Shardok/soundEffects/raise_undead.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 324eca04cae0a4781b5c1bd6a7a5955b
m_Address: Assets/Shardok/soundEffects/lightning.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 356df74eca2df4f4b81f2f0cc7e564cd
m_Address: Assets/Fantasy Interface Sounds/Magic Chimes 03.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 393c5e9cb43f946c3a46ba3af4c8cb15
m_Address: Assets/Shardok/soundEffects/charge.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3a230106bce0c43e590be59b8e13c49a
m_Address: Assets/Shardok/soundEffects/fire_start.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3b9c8d7e6f5a4b2c1d0e9f8a7b6c5d4e
m_Address: Assets/Shardok/Sounds/rain_loop.ogg
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3dc5691a8d3ee4cf2bb9f5f5ca6c74bc
m_Address: Assets/Shardok/soundEffects/fire_spread.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3f7410e4edbe60146942448ebc7c17b9
m_Address: Assets/Medieval Combat Sounds/Swings and Whoosh/Sword Swing 1_1.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4064ef3ae368341488c7f0d431d06397
m_Address: Assets/Shardok/soundEffects/fire_extinguish.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4866a803aaa6d894997ab3e078a4db36
m_Address: Assets/Magic Spells Sound Effects LITE/General Spell/Buff Defense 08.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4c0d9e8f7a6b5c3d2e1f0a9b8c7d6e5f
m_Address: Assets/Shardok/Sounds/blizzard_wind_loop.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 54297a87ade01a14d983f869be713bd4
m_Address: Assets/Fantasy Interface Sounds/Potion and Alchemy 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5aa9e3d34242bee4c840f3ce7790b3c4
m_Address: Assets/Fantasy Interface Sounds/Foley Armour 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 5b9f05a7789c14a7f89df76ce7a875d5
m_Address: Assets/Shardok/soundEffects/runaway_new.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 61f7e6c569a209649b82dd21edc87d20
m_Address: Assets/Fantasy Interface Sounds/Writing 1.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 643eab6675cd7504b95693e88638e107
m_Address: Assets/Medieval Combat Sounds/Magic/Magic Debuffl 02.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 682711ac987ab45f6947be359521c1a6
m_Address: Assets/Shardok/soundEffects/flee_attempt.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 71492767c077742f182f8f3118d40b23
m_Address: Assets/Shardok/soundEffects/repair.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 738aebeadbbdc6c4c86258226634e4ab
m_Address: Assets/Medieval Combat Sounds/Magic/Magic Negative 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7696580094909f54684e881971773723
m_Address: Assets/Magic Spells Sound Effects LITE/Element Scholls/Fire/Fire Spelll 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 77ed6a2cdc66c46a38e348a2f5638d62
m_Address: Assets/Shardok/Sounds/thunder_loud.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7bbdef360a86e4240b0fa165a7feb29f
m_Address: Assets/Shardok/soundEffects/fear attempt.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7f6c7ea917ad64dd6971ff7a2bb0a706
m_Address: Assets/Shardok/soundEffects/meteor.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 80c545af22a74804ad6617be59fef458
m_Address: Assets/Shardok/soundEffects/feast_cheer.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 80148791db78ea24cb4f091ba98c80b1
m_Address: Assets/Magic Spells Sound Effects LITE/Element Scholls/Fire/Fire Spelll 02.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 81a95ab34bee142c9b904132fc181566
m_Address: Assets/Shardok/soundEffects/move.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 820314ba2c927d244a00d1dcf4c27830
m_Address: Assets/Fantasy Interface Sounds/Stone 08.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 844c8c9f821651d438eaeff4e21b1c84
m_Address: Assets/Medieval Combat Sounds/Magic/Magic Buff 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8703f41a6fcc94af6b4e20187a361423
m_Address: Assets/Shardok/soundEffects/burnination.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8756d07bb49104cd0bf32d870dd6f226
m_Address: Assets/Shardok/soundEffects/holy_wave_damage.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9089af59ccc9f3844bcb4a6f1c1fd094
m_Address: Assets/Fantasy Interface Sounds/UI Tight 02.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 965f48a3dbd3f4d4098bfc2699a6b119
m_Address: Assets/Shardok/soundEffects/anybody.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 98df6d9680a1d0948852a7bf91f167d6
m_Address: Assets/Medieval Combat Sounds/Weapons/Metal Weapon Hit Stone 2_1.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9b7f0cbc69cdc95458e379dc0403e7f0
m_Address: Assets/Fantasy Interface Sounds/Locker 1.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9e2192e93722ed043b59edfd6188721a
m_Address: Assets/Magic Spells Sound Effects LITE/Element Scholls/Wind/Wind Spell 1.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9edd53f46d5c05c42931d1c6d8a9afb1
m_Address: Assets/Medieval Combat Sounds/Footsteps/Armored Grass Surface/Heavy Armor Grass Walking 1_01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9f06d48acebbb4785abae075ffff5c0a
m_Address: Assets/Shardok/soundEffects/dismiss_unit.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a1b7bb8017f6149bf8df60af9c17786a
m_Address: Assets/Shardok/soundEffects/jail_door.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a8bff7f82ebf744c988efe621d929d26
m_Address: Assets/Shardok/Sounds/thunder_clap.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: aab3a37b168aa45b1bc0c1915b92b326
m_Address: Assets/Shardok/Sounds/thunder_distant.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ad788f28ae846864fa7e6980c35e21d5
m_Address: Assets/Medieval Combat Sounds/Weapons/Arrow Hit Stone 1_2.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ae854c089c217d841acdbc304a74e5af
m_Address: Assets/Fantasy Interface Sounds/Magic Chimes 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b56606a67754c472b9e327fb538fdf4f
m_Address: Assets/Shardok/soundEffects/battle_shout.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b6c393d275ec3a54896db3987fa3c8df
m_Address: Assets/Medieval Combat Sounds/Weapons/Metal Weapon Hit Metal 1_1.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bf2bc9c55ed0e6240afe76a1ed889857
m_Address: Assets/Fantasy Interface Sounds/Bag 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c43b41f3acf6689449422f3ea9547975
m_Address: Assets/Magic Spells Sound Effects LITE/General Spell/Negative Effect 04.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c4e4a16ec1a025a40917a97f0026bd51
m_Address: Assets/Fantasy Interface Sounds/Whoosh 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: cdb6ada03a86044a1b3fe4078d494dc8
m_Address: Assets/Shardok/soundEffects/boo.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d10f8d9a69796324099077705d5862c8
m_Address: Assets/Medieval Combat Sounds/Swings and Whoosh/Dash Heavy Armor 1_01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d17b12f9ec69d44dab329950970bbb42
m_Address: Assets/Shardok/soundEffects/freeze.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d180d9fab29d3465d83cd5f30cbdd55e
m_Address: Assets/Shardok/soundEffects/raging_fire.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d20ba1e2f250d411cb4a402ce7d8201b
m_Address: Assets/Shardok/soundEffects/braved_water.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d31d7ceb406c544358b013833291e4f6
m_Address: Assets/Shardok/soundEffects/reduce.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d4b6b622ffce24e469f75722f266ddef
m_Address: Assets/Fantasy Interface Sounds/Coins 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: df294057e81c740fd9bee9d9bd6fbfb9
m_Address: Assets/Shardok/soundEffects/undead_grew.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e0d38576140e948b7b006e617fa4b44a
m_Address: Assets/Shardok/soundEffects/holy_wave.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e480e2182aa2b49cbad019fe89d6e9ca
m_Address: Assets/Shardok/soundEffects/mind_control.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e68cd340e1a424f28a7adebe50287232
m_Address: Assets/Shardok/Sounds/thunder_crack.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f321f83128583f84c8964cddb405182d
m_Address: Assets/Magic Spells Sound Effects LITE/Element Scholls/Holy/Holy Spell 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f5a74fde83300445e825e4ab9251d383
m_Address: Assets/Shardok/soundEffects/splash.mp3
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f7f8c4e324017c341b4bc820961c201c
m_Address: Assets/Magic Spells Sound Effects LITE/Element Scholls/Ice/Ice Spelll 06.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f8167a5229da99740a43bbb92bf385e0
m_Address: Assets/Fantasy Interface Sounds/Special Click 01.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f94699714bc34a24583886479fb97e74
m_Address: Assets/Medieval Combat Sounds/Punch and Melee/Body Fall 1.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: fc0ee2e934f6046459bbcbc60219946f
m_Address: Assets/Magic Spells Sound Effects LITE/General Spell/Positive Effect 1.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ff7723b1d7f5ec942801185de588f85b
m_Address: Assets/Magic Spells Sound Effects LITE/General Spell/Positive Effect 6.wav
m_ReadOnly: 0
m_SerializedLabels:
- sfx-combat
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: bffc3d84835754ce69a78a771fc85002, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7, type: 2}
- {fileID: 11400000, guid: b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,83 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
m_Name: Tactical Assets
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.AddressableAssetGroup
m_GroupName: Tactical Assets
m_GUID: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
m_SerializeEntries:
- m_GUID: bb98861c48ad2024ca93498c7dd93a6c
m_Address: Assets/Shardok/LargeFlames.prefab
m_ReadOnly: 0
m_SerializedLabels:
- tactical-fire
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 09a9c87797baf8342ab0a1e60bc2a085
m_Address: Assets/TileableBridgePack/Objects/bridge_04a.fbx
m_ReadOnly: 0
m_SerializedLabels:
- tactical-bridge-permanent
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1eea89f3bf572e444be6e7cdb4248777
m_Address: Assets/TileableBridgePack/Objects/bridge_04b.fbx
m_ReadOnly: 0
m_SerializedLabels:
- tactical-bridge-permanent
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 40e19755cc788bb4989f358091447aa8
m_Address: Assets/TileableBridgePack/Objects/bridge_03_tile_03.fbx
m_ReadOnly: 0
m_SerializedLabels:
- tactical-bridge-permanent
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 709e7203b4defa24d9691bc36006e956
m_Address: Assets/TileableBridgePack/Objects/bridge_04b_tile_03.fbx
m_ReadOnly: 0
m_SerializedLabels:
- tactical-bridge-permanent
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c26cb0935db6115438c445c2f1b543ab
m_Address: Assets/TileableBridgePack/Objects/bridge_03a_02.fbx
m_ReadOnly: 0
m_SerializedLabels:
- tactical-bridge-permanent
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 45c3e31ca93c72d4fbe3864b1736c350
m_Address: Assets/TileableBridgePack/Objects/bridge_01_tile_02.fbx
m_ReadOnly: 0
m_SerializedLabels:
- tactical-bridge-constructed
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 60213f43a88610b4682fffeb36f3b561
m_Address: Assets/TileableBridgePack/Objects/bridge_03b.fbx
m_ReadOnly: 0
m_SerializedLabels:
- tactical-bridge-constructed
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6953ea73d282ba44baa70f8d55e37e51
m_Address: Assets/TileableBridgePack/Objects/bridge_03b_tile_07.fbx
m_ReadOnly: 0
m_SerializedLabels:
- tactical-bridge-constructed
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: bf454b27bde4f2c4780a957359bd0a6b
m_Address: Assets/TileableBridgePack/Objects/bridge_01.fbx
m_ReadOnly: 0
m_SerializedLabels:
- tactical-bridge-constructed
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: bffc3d84835754ce69a78a771fc85002, type: 2}
m_SchemaSet:
m_Schemas:
- {fileID: 11400000, guid: c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6, type: 2}
- {fileID: 11400000, guid: d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7, type: 2}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 60c2152829b7c4d288515e4b679231a1
guid: 636f54dc7165d464bab442be0d7a0bfa
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -1,7 +1,6 @@
fileFormatVersion: 2
guid: 8369793dfc9d04390bbd12391725c2d4
timeCreated: 1513468984
licenseType: Free
guid: 586189e567d921745822af3c2f6f137e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 255595f624665a945a27a9f33c1017ee
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fd0c4895e62130b4ea80272989d1871b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More