Add --keychain flag to all codesign commands to explicitly specify which
keychain to use. This fixes the "ambiguous" error when the same signing
identity exists in multiple keychains (build keychain and login keychain).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Mac build was failing with errSecInternalComponent because the newly
created keychain wasn't added to the search list. Without this step,
codesign cannot find the certificate.
Changes:
- Add keychain to search list with security list-keychains
- Add -T /usr/bin/security to import command for completeness
- Set 1-hour keychain timeout for signing large app bundles
This matches the keychain setup used in the iOS TestFlight workflow.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix fullscreen toggle not working on Windows
Screen.fullScreen alone doesn't reliably toggle fullscreen on Windows.
Use Screen.SetResolution with explicit FullScreenMode instead.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix fullscreen toggle control wiring in Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Heroes who suppress riots via the crack down command now get a backstory
event recording the encounter. The event captures the province, riot size,
casualties, battalion used (if any), and whether suppression succeeded.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add province weather effect overlays for strategic map
Implements animated weather effect overlays (blizzard, flood, drought) on
the Eagle strategic map. Effects are province-specific, dynamically created
as child overlays when weather events are active.
- Add ProvinceWeatherShader with scrolling animation and distortion support
- Add ProvinceWeatherController that dynamically creates overlays per province
- Add placeholder textures for snow, water caustics, and cracked earth
- Add effect materials with configured animation parameters
- Integrate weather updates into MapController's model setter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update weather effect materials with proper settings
- BlizzardEffect: Set scroll direction to move snow downward
- Assign texture tiling for appropriate particle size
- Wire up weatherController reference in Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When HandleAvailableCommands receives commands with a token that matches
a pending command, skip loading the commands entirely. The token is still
set (so TryPendingCommands can match and post the pending command), but
the commands are not loaded since they're stale - we already acted on them.
This is simpler than the flag-based approach:
- No SuppressNextUIUpdate flag
- Direct check in HandleAvailableCommands via callback
- If pending command fails, error handling refreshes state
Changes:
- Add HasPendingCommandWithToken callback to IClientConnectionSubscriber
- Add HasPendingEagleCommandWithToken helper to PersistentClientConnection
- Wire up callback in Subscribe method
- In HandleAvailableCommands, skip loading commands if token matches pending
- Preserve LastPostedToken across reconnects (don't clear in HandleStartingState)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a province has less than 3 months of food remaining after
consumption, generate a notification where the ruling hero warns
about the food shortage in their personality/voice.
- Add LowFoodWarningDetails proto message for notification details
- Add LowFoodWarningMessage proto message for LLM request
- Create LowFoodWarningPromptGenerator for hero-voiced warnings
- Wire up notification generation in PerformFoodConsumptionPhaseAction
- Add C# notification generator for Unity client display
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix infinite reconnect loop when server rejects command with ERROR
When the server returned a command ERROR (e.g., invalid province selection),
the client would dispose the connection but NOT clear the pending command.
On reconnect, it would retry the same invalid command, get ERROR again,
and loop forever with increasing backoff.
The fix: handle ERROR the same way as BAD_TOKEN - remove the pending command
(it was processed/rejected, don't retry) and refresh the game subscription
to get current valid state. Don't disconnect since the connection is fine.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Display command error to user via ErrorHandler
When server rejects a command with ERROR, invoke OnCommandError callback
which displays the error message via Debug.LogError (caught by ErrorHandler).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Change default lightning interval from (1, 4) to (2, 8) seconds,
making flashes occur approximately half as often.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- List available keychains
- List available signing identities
- Show export options plist
- Remove -allowProvisioningUpdates (not needed for manual signing)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The notification generator was accessing victor.Value unconditionally,
causing a NullReferenceException when the game ended without a victor.
This prevented the "Game Over" notification from being displayed.
Now safely handles the no-victor case and uses TryGetValue for the hero
lookup.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The previous logic only checked if a faction leader was ruling a
province. If all leaders were traveling in armies (or in any other
non-ruling state), the game would incorrectly end.
Simpler fix: a faction can play if ANY leader is NOT imprisoned.
This covers all valid states (ruling, traveling, in province, etc.)
without enumerating them.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
For DeadlineExceeded (5 min gRPC timeout) and StreamEndedNormally,
reconnect immediately without backoff delay. These are expected events,
not failures - there's no reason to wait 2+ seconds before reconnecting.
This eliminates the "Retrying in..." status for normal stream refreshes.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Try using full certificate name "Apple Distribution: Daniel Crosby (UWJ88DX8WQ)"
instead of just "Apple Distribution" to see if that resolves the export signing issue.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents analysis of why client notification generators currently
require per-action-result game state diffs, and proposes a solution
to enable diff batching by moving display data to server-side.
Deferred for now as current performance is acceptable.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Archive step now skips signing (CODE_SIGN_IDENTITY="-")
- This avoids "UnityFramework does not support provisioning profiles" error
- Export step handles all signing with proper certificate and profile
- Added signingCertificate to export options
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change UNIVERSALLY_VISIBLE_TYPES from Vector to Set for O(1) contains
- Replace O(n²) Vector concatenation with O(n) ListBuffer in filterForPlayer
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Fixes "xcode-select: error: tool 'xcodebuild' requires Xcode" when
the build machine's active developer directory is set to Command
Line Tools instead of Xcode.app.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The unrequestedTextHandler was being called BEFORE the game was added to
gameControllerInfos. This caused isGameValid(eagleGameId) to return false
for the game being loaded, causing all its LLM requests to be skipped
as "game deleted".
The fix adds the game to gameControllerInfos BEFORE calling the text
handlers, then updates it again after text handling completes.
Also add eagleGameId (in decimal and hex) to LLM request logging.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Logs each unrequested text being processed with its type, ID, and
requestedAfterHistoryCount. Also logs the result of each request
(submitted, bypassed, deferred, or waiting on dependency).
This helps diagnose why certain LLM requests are repeatedly triggering
disk loads for old game states.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
TryRestoreSessionAsync() fires OnLoginSuccess on success, which triggers
ConnectWithOAuth() via the OnOAuthLoginSuccess handler. The calling code
was also calling ConnectWithOAuth() directly when restore returned true,
causing _createConnection() to be called twice in quick succession.
This caused the first connection to be disposed while still connecting,
leading to ObjectDisposedException and slow reconnection recovery.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Skip validation when replaying persisted action results
Create separate appliers:
- validatingApplier: for new results (e.g., new game creation)
- replayApplier: for replaying persisted results from storage
Persisted results were already validated when first applied, so
re-validating them during replay is unnecessary overhead.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add logging to track when stateAfter loads from disk
Logs the requested count, persisted count, and how far back in history
the request is going. This helps diagnose performance issues with LLM
prompt generation that requires loading historical game states.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The previous implementation used Vector with `:+` append in a foldLeft,
which is O(n) per append, giving O(n²) total time for n results.
Changed both formAwrs and formAwrsFromScala to use:
- ListBuffer for O(1) amortized append
- foldLeft with tuple to track (buffer, currentState)
This should significantly improve performance when replaying persisted
game history.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When multiple runners on the same machine try to sign simultaneously,
they were conflicting on the shared keychain names (build.keychain,
ios-build.keychain). This caused errSecInternalComponent errors.
Changes:
- mac_build.yml: Use build-${run_id}.keychain
- ios_testflight.yml: Use ios-build-${run_id}.keychain
- codesign_mac_app.sh: Support KEYCHAIN_NAME env var with fallback
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a command POST fails due to disconnect, the client would get stuck
after reconnecting:
1. PostCommand() sets LastPostedToken before POST succeeds
2. POST fails due to DeadlineExceeded, command kept in pending queue
3. After reconnect, server sends update with same token
4. HandleAvailableCommands() skips update because token == LastPostedToken
5. Client stuck with no commands despite server saying YourTurn
Fix: Instead of skipping when token matches LastPostedToken and we have
no commands, accept the commands. The server will return BAD_TOKEN if
we try to re-post a successfully processed command. But if our POST
failed, we need these commands to proceed.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add Map Editor lighting data and reflection probe assets
- Add missing VassalRisesDetailsNotificationGenerator.cs.meta
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add EAGLE0_BUILD_DIR environment variable to workflows, defaulting to
/tmp/eagle0-${github.run_id}. This allows multiple runners with the
unity-mac label to run builds in parallel without path conflicts.
Changes:
- Workflows set EAGLE0_BUILD_DIR based on run ID
- Build scripts use EAGLE0_BUILD_DIR with fallback to /tmp/eagle0
- Library cache remains shared (beneficial for build speed)
To add a second parallel runner:
1. Set up a new runner with the same 'unity-mac' label
2. GitHub Actions will automatically distribute jobs to available runners
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The cleanup job needs this permission to delete intermediate
xcode-project artifacts. Without it, the gh api DELETE call
fails with HTTP 403.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Update bundle identifier to net.eagle0.eagle (matches provisioning profile)
- Set appleDeveloperTeamID in Unity project settings
- Add CODE_SIGN_IDENTITY="Apple Distribution" to use distribution cert
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Enable PleaseRecruitMe from prisoners
Prisoners can now trigger PleaseRecruitMe, allowing them to request
to join the faction that holds them. The LLM prompt includes context
about their prisoner status, which faction captured them, and when
they were captured.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix BUILD deps and use correct API for prisoner prompt
- Add missing BUILD dependencies for event_for_hero_backstory_trait and
unaffiliated_hero
- Use TextGenerationSuccess instead of non-existent TextGenerationResult.pure
- Use GeneratorUtilities.dateString for date formatting
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Reuse the same credentials already used for Mac notarization instead
of requiring a separate App Store Connect API key.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- New workflow_dispatch-only workflow for iOS TestFlight builds
- build_unity_ios.sh: Builds Unity iOS player (generates Xcode project)
- archive_ios.sh: Archives and exports IPA using xcodebuild
- upload_testflight.sh: Uploads IPA to TestFlight via App Store Connect API
Required secrets:
- IOS_CERTIFICATE: Apple Distribution certificate (.p12, base64)
- IOS_CERTIFICATE_PWD: Certificate password
- IOS_PROVISIONING_PROFILE: App Store provisioning profile (base64)
- APP_STORE_CONNECT_API_KEY: JSON with key_id, issuer_id, and key
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Additional layout improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up ambient volume slider
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Improve rain particle randomization and wind scaling
- Add per-particle speed variation (0.7x to 1.3x) to break up "sheet" effect
- Scale wind offset proportionally to rain speed so angle stays consistent
- Add rainWindMultiplier setting to Inspector for easy tuning
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace the Health link that navigated to a separate page with an
inline status indicator that auto-refreshes every 10 seconds using
htmx. This provides at-a-glance health visibility without requiring
navigation.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Consider alliances when vassals decide to mobilize
Change chosenMobilizeIfAdjacentEnemyCommand to use FactionUtils.hostileNeighbors
instead of ProvinceUtils.adjacentHostiles. The latter treats any province owned
by a different faction as hostile, causing vassals to mobilize (and withhold
supplies) even when bordered only by allied provinces.
Now vassals will correctly send supplies to their faction head when their only
neighbors are allies or have truces, rather than unnecessarily mobilizing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete misleading ProvinceUtils.adjacentHostiles method
This method was named "adjacentHostiles" but actually returned provinces
owned by any different faction, ignoring alliance/truce relationships.
It had only one production usage which was just fixed to use the correct
FactionUtils.hostileNeighbors method instead.
Deleting to avoid future confusion.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add tests for FactionUtils.hostileNeighbors
Test coverage for:
- Returns hostile (no relationship) neighbors
- Excludes allied provinces
- Excludes truce provinces
- Excludes unruled provinces
- Excludes own faction's provinces
- Returns empty for province with no neighbors
- Correctly filters mixed neighbor types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace the last unlicensed sound effect with Negative Effect 04.wav
from the purchased Magic Spells Sound Effects LITE pack.
This completes the asset audit - all required items are now resolved.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Wrap ensureGameLoaded in try-catch to prevent one corrupted/broken game
from blocking all game loading for a user. On failure:
- Log loudly to stderr with CRITICAL prefix and stack trace
- Send exception to Sentry for monitoring
- Return false so other games can continue loading
This ensures players can still access their other games even if one
game's save data is corrupted or fails to deserialize.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add fullscreen toggle, resolution dropdown, and F11 shortcut
- Add fullscreen toggle to settings panel
- Add resolution dropdown (same as connection canvas)
- F11 key toggles fullscreen from anywhere
- Resolution change preserves current fullscreen/windowed mode
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up fullscreen toggle and resolution dropdown in Unity
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ambient volume setting for weather sounds
- Add ambientSlider to SettingsPanelController with static AmbientVolume property
- Weather effects (rain, thunderstorm, blizzard) respect ambient volume
- Volume updates in real-time when slider is adjusted during playback
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add distant thunder sound effect
Source: Freesound #351526 by LittleRainySeasons (CC BY 4.0)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add more thunder sound effects
- thunder_loud.mp3 - mokasza (CC BY 4.0, Freesound #810746)
- thunder_crack.wav - OneSoundToRuleThemAll (CC BY 4.0, Freesound #238796)
- thunder_clap.wav - FreqMan (CC BY 4.0, Freesound #32544)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up thunder sounds and ambient volume slider in Unity
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When all human factions are eliminated or have no provinces ruled by
faction leaders, the game now ends automatically instead of continuing
to run AI commands indefinitely.
Changes:
- Add endGame() method to Engine trait and EngineImpl to create a
GameEnded action result
- Add allHumansDefeated() check in GameController.performAiCommands
- Check both before the AI loop (handles game load case) and after
each batch of AI commands (handles AI destroying human factions)
- Handle edge case where game state has no factions yet (initialization)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add resolution change detection to hex grid
Detects when the mapArea size changes (e.g., resolution change) and
rebuilds the grid with new metrics. Preserves terrain textures while
recreating all UI elements with correct sizes for the new resolution.
- Track mapArea size and detect changes in Update()
- RebuildGrid() destroys and recreates cells with new HexMetrics
- OnGridRebuilt event notifies ShardokGameController to refresh labels
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Unity scene and remove unused turnHistoryButtonText
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds notification generator and dispatcher registration for the VassalRises
notification added in #5642. Displays appropriate messages when a vassal
rises to lead a faction after all leaders are killed or imprisoned.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add sound effect hooks to WeatherEffectAnimator
Audio clips for weather sounds (assign in Inspector):
- rainLoopSound: gentle pitter-patter for normal rain
- thunderstormLoopSound: more intense rain for thunderstorm
- thunderSound: one-shot thunder clap (plays on lightning flash)
- blizzardLoopSound: intense wind sound
Volume controls:
- loopVolume: volume for looping sounds (default 0.5)
- thunderVolume: volume for thunder (default 0.8)
Sound behavior:
- Rain: plays rainLoopSound continuously
- Thunderstorm: plays thunderstormLoopSound + thunderSound on each flash
- Snow: no sound
- Blizzard: plays blizzardLoopSound continuously
- Sun/Clouds: no sound
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Support multiple thunder sounds with random selection
- Change thunderSound field to thunderSounds array
- Each lightning flash picks a random sound from the array
- Allows assigning Lightning Spelll 01-08 for variety
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add rain loop sound effect from Freesound
- rain_loop.ogg by aesqe (CC BY 4.0)
- Source: https://freesound.org/people/aesqe/sounds/37618/
- Rain falling on clay roof tiles, loopable
- Added attribution to docs/ASSET_AUDIT.md
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Document CC BY 4.0 sound effects for weather and runaway
Added to ASSET_AUDIT.md:
- rain_loop.ogg by aesqe (Freesound #37618) - for rain effect
- blizzard_wind_loop.wav by nsstudios (Freesound #651540) - for blizzard
- runaway.mp3 by Yap_Audio_Production (Freesound #218997) - replaces unlicensed file
Note: Files must be downloaded manually from Freesound (requires login).
Only 1 unlicensed sound effect remaining: failure_horn.mp3
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove stray download file
* Add rain and blizzard wind sound effects
- rain_loop.ogg by aesqe (CC BY 4.0, Freesound #37618)
- blizzard_wind_loop.wav by nsstudios (CC BY 4.0, Freesound #651540)
Location: Assets/Shardok/Sounds/
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Replace runaway.mp3 with licensed version
MedievalArmyRunningLoop by Yap_Audio_Production (CC BY 4.0)
Source: https://freesound.org/people/Yap_Audio_Production/sounds/218997/
This resolves the licensing issue for runaway.mp3.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up weather sound effects in scene
- Assign rain_loop.ogg to rainLoopSound
- Assign blizzard_wind_loop.wav to blizzardLoopSound
- Configure thunder sounds array
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add vassal rises to leader when faction has no available leaders
When all faction leaders are killed or imprisoned, the strongest vassal
(by HeroUtils.power) is now promoted to faction leader instead of
destroying the faction. This prevents "zombie factions" that have
provinces and armies but can't act due to imprisoned leaders.
Changes:
- Add VassalRises notification and VassalRisesMessage LLM request types
- Add proto definitions for new types in notification and LLM protos
- Add proto converters for VassalRises types
- Create VassalRisesPromptGenerator for LLM narrative generation
- Modify CheckForFactionChangesAction to detect imprisoned leaders and
promote vassals when no available leaders remain
- Generate appropriate notifications and LLM messages for vassal rises
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Enhance VassalRises prompt with previous leader details
Add previousLeaderIds to VassalRisesMessage to track fallen/imprisoned
leaders. The prompt generator now includes detailed descriptions for
each previous leader including:
- Their name and full backstory via HeroDescriptionGenerator
- Status: KILLED IN ACTION or IMPRISONED in [province] by [faction]
This gives the LLM more context to generate a message that properly
acknowledges and references the fallen/captured leaders.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add weather visual effects to Shardok battles
Adds full-screen weather effects based on the current weather conditions:
- SUN: Subtle brightness overlay
- CLOUDS: Slight dimming overlay
- RAIN: Falling rain particles with wind influence
- THUNDERSTORM: Heavy rain + periodic lightning flashes
- SNOW: Falling snowflakes with drift animation
- BLIZZARD: Heavy snow with strong horizontal wind
The WeatherEffectAnimator creates particles on a dedicated canvas
overlay and updates them continuously. Wind speed from the weather
data influences particle movement.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add procedural sprite generation and pre-made sprite assets
- Add PNG sprites for rain drops and snowflakes in Assets/Shardok/Sprites/
- Add procedural sprite generation as fallback if sprites not assigned
- Update tooltip to indicate sprites are optional
- Clean up generated textures in OnDestroy to prevent memory leaks
The component now works out of the box without requiring any assets
to be manually assigned. Sprites will be generated procedurally if
the inspector fields are left empty.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove procedural sprite generation, fix canvas render mode
- Remove procedural sprite generation code (use pre-made PNGs instead)
- Force canvas to Screen Space - Overlay mode so particles fall
correctly relative to the screen instead of the 3D world
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Weather Canvas and wire up WeatherEffectAnimator in scene
- Add Weather Canvas with Screen Overlay for weather effects
- Wire up rainDropSprite and snowFlakeSprite references
- Connect weatherEffectAnimator to ShardokGameController
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix particle positioning to use screen coordinates
- Anchor particles to bottom-left (0,0) so anchoredPosition is in
screen pixel coordinates
- Use Screen.width/height directly instead of canvas rect size
- Remove unused _canvasRect field
This ensures particles appear correctly on screen regardless of
canvas RectTransform configuration.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix particle positioning using canvas rect size
- Use canvas RectTransform rect.size instead of Screen dimensions
- This properly accounts for CanvasScaler settings
- Anchor particles to center (0.5, 0.5) and position relative to center
- Wrap-around logic uses half-width/height from center
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Double particle count and size for all weather effects
- Rain: 100→200 particles, 2x12→4x24 size
- Thunderstorm: 200→400 particles
- Snow: 80→160 particles, 3-8→6-16 size
- Blizzard: 250→500 particles
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove PregeneratedText proto and unused service handling
Now that clients no longer request pregenerated texts (they're sent via
normal ClientTextStore streaming), remove the unused proto messages:
- PregeneratedTextRequest/Response from eagle.proto
- pregenerated_text.proto file entirely
- Empty case handler in EagleServiceImpl.scala
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove pregenerated_text.proto reference from protos.csproj
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace all icons in Assets/free_icons/ (unknown license) with
equivalents from purchased Asset Store packs and delete the folder:
- blizzard.png → 16_blizzard_nobg.png (4000_Fantasy_Icons)
- rain.png → 12_Magic_rain_nobg.png (4000_Fantasy_Icons)
- thunderstorm.png → 27_Storm_nobg.png (4000_Fantasy_Icons)
- wind.png → 23_Light_blow_nobg.png (4000_Fantasy_Icons)
- thermometer.png → startFire.png (existing licensed asset)
These icons were only used in Map Editor (dev tool).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace the free_icons/snow.png (unknown license) with
16_blizzard_nobg.png from 4000_Fantasy_Icons (purchased Asset Store
pack) in all locations:
- Gameplay.unity (Province Info blizzard indicator)
- Dominion Wide.prefab
- Dominion Two Rows.prefab
- Map Editor.unity
Delete the now-unused snow.png file.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Pregenerated texts are now populated into each game's ClientTextStore
and sent via normal text streaming (see #5630). This removes the
separate PregeneratedTextRequest/Response mechanism.
- Remove PregeneratedText import from EagleServiceImpl
- Replace PregeneratedTextRequest handler with no-op for backward compatibility
- Remove getPregeneratedClientText method from GamesManager
- Remove unused pregenerated_text_scala_proto dependency from BUILD
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update asset audit to reflect current state
- 2 sound effects remaining to replace (failure_horn.mp3, runaway.mp3)
- List the 8 weather icons in free_icons folder
- Reorganize action items into Must Replace / Low Priority / Resolved
- Update recommendation section
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete unused weather icons (cloud.png, sun.png)
These icons were not referenced anywhere in the codebase.
Update asset audit to reflect 6 remaining weather icons.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove client-side pregenerated text mechanism
Pregenerated texts (hero names, backstories) are now sent to clients
via normal ClientTextStore streaming (thanks to #5630), so the separate
pregenerated text request/response mechanism is no longer needed.
- Remove ClientPregeneratedText.cs class and its Unity component
- Remove PregeneratedTextRequest sending from ConnectionHandler
- Remove PregeneratedTextResponse handling from PersistentClientConnection
- Remove ILobbySubscriber.ReceivePregeneratedTextUpdate method
- Update ClientTextProvider.GetTextEntry to not check pregenerated cache
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add resolved hero names to AvailableLeader for lobby display
The previous commit removed client-side pregenerated text storage, but
lobby UI (hero dropdowns for Create/Join game) still needed hero names.
This fix has the server resolve names and send them in AvailableLeader.name,
so the client can display them immediately without text lookups.
- Add name field to AvailableLeader proto message
- Server resolves names from ClientTextStore (running games) or
PregeneratedClientTextStore (new game options)
- HeroDropdownController uses PreResolvedNames when available
- RunningGameItem/WaitingGameItem use leader.Name with fallback
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The river crossing warning is only relevant when assaulting enemy
territory. When marching to a province owned by the same faction,
there's no assault so the river crossing isn't a concern.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When creating or loading a game, copy the relevant pregenerated texts
(hero names, backstories, initial chronicle) from the global store into
the game's ClientTextStore as complete texts.
This ensures:
- Games are self-contained and immune to TSV file changes
- Texts are saved with the game and sent to clients normally
- Old games get the pregenerated texts they need on load
This is step 1 of removing the separate pregenerated text mechanism.
The global store is still used as a lookup source, but texts are now
stored in the regular completeTexts map for each game.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Layout tweaks to game list prefabs and Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix provider icon bug and handle invalid stored accounts
Bug fixes:
1. Provider icon bug: Session transfer was storing "session_transfer" as
the provider instead of using the actual provider (discord, twitch,
etc.) from the response. Now uses response.User.Provider.
2. User not found: When stored account validation fails with "user not
found" (NotFound gRPC status), the invalid account is now removed from
storage instead of just clearing the current selection.
3. NullReferenceException: When _createConnection() fails (no valid token),
_internalConnectEagle() was continuing to use _persistentClientConnection
which was null. Now returns early if connection creation failed.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When suggesting a display name from the OAuth provider's username,
sanitize it to meet our validation requirements:
- Replace spaces with underscores
- Remove invalid characters (only allow alphanumeric + underscore)
- Collapse multiple underscores into one
- Trim leading/trailing underscores
- Truncate to 20 characters max
- Return empty if result is less than 3 characters
This prevents users from seeing a pre-filled invalid name that they
would need to manually fix.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After a user completes the invitation flow (OAuth sign-in, display name
selection), send them a welcome email with:
- Download instructions for Windows and Mac
- Download links for both platforms
- Deep link button to complete sign-in automatically
- Info about which provider/account they used
Also extends session transfer code expiration from 5 minutes to 1 hour
to give users time to install the game before the deep link expires.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When minimizing the game for OAuth browser flow, we were storing
fullscreen mode but not resolution. When restoring, we only set
Screen.fullScreen=true, which uses whatever windowed resolution
Unity had stored (often 1280x720).
Now we:
- Store both fullscreen mode AND resolution (width/height) before exiting
- Use Screen.SetResolution() to restore both together
This affects both WindowFocusManager (OAuth flow) and ConnectionHandler
(new user sign-in flow).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove confirmation dialog for deleting non-pending invitations (already
redeemed/expired/revoked, no need to confirm)
- Reload page after delete actions instead of htmx partial updates (the
search endpoint returns rows with different columns than the accounts page)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update the sign-in tutorial text to:
- Mention the "Complete Sign-In with Eagle0" button on the download page
- Clarify they should use the same account they created
Also exit fullscreen when showing the auth panel to new users (no stored
accounts) so they can see the browser with the sign-in link, then restore
fullscreen after successful login.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: HandleStartingState unconditionally overwrites ChronicleEntries.
During startup, multiple StartingState messages can arrive - a stale one
(e.g., from stateAfter(1) early in the game before chronicles existed)
arriving after a current state would wipe out existing entries.
Fix:
1. Don't overwrite existing chronicle entries with an empty list, since
chronicles are historical records that accumulate (never disappear)
2. Close the chronicle canvas if entries are cleared while visible
(defense-in-depth + correct UX behavior)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
On Windows, when a second instance is launched via deep link, the URL
needs to be passed to the existing instance. Previously we just
activated the window and exited, losing the URL.
Now uses named pipes (the standard .NET IPC mechanism):
- First instance creates a NamedPipeServerStream and listens
- Second instance connects, sends the URL, then exits
- First instance receives the URL and processes it via OnDeepLinkReceived
This is the same pattern used by Electron's requestSingleInstanceLock().
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add unique IDs to filter inputs to prevent hx-include conflicts
- Fix invitation actions to only refresh invitations table, not users
- Use ID-based selectors instead of name-based for HTMX includes
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the app is launched via a deep link (e.g., from the invitation
download page), the deep link can arrive in Awake() before
ConnectionHandler calls SetServerUrls(). This caused the session
transfer to silently fail because _authClient was null.
Fix by queuing the session transfer code and processing it once
SetServerUrls() is called and the auth client is available.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Merges the separate Users and Invitations pages into a unified
two-column layout under a single "Accounts" navigation tab.
- New /accounts route with side-by-side Users and Invitations panels
- Each panel has independent search/filter functionality
- Responsive layout stacks columns on smaller screens
- Keeps existing /users and /invitations routes for HTMX partial updates
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Use gRPC for session transfer instead of HTTP
The session transfer was using an HTTP endpoint on port 8080 which isn't
exposed to clients. Convert to use gRPC on port 40033 like all other auth
methods.
- Add ExchangeSessionTransferCode RPC to auth.proto
- Update AuthClient.cs to call gRPC instead of HTTP
- Add gRPC handler in Go auth service
- Remove HttpClient and SessionTransferResponse class from C#
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add exchangeSessionTransferCode to Scala auth service
The Scala Eagle server also implements the Auth gRPC service and needs
the new method. Add a stub that proxies to the external Go auth service
when available, or returns UNIMPLEMENTED when not configured.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add single-instance enforcement for Windows deep links
On Windows, URL schemes just launch the executable - they don't redirect
to an existing instance like macOS does. This causes deep links to open
a new game window instead of activating the existing one.
Added SingleInstanceEnforcer which:
- Uses a named mutex to detect if another instance is running
- If launched via deep link (eagle0://) and another instance exists:
- Finds the existing window via FindWindow
- Brings it to foreground via SetForegroundWindow
- Exits the duplicate instance
- Normal launches (without deep link) still allow multiple instances
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix single-instance detection: use process name instead of window title
- Use Process.GetProcessesByName instead of FindWindow - more reliable
- Use Environment.Exit(0) instead of Application.Quit() - works early in lifecycle
- Increase sleep to 500ms to ensure window activation completes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After completing OAuth on the web invitation flow, users can now click a
"Complete Sign-In in Eagle0" button to automatically sign in to the game
without going through OAuth again.
Implementation:
- New SessionTransferService generates one-time codes after OAuth
- Codes are 256-bit random, expire in 5 minutes, single-use
- Download page shows deep link button: eagle0://auth/session/{code}
- Unity client handles deep link, exchanges code for tokens via HTTP
- Login screen shows hint for users coming from invitations
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. WebView Close() was calling Destroy() directly from a goroutine,
but this didn't properly terminate the event loop on the main thread.
Now uses Dispatch(Terminate()) to properly exit the event loop.
2. handleInstallerReplacement was using fmt.Println instead of the UI,
so the WebView showed "Initializing..." while the actual logic was
invisible. Now uses ui.UpdateStatus/ShowError/etc for visibility.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds LoadIntoImage alongside LoadIntoRawImage for loading runtime
textures into Unity Image components (which support Preserve Aspect
in layout groups). Factored out common functionality into helpers:
- CreateTexture: creates Texture2D from bytes or placeholder
- CreateSprite: creates Sprite from Texture2D
- CleanupRawImageTexture/CleanupImageSprite: cleanup helpers
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Changed Debug.LogError to Debug.LogWarning for handled OAuth errors.
The SimpleErrorHandler subscribes to Unity log messages and shows
popups for errors, but OAuth timeouts are expected conditions that
are already displayed in the status text.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The auto-redirect with window.close() was closing the page before
users could accept the browser's permission dialog to open the app.
Changed to a button-based approach where the user clicks to return
to the app, giving them control over when the deep link triggers.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Overlay Mesh transform for Shardok map
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Province Info Panel being disabled on dominion toggle
Changed DominionViewToggleClicked to hide only the panel content
instead of disabling the entire controller GameObject.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add iOS Addressables content state and image meta file
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Unity-generated files (csproj, fonts, addressables settings)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Only log timing warnings when operations take more than 500ms,
reducing noise from normal operation while still catching slow
operations worth investigating.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Register eagle0:// URL scheme on Windows for OAuth deep links
The Windows installer now registers the eagle0:// URL scheme in the
registry (HKEY_CURRENT_USER\Software\Classes\eagle0) pointing to the
game executable. This allows OAuth callbacks to redirect back to the
app automatically.
Also updated OAuthManager to check command line arguments for deep
links since Windows passes URL scheme launches as command line args.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add golang.org/x/sys dependency for Windows registry access
The Windows installer uses golang.org/x/sys/windows/registry to register
the eagle0:// URL scheme. This adds the required dependency to go.mod,
go.sum, and MODULE.bazel.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After OAuth authentication completes, the browser now redirects to
eagle0://auth/complete to bring the app back to foreground. This
provides a better UX than asking users to manually close the browser.
- Unity client registers for deep link events and focuses window
- DeepLinkPostProcessor adds URL scheme to Info.plist on macOS/iOS
- All OAuth handlers (Google/Discord, Apple, Steam) redirect via deep link
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change HexMetrics to accept RectTransform instead of Canvas
- Rename mapCanvas to mapArea in HexGrid (RectTransform type)
- Constrain map area to exclude sidebar for proper sizing at
different aspect ratios
This allows the map to be properly sized based on the available
space, accounting for the sidebar at various screen resolutions.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add three-tier layout mode (Wide/Medium/Narrow) for command panels
- Add LayoutMode enum: Wide (>2.0), Medium (>=1.35), Narrow (<1.35)
- EagleGameController exposes CurrentLayoutMode property
- CommandSelector base class receives layout mode from CommandPanelController
- Adds IsNarrowLayout helper for selectors to check narrow mode
In narrow mode (iPad-like 4:3 aspect ratios), selectors can hide
hero/battalion pickers since users can select via the Resident
Heroes and Battalions panels instead.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add heroesColumn and battalionsColumn fields to March/Defend selectors
These GameObject fields allow hiding the hero and battalion selection
columns in narrow layout mode. Wire them up in Unity editor to the
appropriate column GameObjects.
Also fixes DefendCommandSelector import: UnityEditor -> UnityEngine
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Hide hero/battalion columns in narrow layout mode
In SetUpUI(), hide heroesColumn and battalionsColumn when IsNarrowLayout
is true. Users can still select heroes and battalions via the Resident
Heroes and Battalions side panels.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up heroesColumn and battalionsColumn in Unity
Connect the column GameObjects to MarchCommandSelector and
DefendCommandSelector so they can be hidden in narrow layout mode.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add new icon.png for app icon
- Remove old unused image asset
- Update ProjectSettings for splash screen
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add turn mismatch detection with auto-reconnection
Detects when server reports YourTurn but client has no available
commands. After a 3-second grace period (to avoid false positives
during normal update processing), logs an error via ErrorHandler
and triggers ForceReconnect() to recover state.
This addresses intermittent bugs where exceptions or token mismatches
leave the connection status showing "Your Turn" but the UI doesn't
display any turn actions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Read ServerGameStatus from GameUpdate level, add BattleInProgress display
- Move ServerGameStatus reading from ActionResultResponse to GameUpdate
level so status updates work for all update types including Shardok
- Add "Battle in progress" display for the new BattleInProgress status
This fixes the turn mismatch false positives during Shardok battles -
the server now sends BattleInProgress instead of YourTurn during battles.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add new BATTLE_IN_PROGRESS status to ServerGameStatus enum
- Add server_game_status field to GameUpdate so any update type can
include status (not just ActionResultResponse)
- Report BATTLE_IN_PROGRESS when there are outstanding Shardok battles
instead of YOUR_TURN
- Set serverGameStatus on both GameUpdate and ActionResultResponse
for backwards compatibility
This fixes the issue where "Your Turn" was shown during battles,
including observed battles where the player isn't participating.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor widescreen layout to use enable/disable instead of reparenting
Instead of moving UI panels between containers at runtime, use duplicate
panels positioned in Unity and enable/disable the appropriate version
based on aspect ratio.
Changes:
- Add separate narrow/wide versions of FactionsTable, MovingArmiesTable,
and DominionPanel
- ArrangeLayout() now just enables/disables panels instead of reparenting
- Remove unused container GameObjects (factionsAndMovingArmiesRow, etc.)
- Both panel copies are kept in sync with model data
This approach is simpler to maintain and allows layouts to be fully
configured in the Unity editor.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Unity scene with layout adjustments
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Trim map textures to remove excess water border
Crop all map textures (map_bw_labels.png and province textures 1-43.png)
from 4096x2064 to 3786x1834, removing:
- Left: 200px
- Top: 130px
- Right: 110px
- Bottom: 100px
This leaves a small water margin around the landmass edges.
Also update newMapPrefab.prefab to set AnchoredPosition and SizeDelta to 0
instead of previous offset values.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Trim rawGray.bytes to match trimmed map textures
Apply same crop (left 200, top 130, right 110, bottom 100) to the
province hit-test data so pixel lookups remain aligned with the
trimmed map images.
Dimensions: 4096x2064 → 3786x1834
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update texture import settings and RectTransforms for trimmed map
- Set Non-Power of 2 to "None" for map texture to preserve actual
dimensions (3786x1834) instead of scaling to 4096x2048
- Update RectTransforms to remove offsets now that images are trimmed
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After moving ShardokGameController from ShardokCanvas to ShardokContainer,
several button onClick events lost their target references. Re-wire:
- EndTurn button (Back to Eagle / End Turn)
- DisplayActionsButtonClicked (9 action buttons)
- WarningPanelCommitClicked
- WarningPanelCancelClicked
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add .meta files for:
- BuildInfo.cs (build info feature)
- Editor/ folder and its scripts
- TouchAwareTooltip.cs
Also add ServerData/ to .gitignore to prevent local data from being tracked.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Move ShardokGameController from ShardokCanvas to ShardokContainer
When exiting a battle, ShardokGameController calls this.gameObject.SetActive(false).
Previously this deactivated ShardokCanvas, leaving it inactive for the next battle.
Since ShardokCanvas is now inside ShardokContainer, GetComponentInChildren couldn't
find the controller because the canvas was inactive.
Fix: Move ShardokGameController to ShardokContainer so SetActive(false) deactivates
the whole container. Also update code to use GetComponent instead of GetComponentInChildren.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use serialized eagleCommonTextures field instead of GetComponent
After moving ShardokGameController to ShardokContainer, EagleCommonTextures
was also moved. Replace runtime GetComponent calls with a serialized field
reference for better performance and clearer dependencies.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds explicit null checks to identify exactly which reference is null:
- Model
- shardokContainer
- shardokController (from GetComponentInChildren)
This replaces generic NullReferenceException with specific error messages.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add BuildInfo.cs with commit hash, short hash, and build timestamp
- Add BuildInfoGenerator.cs to auto-generate BuildInfo before builds
- Update ErrorHandler to include commit hash in error messages
This helps identify which exact build a production error came from.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The ShardokContainer reference was missing (fileID: 0), causing a
NullReferenceException when clicking "Return to Lobby" in settings.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: SwapModel() returns early when Model is null (line 504),
but goToBattleButton visibility is updated after that early return
(line 626). When StopAll() sets Model to null via SwapModel(), the
button was never hidden.
Fix: Hide the goToBattleButton in the early return path when Model
becomes null.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the underlying GrpcChannel is disposed (e.g., during environment
switch), the PersistentClientConnection now invokes an OnChannelDead
callback instead of futilely attempting to reconnect on the dead channel.
ConnectionHandler sets this callback to recreate the EagleConnection
and PersistentClientConnection, properly recovering from channel disposal.
Fixes ObjectDisposedException spam when channel dies.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adjust the overlay mesh local position to align correctly with the
hex grid on widescreen displays.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>