The tutorial game creation was not calling withPopulatedPregeneratedTexts(),
causing hero names to show as "Hero" instead of their actual names because
the name text IDs couldn't be resolved.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Force callers to explicitly specify gameType when constructing GameState,
making the type requirement more explicit and preventing accidental defaults.
Updates all test files and InMemoryHistory to explicitly pass GameType.Normal.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add GameType enum (NORMAL, TUTORIAL) in common proto and Scala model
- Store gameType in GameState and pass to GameStateView for client
- Add newGameType field to ActionResult for setting game type on creation
- Mark tutorial games as TUTORIAL in TutorialGameCreation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move Ikhaan Tarn and province 31 into Bregos Fyar's King's Loyalists faction
- Add new Vengeance faction with Bridget and 300 light infantry in Kojaria (province 39)
- Fix startingTrusts to target player faction (was targeting self after reorganization)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add TutorialGameCreationTest to verify tutorial game can be created
without exceptions
- Fix TutorialGameCreation to properly set provinceOrders from JSON
config using ProvinceOrderTypeConverter
- Add test visibility for battalion_types resource
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Aligns pinned versions with what the dependency graph was already resolving,
eliminating the --check_direct_dependencies warnings.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Provinces 14 (player) and 31 (enemy) were missing the "orders" field,
causing a validation error: "Ruling player present, but province orders
not set".
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Tutorial button to lobby for launching tutorial games
- Add tutorialButton field to ConnectionHandler for Unity to wire up
- Add CreateTutorialGame() method that creates a game with isTutorial=true
- Modify _internalCreateGame() to accept isTutorial parameter and include
it in the CreateGameRequest proto message
- Wire up tutorial button click handler in SetupLobbyUI()
The Tutorial button will create a single-player game where Eagle sets up
the hardcoded tutorial scenario (defending Onmaa from Ikhaan Tarn's attack).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Hide tutorial button for non-nolen users
Temporary gate while tutorial is in development.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove defensive null checks on Inspector fields in lobby setup
If these fields aren't wired in the editor, we want a
NullReferenceException so the problem is immediately obvious.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updates tutorial_parameters.json to use Vellus Kade (who already exists
in heroes.tsv) as the ruler of province 40, instead of the non-existent
"Spartacus the Younger". Uses battalion names from game_parameters.json.
Also removes swornBrotherNames for Bregos Fyar in tutorial - Vellus Kade
is just a vassal.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Windows build covers shared C#/proto compilation. Mac only needs
to build on PRs when Mac-specific config changes (workflow, build
scripts, Sparkle, code signing, etc.). Mac still builds on every
push to main.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
These scripts managed an rsync-based Library/ cache to /tmp/eagle0/ but
are never called from any workflow. Library/ caching now relies on
clean: false preserving the working directory between self-hosted runs.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Bee's incremental compilation handles content-only .cs modifications
fine. The stale DAG errors that originally motivated unconditional Bee/
deletion are caused by structural changes (files moved/renamed/deleted),
not content edits. This saves significant rebuild time on asset-only
changes like FBX meta edits or texture updates.
The last-built commit SHA is saved after each successful build and
compared on the next run to detect structural C# changes.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Split bridge prefabs into permanent and constructed styles
Pre-existing bridges now use stone/wooden models while engineer-built
bridges use hastily-constructed models, selected at render time via
the existing _bridgeBuilderLocations tracking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Scale up bridge models to better fill hex tiles
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
HeroGenerator.getHero() returns a new generator after consuming a hero
from the pool, but this updated generator was never stored back in
EngineImpl. This caused duplicate heroes to appear across rounds since
the same pool was being used repeatedly.
Changes:
- Add PhaseAdvancementResult to hold both results and optional updated generator
- Add resultsWithGenerator() to PerformUnaffiliatedHeroesAction
- Update RoundPhaseAdvancer to return PhaseAdvancementResult
- Add recursiveTransformWithGenerator() to EngineImpl to store updated generator
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Azure Artifact Signing at ~$10/mo eliminates the SmartScreen
"unknown publisher" warning on Windows builds.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
With the CDN TTL increased to 1 hour, iOS addressables updates need
active cache purging to be visible to clients promptly.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The Unity DeepLinkPostProcessor depends on UNITY_IOS being defined at
script compilation time, which can fail if the editor starts with a
non-iOS build target. Set the key via PlistBuddy in the archive script
as a reliable fallback that doesn't depend on Unity preprocessor state.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Instead of sleeping 60s for CDN cache to expire, actively purge the
DigitalOcean Spaces CDN cache before notifying clients of new builds.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Both jobs now run on a dedicated [self-hosted, macOS, testflight]
runner (halfdan), so the Xcode project stays on disk between jobs.
This removes the tar/upload/download/extract cycle through GitHub's
artifact storage, saving ~5-13 minutes per TestFlight build.
Requires adding the 'testflight' label to the halfdan runner.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Git sparse checkout cone mode only supports directories. Add
sparse-checkout-cone-mode: false to allow the docker-compose.prod.yml
file pattern.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The cleanup job on ubuntu-latest already deletes these artifacts with
if: always(). The deploy job's copy was redundant.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Move sleep 60 + client notify from self-hosted runners to ubuntu-latest
jobs in mac_build and unity_build, freeing expensive runners sooner
- Skip eagle_build and shardok_build on main pushes (redundant with
docker_build and shardok_arm64_build respectively, kept for PRs)
- Add lfs: false to bazel_cache_cleanup (only runs bazel clean)
- Add sparse checkout + lfs: false to docker_build deploy job (only
needs docker-compose.prod.yml, nginx/, and scripts/)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Build-only workflows (bazel_test, eagle_build, shardok_build) cancel
superseded runs for both PRs and main pushes.
Deploy workflows (mac_build, unity_build, ios_addressables,
shardok_arm64) cancel superseded PR builds but queue main pushes to
avoid interrupting in-progress deployments.
docker_build already had concurrency control.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Same change as #6099 for mac/windows: switch to `clean: false` with
manual `git clean -ffd` to preserve Library/ between runs on
self-hosted runners. Eliminates two rsync operations (~4GB each)
per build.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The previous approach used `clean: true` (which runs `git clean -ffdx`,
deleting gitignored files like Library/), then rsync'd ~4GB from /tmp
to restore it, then rsync'd it back after the build.
Since self-hosted runners have persistent working directories, Library/
naturally survives between runs. Switch to `clean: false` with a manual
`git clean -ffd` (without -x) to remove stale untracked files while
preserving gitignored Library/. Bee/ is still cleaned each build to
avoid stale DAG files.
The restore/persist scripts are kept for the iOS workflows.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The deploy job runs on [self-hosted, macOS, unity-mac] which doesn't
have `gh` CLI installed. Replace `gh api` calls with `curl`/`python3`
which are universally available on macOS.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Replace dotnet proto build with Bazel-hermetic C# proto generation
Use rules_proto_grpc_csharp to generate .cs source files via Bazel
instead of requiring a locally-installed dotnet SDK to build protos.dll.
This eliminates the dotnet dependency for CI runners and makes the
proto build fully hermetic.
- Add bazel_dep for rules_proto_grpc_csharp 5.8.0
- Bump grpc 1.74.0 -> 1.74.1 (required by rules_proto_grpc_csharp)
- Add csharp_proto_compile/csharp_grpc_compile targets across 8 proto
BUILD.bazel files (81 .cs files from 78 protos)
- Rewrite build_protos.sh to use bazel build + copy instead of dotnet
- Output goes to Assets/GeneratedProtos/ with package-based subdirs
to avoid filename collisions (e.g. ActionResultView.cs in both
shardok/api/ and eagle/views/)
- Add Assets/GeneratedProtos/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove stale Eagle0Protos DLL references
The old build_protos.sh generated protos.dll via dotnet; the new one
generates .cs source files instead. Remove the tracked .meta and
.deps.json files that reference the no-longer-generated DLL, and
gitignore the directory to prevent re-adding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GitHub Actions kills orphan processes after each job, which kills
the Bazel server and forces a cold reload of the skyframe graph on
every CI run (~15-20s for loading+analysis of 1080 packages / 78k
targets). With a warm server this drops to ~2-3s.
Changes:
- .bazelrc: Set startup --max_idle_secs=0 to prevent idle shutdown
- ci/runners/bazel-keepalive.sh: Pings the Bazel server to keep it alive
- ci/runners/install-bazel-keepalive.sh: One-time setup script that
installs a macOS launchd agent to run the keepalive every 5 minutes
The launchd agent owns the Bazel server process, so GitHub Actions'
orphan cleanup won't kill it. To set up on a new runner:
./ci/runners/install-bazel-keepalive.sh /path/to/actions-runner/_work/eagle0/eagle0
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Both docker_build and shardok_arm64_build workflows were running
'bazel build //ci:eagle_server_push' just to extract crane from
its runfiles. In the ARM64 workflow this was especially costly (~29s)
because it triggered 'discarding analysis cache' due to the platform
flag change from linux_arm64 back to the default.
Install crane directly to ~/.local/bin with a version check, cached
across runs. First run downloads it; subsequent runs reuse the cached
binary.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
od -tx2 reads the 2-byte e_machine field as a 16-bit value in host
byte order (little-endian on macOS ARM64), so the ELF bytes b7 00
are displayed as 00b7, not b700. The check was producing a false
WARNING on every build even though the binary is correct.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Instead of downloading crane fresh every deployment, check if the
expected version already exists and skip the download. Saves ~3-5s
per deploy.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Without an integrity hash, Bazel logs "Fetching without an integrity
hash, result will not be cached" and re-downloads the base image every
build. Pinning the sha256 digest lets Bazel cache the download across
runs, saving ~5-10s.
Includes a comment with the command to update the digest when the base
image needs to be bumped.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Pull all five images (eagle, admin, jfr-sidecar, nginx, certbot)
concurrently, then load the OCI tarballs sequentially. The network
download is the bottleneck, so parallelizing pulls saves ~15-20s.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The deploy job was rebuilding //ci:warmup_tar (~24s) even though
build-all already built it. Upload the warmup binary as a GitHub
Actions artifact and download it in the deploy job instead.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Push all three images (eagle, admin, jfr-sidecar) concurrently instead
of sequentially. Each push takes ~15-20s, so this should reduce the
push step from ~58s to ~20s.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add logging for rubber-band hero spawns
Logs when a hero is spawned via the rubber-banding mechanic, including:
- Hero ID
- Target province name
- Faction name
- Spawn chance percentage and roll
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add subtle backstory hint for rubber-band spawned heroes
Appends "seeking a worthy cause" to personalityWords for heroes spawned
via the rubber-banding mechanism, providing a subtle narrative hint
without an explicit notification.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The post-processor returned early if the eagle0:// URL scheme was
already in the plist, skipping the ITSAppUsesNonExemptEncryption write.
Move the encryption key set before the URL scheme check so it's always
written.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The client received the CreateGameResponse with a failure result but
silently ignored it. Now it calls Debug.LogError which triggers the
existing ErrorHandler popup.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Two changes to reduce time spent holding global locks:
1. GamesManager.ensureGameLoadedUnsafe: Use per-game locks (ConcurrentHashMap)
so heavy I/O (disk reads, text initialization, hero name fetching) runs
outside GamesManager.this.synchronized. The main lock is only held briefly
to check/update gameControllerInfos.
2. EagleServiceImpl.streamOneUpdate: Move isEagleGame() call outside
lockAndDoWithUserId. isEagleGame calls ensureGameLoaded which can do
heavy disk I/O on cache miss — previously this blocked all other
user operations while a game was being loaded.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When createGame() threw an exception, the Future's failure was silently
discarded (`: Unit`), leaving the client waiting forever for a response.
Now the failure is caught with .recover, logged, reported to Sentry, and
an error response is sent back to the client.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Batch SQLite writes in transactions to eliminate per-statement fsync overhead
The accumulate fold in handleUnrequestedTexts was taking ~595ms because each
withMarkedRequested/withBypassed call triggered an individual autocommitted
SQLite write (~9ms fsync each × 65 operations). Similarly, the heroNameFetch
timing included 38 name writes at ~9ms each.
Adds beginTransaction/commitTransaction/rollbackTransaction to ClientTextStore
trait (no-op defaults for in-memory implementation) with SQLite overrides that
control autocommit. Wraps the four SQL-heavy folds in UnrequestedTextHandler
with transactions so all writes within each fold share a single fsync.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Suppress HeroNameCache log when serving 0 names
takeNames is called on every game tick for every game, and most calls
have no GeneratedHeroName requests. Only log when names were actually served.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Skip hero name and fixed name blocks when there are no requests
takeNames was being called on every game tick even with 0 names needed,
causing noisy logs. Now both the fixed names and hero names blocks short-
circuit when their respective request lists are empty.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add service-wide hero name cache with async replenishment
Pre-fetches hero names into a service-wide cache to eliminate ~612ms of
synchronous network latency from the game creation hot path. The cache
maintains three gender-specific pools using ConcurrentLinkedQueues,
replenished asynchronously via a single-thread executor. Falls back to
synchronous HeroNameFetcher.names() on cache miss.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Increase hero name cache pool size from 20 to 200
Negligible memory cost, reduces chance of cache misses during bursts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When all backstory versions for a hero are unresolved (e.g., bypassed LLM
requests removed from unrequestedTexts), effectiveBackstory's fallback
`getOrElse(versions.last)` picked a version guaranteed to be unresolved,
causing getText to return TextGenerationDependencyUnknown which crashed
sortedLlmRequestsWithPrompts.
Now handles the no-available-version case gracefully: logs a loud warning
with the hero's backstory version states for investigation, and returns
an empty string so dependent LLM requests can proceed with degraded
quality rather than crashing the command pipeline.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Helps identify where time is spent in synchronizedHandlePostResults
during game creation. Logs breakdown when total exceeds 50ms.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
SqliteClientTextStore.createWithData was doing batch inserts without
an explicit transaction, causing SQLite to auto-commit after each
batch operation. This led to ~3 seconds of disk syncs during game
creation.
Wrap all inserts in a single transaction like withAddedCompleteTexts
does, reducing disk syncs from O(n) to O(1).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Move the eagleCanvas.activeSelf check inside the MainQueue callback.
The previous location checked at response arrival time, but the queued
work executes later - by which time the game canvas may have become
active, making the connection canvas inactive and causing
GetComponentInParent to fail.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Changed the dark player colors used when observing battles:
- Defender (idx 0): green instead of black
- Attacker (idx 1): red instead of dark green
- Remaining colors also brightened for better visibility
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
withPopulatedPregeneratedTexts was calling withAddedCompleteText for each
hero name and backstory (~400 calls), with each call doing 3+ SQL operations.
This was a major bottleneck during game creation.
Added withAddedCompleteTexts() that:
- Collects all texts first
- Uses a single transaction
- Batch inserts texts and visibility entries
This reduces ~1200+ individual SQL operations to ~3 batch operations.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
accessibleTo() was loading the entire visibility table from SQLite on
every LLM streaming token, causing severe slowdown during game creation.
Added accessibleToForId(id) with a focused SQL query that only retrieves
visibility for a single text ID.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When creating a new game, a lobby update arrives after the game has
started. At that point, the connection canvas is inactive, causing
GetComponentInParent<EagleCommonTextures>() to fail since it doesn't
traverse inactive GameObjects.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Prevents visible repositioning on first Shardok battle after launch.
The mapArea rect size wasn't settled when SetUp() was called, causing
the grid to rebuild on the next frame when Update() detected the size
change.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instead of including ALL pregenerated texts (~10K) in getCompleteTextsAccessibleTo(),
which would send them all to every client, call withPopulatedPregeneratedTexts()
when creating new games. This copies only the relevant hero names and backstories
for heroes actually in the game into the SQLite database.
This is the same approach used for loaded games, ensuring consistency.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
getCompleteTextsAccessibleTo() only queried the SQLite database, missing
pregenerated texts stored in PregeneratedClientTextStore. For existing
games this worked because withPopulatedPregeneratedTexts() copies them
to SQLite during load. But new games never had that copy step.
Now getCompleteTextsAccessibleTo() includes pregenerated texts, with
database texts taking precedence for any duplicates.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The optional update notification panel was not appearing when clicked
because its parent GameObject was inactive. SetActive(true) on a child
doesn't make it visible if the parent is inactive.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Sadar Rakon is no longer a sworn brother of the King; he is now a
potential warlord (great person) with faction name "The Reclamation"
- Updated Sadar's backstory: rebelled against Tarn's tyranny before
the Eagle arrived, now fights independently but open to reconciling
with the King
- Added Vellus Kade as the King's new second-in-command (sworn brother)
- Vellus is a former mercenary captain who proved his loyalty to the Crown
- Vellus commands province 40 with battalions: Frontier Outriders,
Kade's Company, Crown Vanguard, Ironside Regiment, Borderland Levy,
Provincial Militia
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add professionImage RawImage field
- Remove unused goButton field (whole row is now a button)
- Populate profession image using EagleCommonTextures
- Handle UnknownProfession by returning null (hides image)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add hero rubber-banding for struggling factions
When a faction has few heroes relative to their territory, they now have
a chance (up to 25%) to spawn a low-power, easy-to-recruit hero each
round. This prevents players from getting stuck due to hero scarcity.
Key features:
- Spawn chance scales from 25% (struggling) to 0% (healthy)
- Spawns only in provinces containing faction leaders
- Low-power heroes have stats 1-50, ambition 1-30, no profession
- These heroes are much easier to recruit due to low power/ambition
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Address PR review comments
- Add FactionUtils.provincesWithLeaders() and use it in PerformUnaffiliatedHeroesAction
- Replace isInstanceOf with pattern matching in FactionStruggleUtils
- Keep constitution range at 60-100 (same as normal heroes) so they can march
- Heavily discount (0.25x) heroes in neighboring provinces since player may not
know about them and needs to expand to access them
- Add tests for owned vs neighboring province weighting
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When populating the lobby, gamesFor() previously called ensureGameLoaded()
for every game just to extract leader name/profession/image. This loaded
full game history from disk, blocking the lobby response.
Now save() persists a factionLeaderCache in RunningGame, and gamesFor()
reads leader info directly from the cache. Falls back to full load for
old-format files or unresolved names.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Display provinces that are already known (owned, allied, or reconned)
with strikethrough and grey color in the quest description. The counter
also reflects actual known province count.
Example: "Recon ~~Onmaa~~, Chapellia (1/2)"
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Allied provinces are already visible to the player, so it doesn't make
sense to ask them to recon those. This change:
1. Quest creation: filter out provinces owned by allied factions
2. Fulfillment: count allied provinces as already known
Adds FactionUtils.selfAndAlliedFactionIds for efficient lookup.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The quest was broken because reconning the same target province multiple
times incorrectly incremented the completion counter. This fix changes
the approach:
1. Quest creation: filter out provinces already reconned or owned
2. Fulfillment: check if all targets are in reconnedProvinces or owned
3. Remove counter-increment from PerformReconResolutionAction
This ensures the quest can only be fulfilled by actually reconning each
unique target province (or owning it).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Changed LLM prompts to use "scout" instead of "recon" to avoid
confusion with "reconquest" - the quest is for reconnaissance/scouting
- ReconProvincesQuest and ReconSpecificProvincesQuest now require the
divining faction to have at least one Ranger hero
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add tests for RestProvinceQuest increment behavior
Adds two test cases verifying that RestProvinceQuest progress increments
correctly when resting:
- When the free hero is in the same province being rested
- When the free hero is in a different province but has a quest targeting
the resting province (cross-province scenario)
Both tests pass, confirming the existing logic is correct: the quest
increments based on the quest's targetProvinceId matching the province
where rest happens, regardless of which province the hero is currently in.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* RestProvinceQuest now targets current province only
Changed quest creation to only generate RestProvinceQuest targeting
the province where the free hero is located, rather than creating
quests for all faction provinces. This makes the quest more intuitive -
the hero wants the faction to rest in their province specifically.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
In editor mode, deep links don't work since the editor isn't registered
for the eagle0:// URL scheme. Instead of quitting and waiting for a
deep link relaunch, poll for OAuth completion using the existing
PollForOAuthCompletionAsync infrastructure.
Standalone builds still use the quit-and-relaunch approach.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The global `common --stamp` in .bazelrc caused every action's cache key
to include workspace status data, preventing cache reuse between the
bazel_test, eagle_build, and shardok_build workflows. Only three Go
binary targets actually use stamped x_defs values (admin_server,
authservice, installer), so --stamp is now passed only in the CI
workflow commands that build those targets.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Change starting phase from HostileArmySetup to BattleRequest
- Set up hostileArmies with status Attacking (attack decision pre-made)
- Set up defendingArmy (defense decision pre-made)
- User now sees battle immediately upon starting tutorial
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Thread tutorial_battle_config from NewGameRequest through
EagleInterfaceGrpcServer -> ShardokGamesManager -> SetUpController,
calling engine->SetTutorialBattleConfig() so the TutorialBattleController
is enabled and scripted tutorial events fire during battle.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add UnknownProfession to ProfessionNames and ShortProfessionNames
dictionaries to prevent KeyNotFoundException when displaying games
where leader info isn't fully available (e.g., tutorial games).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add Bregos Fyar faction with provinces 37 and 40 from game_parameters.json
to tutorial setup. Uses Spartacus the Younger as sworn brother ruling
province 40.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add tutorial battle support to CustomBattleRequest
When is_tutorial_battle is set, Eagle constructs a hardcoded tutorial
battle (Sadar Rakon defending against Ikhaan Tarn) with scripted events
via TutorialBattleConfig, ignoring the rest of the request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move tutorial battle from CustomBattleRequest to CreateGameRequest
Tutorial battles are now triggered via CreateGameRequest.is_tutorial
instead of CustomBattleRequest.is_tutorial_battle, providing plumbing
for a full tutorial Eagle game. CreateGameResponse returns the
shardok_game_id for tutorial games.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove shardok_game_id from CreateGameResponse
The client discovers the battle through the normal Eagle game stream,
so CreateGameResponse doesn't need to return it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Route tutorial through real Eagle game instead of standalone CustomBattle
Instead of creating a standalone Shardok battle via CustomBattleManager,
the tutorial now creates a real Eagle game where the player holds Onmaa
(province 14) and Ikhaan Tarn's army attacks from Nikemi (province 31).
The battle flows through Eagle's normal phase system with
TutorialBattleConfig threaded through to Shardok.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Load tutorial heroes from data files instead of hardcoding
Replace inline hero stats, battalions, and faction config in
TutorialGameCreation with a data-driven approach: heroes loaded from
heroes.tsv and tutorial setup defined in a new tutorial_parameters.json
parsed via ScalaPB's JsonFormat. This follows the same pattern used by
regular game creation in NewGameCreation.
- Add Colin, Agamemnon, Tall Edgtheow, Waylaid Julius to heroes.tsv
- Create tutorial_parameters.json with faction/province/battalion config
- Rewrite TutorialGameCreation to use StartGameActionResultUtils helpers
- Expose tutorialGameParameters lazy val in GameParametersUtils
- Remove 3 duplicate heroes from generated_heroes.tsv (name collisions)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add headshot entries for tutorial heroes
The fixed_headshots_present_test requires every hero in heroes.tsv to
have a corresponding entry in headshots.tsv. Add the 4 new tutorial
heroes (agamemnon, colin, tall_edgtheow, waylaid_julius) to the fixed
section.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Beasts already had a dynamic tooltip ("3 wolves"); now blizzard, flood,
drought, epidemic, and festival icons also show a brief label on
hover/long-press. Extracted a SetEventTooltip helper to reduce
duplication.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix overlays not clearing when switching from board unit to reserve unit
When selecting a reserve unit while a board unit was already selected,
the old overlays showing valid moves for the board unit remained visible
alongside the new placement options.
SelectedReserveUnitChangedTo() now:
- Clears _selectedGridIndex (board unit selection)
- Clears overlays before drawing new placement options
- Also clears overlays when deselecting a reserve unit (unit == null)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add swap behavior when clicking reserve unit with board unit selected
In Shardok placement phase, clicking a reserve unit while a board unit is
selected now swaps them: the board unit moves to reserves and the reserve
unit is placed at the board position.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
PerformPlacementAction() was missing the overlay cleanup that
PerformAction() has. After placing or moving a unit during the
placement phase, the flashing borders showing available positions
would remain visible.
Added hexGrid.ClearOverlays() and HandleEnemyStartingPositionOverlays()
calls to match the cleanup pattern in PerformAction().
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Increase weather effect visibility
- Increase epidemic haze alpha from 0.25 to 0.51 for better visibility
- Increase drought texture scale from 2x to 20x
- Increase rain texture scale from 4x to 8x
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix epidemic skulls invisible on white provinces
The particle material was using Additive blending shader, which adds colors
to the background. On white backgrounds, adding any color results in white,
making particles invisible.
Changed shader from "Legacy Shaders/Particles/Additive" to
"Legacy Shaders/Particles/Alpha Blended" which properly replaces background
pixels based on alpha.
Also increased haze emit rate and size for better visibility.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add per-improvement-type lock toggles for ImproveCommandSelector
Replace single lock toggle with four separate lock toggles (one per improvement
type: economy, agriculture, infrastructure, devastation). Only the currently
selected improvement type's lock toggle is enabled; others are grayed out.
When posting a command, only the selected type's lock state is used.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up lock toggles and hide non-selected locks
- Wire up economyLockToggle, agricultureLockToggle, infrastructureLockToggle,
devastationLockToggle references in Unity
- Hide (SetActive false) lock toggles for non-selected improvement types
- Disable lock toggle if improvement type isn't available
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify lock toggle visibility: only show for selected type
Lock toggle is shown only for the currently selected improvement type,
hidden for all others. Availability doesn't affect lock visibility.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix lock toggle wiring and visibility
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Two fixes:
1. Update prefab hazeColor to use 25% alpha (was 8% due to serialized override)
2. Fix ParticleAlphaBlendMaterial to use proper alpha blending instead of
additive - adds _ALPHABLEND_ON keyword and explicit blend modes
(_SrcBlend: 5, _DstBlend: 10)
The additive blending was causing skulls to be invisible on white
backgrounds since adding to white can't make it any darker.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix weather effects not moving with map zoom/pan
- Fix mapContainer references: all weather controllers now use the
correct Content transform instead of Viewport
- BlizzardEffect now updates map bounds every frame in LateUpdate
instead of using stale bounds from spawn time
- Removes one-time SetMapBounds in favor of SetMapContainer which
allows continuous bound recalculation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add zoom compensation for blizzard effect
Maintain consistent screen-space appearance when zooming:
- Counter-scale particle system by 1/zoom for consistent snowflake size
- Expand emission area by zoom to still cover province
- Scale emission rate by zoom² to maintain particle density
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add zoom compensation for flood effect
Apply same fixes as blizzard:
- Update map bounds every frame for correct province masking
- Counter-scale particle systems to maintain screen-space size
- Scale emission rate by zoom² to maintain density
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Bake skull texture directly into material (runtime texture assignment
doesn't work on iOS Metal renderer)
- Remove unused skullTexture field and RefreshSkullTexture method
- Remove fallback shader code - fail loudly if materials aren't assigned
- Increase haze opacity from 8% to 25% for better visibility
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
StartEpidemicCommand was a ProtolessSimpleAction that never checked for
quest fulfillment. Changed it to ProtolessSequentialResultsAction and
added quest matching logic (following the ControlWeatherCommand pattern)
so that StartEpidemicQuest is properly fulfilled when the epidemic
targets the matching province.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a user enters the lobby, we were blocking until all their games
were loaded from disk. With the recent change to load more history
chunks, this could take several seconds for users with multiple games.
Fix: Send lobby response immediately with placeholder info for unloaded
games ("[Loading...]" for leader names), then load games in background.
When loading completes, send an updated lobby response with full info.
Changes:
- Add gamesForWithoutBlocking() that returns immediately with placeholder
info for unloaded games
- Add loadGamesInBackground() that loads games async and calls callback
- Split lockedSendLobbyUpdate into internal method that takes game list
- On EnterLobbyRequest: send immediate response, start background load,
send update when done (if user still in lobby)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When loading a game from disk, we were only loading the last partial
game file (up to 25 results). But reconnection requires the last 100
results (maxInitialResults), causing stateAfter() to load from disk
during reconnection and adding ~450ms latency.
Fix: when loading a game, if recentHistory.length < 100, load additional
chunk files from disk to have enough for reconnection. This loads ~4
chunks instead of 1, adding ~150-300ms to game load but saving ~450ms
on each reconnection.
Also:
- Move minResultsToKeep constant to companion object
- Update log message to show how many chunks were loaded
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
In streamUpdates() when reconnecting to a large game (>100 results),
stateAfter(history.count - maxInitialResults) was being called twice:
1. Inside filteredResultsFrom() for ActionResultFilter
2. Again for GameStateViewFilter to create the stateView
This caused the same partial game to be loaded from disk twice,
adding ~1 second to reconnection time.
Fix: compute starting state once and pass to both consumers via
new filteredResultsFromWithState() method.
Also fixes a minor issue where history.since() was called twice
in filteredResultsFrom - now called once and reused.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Implement platform-specific update behavior
- Mac: Trigger Sparkle to check for updates immediately (shows native UI)
- Windows: Relaunch the application before quitting
- iOS: Open TestFlight URL to update
- Also added iOS platform detection for update notifications
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix TestFlight URL to correct app ID
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When filtering action results for a player, we were calling
filteredGameState twice per result (before + after states). But the
"after" state of result N is the "before" state of result N+1.
Now we cache the filtered view and reuse it, cutting the number of
expensive filteredGameState calls from 2N to N+1.
For 100 results, this reduces filter time from ~1600ms to ~800ms.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add client update notification system
Notify connected clients when a new version is available after CI deploy.
Flow:
1. CI deploys new build and waits 60s for CDN cache
2. CI calls admin server HTTP endpoint with shared secret
3. Admin server calls Eagle via gRPC
4. Eagle broadcasts to all connected lobby users
5. Unity client shows notification with "Restart Now" button
Components:
- Proto: ClientUpdateAvailable message, NotifyClientUpdate RPC
- Eagle: notifyClientUpdate() broadcasts to lobby users
- Admin server: /notify-update HTTP endpoint with secret auth
- Unity: UpdateNotificationManager, Panel, and RequiredModal
- CI: Notify steps in mac_build.yml and unity_build.yml
Note: NOTIFY_SECRET env var must be set on admin server deployment.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add UpdateIndicator for top bar update notification
Instead of a popup panel for optional updates, show a pulsing indicator
in the top bar. Clicking it opens the details panel.
- UpdateIndicator: pulsing green circle button for top bar
- UpdateNotificationManager: stores pending update info, shows indicator
- Panel only opens when user clicks indicator
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up update notification UI in Unity scene
- UpdateIndicator in top bar (pulsing green circle)
- UpdateNotificationPanel for optional update details
- UpdateRequiredModal for blocking required updates
- UpdateNotificationManager with all references connected
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix update notification panel showing and simplify fallbacks
- Remove fallback chain when panel/modal references are null
- Just warn and return instead of cascading to other UI
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Broadcast update notifications to all connected users
Previously notifications only went to lobby users. Now they also go to
users who are in a game. This ensures players don't miss critical updates
while playing.
- Add broadcastToAllGameUsers() to GamesManager
- Update notifyClientUpdate() to send to both lobby and in-game users
- Log shows breakdown of lobby vs in-game notifications
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove Debug.Log messages from update notification UI
Keep only Debug.LogWarning for actual error conditions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
During game join, for each complete text being sent, we were doing
linear searches through streamingTextStatuses (O(n)). With thousands
of texts and statuses, this caused O(n²) behavior.
Added a lazy cached Map keyed by llmIdentifier for O(1) lookups.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When joining a game, the code was calling `accessibleTo` (which loads
the entire visibility table) twice per complete text. For games with
thousands of texts, this caused O(n²) database reads.
Added `getCompleteTextsAccessibleTo(factionId)` method that performs
a single SQL query with EXISTS subqueries to fetch only the texts
accessible to the joining player.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Optimize text store accessors to avoid loading all entries
1. Replace completeTexts.get(id) with getText(id) for individual lookups
- Avoids loading all complete texts into memory just to look up one
2. Add hasIncompleteTextsAccessibleTo(factionId) method
- Default implementation uses existing maps (for ClientTextStoreImpl)
- SQLite implementation uses optimized EXISTS query
- Avoids loading all incomplete texts to check if any exist
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove old text-based client text store migration code
All existing games have been migrated to SQLite, so the migration
code from the old txt file format is no longer needed.
Removes ~136 lines of migration code and two unused dependencies.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1. Replace completeTexts.get(id) with getText(id) for individual lookups
- Avoids loading all complete texts into memory just to look up one
2. Add hasIncompleteTextsAccessibleTo(factionId) method
- Default implementation uses existing maps (for ClientTextStoreImpl)
- SQLite implementation uses optimized EXISTS query
- Avoids loading all incomplete texts to check if any exist
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace file-based ClientTextStore with SQLite implementation
SQLite provides indexed lookups instead of loading/parsing a 5MB+ text file.
This should significantly reduce the ~142ms spent on loadTextStore during
game loading.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add S3 sync for SQLite client text store
- Store dbPath and optional persister reference in SqliteClientTextStore
- Sync database file to cloud storage in saved() method
- Download from cloud storage on load if local file doesn't exist
- Pass persister to createWithData for new game creation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Sync SQLite to S3 immediately after migration
Prevents data loss if server restarts before saved() is called after
migrating from old text-based format to SQLite.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Breaking down the ~280ms gap between PersistedHistory and "Finished loading":
- loadTextStore: Loading ClientTextStore from disk (5MB+ completeText.txt)
- pregenAndRecovery: Populating pregenerated texts and recovering missing names
- buildController: Creating HeroGenerator, EngineImpl, and GameController
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Only load outstanding shardok battles on game load
Previously, we loaded ALL .e0s shardok battle files when loading a game,
even though most battles are completed. For a game with 71 battles, this
was loading ~70 unnecessary files.
Now we:
1. Load the e0a file first and reconstruct the GameState
2. Extract the outstanding battle IDs from outstandingBattles
3. Only load .e0s files for battles that are still in progress
4. Use parallel loading (via Futures) if > 4 files need loading
This should significantly reduce game load time for games with many
completed battles.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add timing for file listing and directory parsing
Logs the time to list all files in the game directory and parse the
directory index, to identify the remaining ~400ms gap in game loading.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add lazy loading for shardok battles not loaded on startup
When a client requests data for a shardok battle that wasn't loaded
during game initialization (because it wasn't in outstandingBattles),
we now try to load it from disk on-demand.
This handles the edge case where:
1. Client is watching a battle that's about to end
2. Battle ends but client hasn't received all updates
3. Server restarts and doesn't load the completed battle
4. Client requests the missing battle data
The lazy-loaded battles are cached (including negative results) to
avoid repeated disk access for the same battle ID.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Logs how many .e0a and .e0s files exist and how long each takes to load.
This helps diagnose why some games take 1-1.5s to load on first connection.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Move connection status UI to persistent canvas
Centralize connection status management on a persistent canvas that
remains visible across Eagle and Shardok game modes.
Changes:
- Add PersistentUIManager singleton to wire persistent UI elements
- Add PersistentClientConnection.Current static accessor
- Move ConnectionStatusUI to persistent canvas with inspector reference
- Remove connection status wiring from EagleGameController and
ShardokGameController
- ConnectionStatusUI.statusText is now an inspector field instead of
using GetComponent
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* WIP: Lobby UX cleanup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix lobby prefab instantiation transforms
Change worldPositionStays from true to false when instantiating lobby
list items. This allows instantiated prefabs to adopt proper local
transforms relative to their parent, fixing rotation and Z-value issues.
Also removes unnecessary localScale resets since the layout system
handles positioning correctly with worldPositionStays=false.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move Create Game to dedicated lobby section
Replace instantiated CreateGame prefab with a direct scene reference.
The Create Game UI now lives in its own section of the lobby panel.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify Create Game UI and add dropdown zebra striping
- Hard-code total player count to 7, remove totalPlayersDropdown
- Change human player dropdown labels to "1 player", "2 players", etc.
- Change random warlord fallback text from "Random" to "Random Warlord"
- Add AlternatingRowColor component for dropdown zebra striping
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Gitignore map generator output folder
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Assembly-CSharp.csproj with new script references
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Tracks success and failure counts for LLM requests by provider and
request type. Failures are recorded when server errors or timeouts
occur in LlmResolver. The admin console now displays success count,
failure count, and success rate percentage in both the summary cards
and the detailed tables.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a new page at /llm-stats in the admin console that displays:
- Summary cards showing total calls and token usage
- Stats by provider (OpenAI, Claude, Gemini)
- Stats by request type with averages
The page calls the existing GetLlmUsageStats gRPC endpoint added in #6012.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Persist LLM usage stats (by provider and request type) to DigitalOcean
Spaces on graceful shutdown and restore on startup. This follows PR #6012
which added the in-memory tracking.
Changes:
- Add JSON serialization to LlmUsageTracker with PersistedCounters and
PersistedLlmUsageStats case classes
- Add saveToS3/loadFromS3 methods with callback injection to avoid
circular dependencies between common and service packages
- Add registerShutdownHook to persist stats on graceful shutdown
- Create LlmUsageTrackerS3 to wire up S3 callbacks using existing S3Utils
- Initialize persistence in Main.scala after S3 warmup
Stats are stored at eagle/llm_usage_stats.json in Spaces. Stats will be
lost on crashes but preserved across graceful deployments.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
StopAll() cleared the Model but not CurrentProvince on panel controllers.
When starting a new game, the stale province from the previous game still
held hero IDs that don't exist in the new game's Heroes map, causing a
KeyNotFoundException in SetUpHeroesTable().
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Track total calls and tokens (input/output) for each LLM provider
(Anthropic, OpenAI, Gemini) and request type. Usage data is extracted
from streaming responses and recorded in a thread-safe tracker.
Key changes:
- Add LlmUsageTracker with TokenUsage, LlmUsageRecord, LlmUsageStats
- Extract token usage from Claude, OpenAI, and Gemini streaming responses
- Add tokenUsage field to StreamingTextResults
- Record usage in LlmResolver when streams complete
- Add GetLlmUsageStats RPC to GameAdmin service
- Track stats both by provider and by request type for optimization analysis
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When backstory updates are filtered out for AI-only factions, subsequent
backstory updates for that hero would fail because they had a hard
dependency on the previous backstory text ID.
Now the previous backstory text is optional:
- If available, use it for word count calculation
- If unavailable, estimate word count based on number of backstory
versions (assuming each grew by the growth rate)
- Only include available previous backstories in the prompt
- Events are always included regardless of previous backstory availability
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
On fresh deployments where the local save directory has no game files,
sync all saves from S3 during server startup. This avoids slow S3 loads
during the first user request.
The sync is skipped entirely when local game directories already exist
(the common case for normal deployments), so there's no overhead in
typical operation.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, existingKeys called ALL persisters and combined results,
which meant S3 ListObjectsV2 was called even when local files existed.
This caused slow first-connect latency after deployments.
Now existingKeys tries persisters in order and returns the first
non-empty result, matching the behavior of retrieveAsStream.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Filter hero backstory LLM updates to human-visible heroes only
Only generate backstory updates for:
- Heroes in human player factions
- Heroes in factions allied with human players
- Unaffiliated heroes in provinces controlled by human factions
Key changes:
- Add humanFactionIds field to GameState (tracks which factions are human-controlled)
- Create HeroDescriptions object with historyDescription() for event-based backstory
- Filter heroes in HeroBackstoryUpdateActionGenerator based on human visibility
- Update HeroDescriptionGenerator.descriptionWithoutFaction to use history descriptions
- Simplify HeroBackstoryUpdatePromptGenerator to use shared HeroDescriptions.textForEvent
This significantly reduces LLM requests for games with AI-only factions while
maintaining full backstory generation for human-visible heroes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use player-agnostic filtering for hero backstory LLM updates
Replace the previous humanFactionIds-based approach with a cleaner
player-agnostic design that leverages GameController's existing
recipientFactionIds filtering mechanism.
Key changes:
- Set alwaysGenerate=false on HeroBackstoryUpdateAction to enable filtering
- Remove humanFactionIds from GameState, Engine, and related converters
- Simplify HeroBackstoryUpdateActionGenerator to include all heroes
- Add sourceEvents field to BackstoryVersion to preserve events from
bypassed LLM updates
- Add effectiveBackstory() to HeroDescriptions that returns the most
recent available backstory plus collected events from bypassed versions
- Update ChronicleEventTextGenerator, HeroDescriptionGenerator, and
ChronicleUpdatePromptGenerator to use effectiveBackstory()
This approach is cleaner because the engine remains player-agnostic -
it doesn't know which factions are human-controlled. Instead, it relies
on the existing GameController filtering that skips LLM requests where
all recipients are AI factions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update ocean texture with Stylize Water Texture asset
- Replace water_caustics with stylized water texture for ocean background
- Adjust ocean tint color for better ocean appearance
- Increase scroll speed for more visible water motion
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused SnowTex and EpidemicTex from weather shader
Blizzard and Epidemic effects now use 3D particle effects only, so the
2D overlay shader no longer needs snow or epidemic textures. Only Flood
and Drought use the 2D weather overlay.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Since we no longer generate new backstory versions (only the initial
backstory plus structured event history), simplify the data model:
- Add `initial_backstory_text_id` field to battalion.proto (field 9)
- Deprecate the `backstory_versions` repeated field (field 7)
- Change Scala model from `backstoryVersions: Vector[BackstoryVersion]`
to `initialBackstoryTextId: Option[ClientTextId]`
- Update BattalionConverter to:
- Read from new field if non-empty, else fall back to last version
from deprecated field (for migration)
- Write only to new field
- Update BattalionDescriptions to use `initialBackstoryTextId` directly
This simplification reduces complexity since battalions now have exactly
one backstory text that never changes.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Stage 4: Replace backstory usage in BattalionDescriptions with initial
backstory + event history by adding historyDescription() method that
combines the first backstory version with structured event data.
Stage 5: Delete BattalionBackstoryUpdateActionGenerator entirely and
remove all calls to it from EndBattleAftermathPhaseAction,
EndVassalCommandsPhaseAction, EndPlayerCommandsPhaseAction, and
EngineImpl.
This eliminates a significant source of LLM requests since battalion
backstories were being regenerated after every battle and phase change.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ParticleStandardUnlitMaterial for BeastsEffect builds
Create material using Particles/Standard Unlit shader and assign to
BeastsEffect.birdMaterial. This ensures the shader is included in
standalone builds where Shader.Find() would otherwise fail.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Reduce EpidemicEffect green haze intensity
- Make haze color more desaturated (less pure green)
- Lower alpha from 0.12 to 0.08
- Reduce hazeEmitRate and hazeSize
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove the backstory_text_id field from the BattalionView proto message
and corresponding Scala model. This is part of eliminating battalion
backstory updates to reduce LLM request volume.
Changes:
- Mark field 7 as reserved in battalion_view.proto
- Remove backstoryTextId from BattalionView case class
- Update BattalionViewFilter to not set the field
- Update BattalionViewConverter for proto/scala conversion
- Update related tests
See docs/eliminate-battalion-backstory-updates.md for the full plan.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Blizzard now uses only the 3D particle effect (ProvinceBlizzardController).
The 2D shader-based overlay is removed to avoid duplicate effects.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Set backstoryTextId to empty string instead of the actual value when
constructing BattalionView. This stops sending backstory text IDs to
the client.
Part of eliminating battalion backstory LLM updates.
See docs/eliminate-battalion-backstory-updates.md
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
BeastsEffect:
- Change birdColor from dark brown to white so vulture texture renders
at full brightness instead of being nearly invisible
EpidemicEffect:
- Reduce hazeColor saturation and alpha (0.3 -> 0.12) for subtler effect
- Reduce hazeEmitRate from 25 to 8 particles/sec
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Disable the battalion backstory popup that appears on hover in the
Heroes & Battalions panel. This is the first stage of eliminating
battalion backstory LLM updates to reduce request volume.
The popup panel is now always hidden when hovering over battalions.
Hero backstories are unaffected.
See docs/eliminate-battalion-backstory-updates.md for the full plan.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add material fields to effect scripts for standalone build support
Shader.Find() fails in standalone builds because shaders aren't included
unless directly referenced. Add public Material fields to effect scripts
so materials can be assigned in prefabs, ensuring shaders are included.
Effects updated:
- BeastsEffect: birdMaterial
- EpidemicEffect: skullMaterial, hazeMaterial
- BlizzardEffect: particleMaterial
- FloodEffect: particleMaterial
Materials need to be created in Unity and assigned to prefabs:
1. Create material with "Particles/Standard Unlit" shader for Beasts/Epidemic
2. Create material with "Eagle/ProvinceParticle" shader for Blizzard/Flood
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add ProvinceParticleMaterial and include shader in build
- Create ProvinceParticleMaterial.mat referencing ProvinceParticleShader
- Add ProvinceParticleShader to GraphicsSettings always-included shaders
- Ensures shader is available in standalone Mac and iPad builds
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up ProvinceParticleMaterial in effect prefabs
Assign the material reference in BlizzardEffect and FloodEffect prefabs
to ensure the ProvinceParticleShader is included in standalone builds.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add ParticleAlphaBlendMaterial for BeastsEffect and EpidemicEffect
- Create ParticleAlphaBlendMaterial using Legacy Particles/Alpha Blended shader
- Add shader (fileID 200) to GraphicsSettings always-included list
- This fixes vultures showing as black boxes and epidemic lacking transparency
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up ParticleAlphaBlendMaterial in BeastsEffect and EpidemicEffect prefabs
- BeastsEffect: assign birdMaterial
- EpidemicEffect: assign skullMaterial and hazeMaterial
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add 3D particle effect controllers for province events
Add controllers for blizzard, drought, flood, and beasts events:
- ProvinceBlizzardController: falling snow particles
- ProvinceDroughtController: rising dust/heat particles
- ProvinceFloodController: rain/water splash particles
- ProvinceBeastsController: circling vultures/crows particles
These follow the same pattern as Festival and Epidemic controllers.
Prefabs need to be created in Unity and wired up in the scene.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update 3D effects doc with implementation status and prefab requirements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add particle effect components for province events
Add MonoBehaviour scripts that programmatically create particle systems
for each province event type:
- BlizzardEffect.cs: Falling snowflakes with wind drift
- DroughtEffect.cs: Rising dust and heat shimmer particles
- FloodEffect.cs: Falling rain streaks with ground splashes
- BeastsEffect.cs: Circling birds (vultures/crows) with orbital motion
Each effect is configurable via Inspector fields (emit rate, colors,
sizes, speeds, etc.) similar to the existing EpidemicEffect.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up province event effect controllers in Unity scene
- Add effect components to prefabs (Blizzard, Drought, Flood, Beasts)
- Add controllers to Map GameObject in Gameplay.unity
- Wire up mapContainer, centroidsJson, and prefab references
- Wire controller references in MapController
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add test controller for province event effects
- Add ProvinceEffectTestController with test methods for each effect type
- Add TestSpawnEffect() and ClearAllEffects() methods to all controllers
- Supports testing effects without game model data
Usage: Add ProvinceEffectTestController to scene, set province IDs,
then use context menu "Test All Effects" or enable "Spawn On Start".
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert "Add test controller for province event effects"
This reverts commit d13ae05b5661c7b29adb424d8249cec39110efdc.
* Add vulture icon for beasts province effect
- Add griffon-vulture.png icon (Flaticon, requires attribution)
- Fix velocity curve mode error in BeastsEffect.cs
- Wire up vulture texture to BeastsEffect prefab
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Rework province event effects with sun/shimmer drought and masking infrastructure
- DroughtEffect: Replace particle-based effect with pulsing sun icon and
heat shimmer overlay using custom GrabPass shader
- BlizzardEffect/FloodEffect: Add shader fallback chain for province masking
- Controllers: Add rawGray texture loading and province masking data passing
- Add HeatShimmerShader.shader for drought distortion effect
- Add ProvinceParticleShader.shader for province boundary clipping
- Scale particle emission areas based on province perpendicular_width
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix province masking with screen-space coordinate calculation
- Update shaders to use screen position instead of world position for
accurate UV calculation in Canvas UI context
- Controllers now properly convert world corners to screen coordinates
- Handle both Screen Space Overlay and other canvas render modes
- Fix map bounds calculation for province ID texture sampling
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Enlarge weather effects to fill provinces with shader masking
- DroughtEffect: Fix shimmer by using RawImage with white texture,
increase shimmer size to 2x province width for full coverage
- BlizzardEffect: Increase emission area, particle count (300),
emit rate (40/s), and lifetime (4s) for dense snowfall
- FloodEffect: Increase rain emission area, particle count (400),
emit rate (80/s), and splash coverage
- Controllers now use full province width for emission sizing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Double height of blizzard and rain emission areas
- Blizzard: Y scale 20→40, spawn height 60→120
- Rain: Y scale 20→40, spawn height 70→140
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix particle coverage and disable broken drought shimmer
- Blizzard/Flood: Change emission box to 150x150 with lower spawn point
so particles spawn at varying heights across the province
- Drought: Disable shimmer effect - GrabPass doesn't work with UI Canvas
(sun icon still shows)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add gentle waving animation to drought sun
- Sun now waves slowly in X and Y using different sine frequencies
for an organic floating feel
- Reduce default sun size from 60 to 40
- Wave amounts: X=8, Y=5 at speeds 0.4 and 0.6
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove test effects from province event controllers
Remove hardcoded test province spawning and keep-alive logic from all
six province event controllers (blizzard, flood, drought, festival,
epidemic, beasts). Effects will now only appear for provinces that
actually have the corresponding events.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When LLM requests are stuck waiting on dependencies, they were repeatedly
loading game state from disk on each processing cycle. This change caches
the game state directly in the text request (UnrequestedClientText and
IncompleteClientText) so it's loaded once and reused until the request
completes or is removed.
The cache is automatically cleaned up when the request is completed, since
the cached state is attached to the request object itself rather than stored
in a separate map.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Change iPhoneStrippingLevel from 0 (Disabled) to 2 and
managedStrippingLevel for iPhone from 1 (Minimal) to 3 (Medium).
This strips unused managed code from the IL2CPP build, potentially
reducing app size by 10-30MB. Medium stripping is safe for most code
but can break heavy reflection usage - protobuf serialization should
be fine.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The server calculates cost per-battalion with ceiling applied individually,
then sums. The client was summing costs as floats then applying ceiling once.
This caused small discrepancies when multiple battalions had fractional costs.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Two fixes for effect animations:
1. Z-position fix: Changed all effect animator z-positions from 3-6 to 0.
Effects were rendering behind other elements after sorting layer changes.
Affects 16 animators: Meteor, RaiseDead, Catapult, Control, Dismiss,
Duel, Extinguish, Fear, Fire, Flee, Freeze, HolyWave, Lightning,
Scout, Tool, and Water.
2. MeteorCast animation fix: Added IsServerTriggeredAction helper and a
new branch in history handler for server-triggered animations
(like MeteorCast) for the current player.
This was broken by #5130 which split meteor phases. Before #5130,
MeteorTargetCommand mapped to AnimationType.MeteorCast, so the
animation played immediately when targeting. After #5130, it correctly
mapped to MeteorTarget, but the MeteorCast result from the server
fell through all history handler branches.
The fix specifically handles server-triggered actions (MeteorCast,
MeteorCancel) that happen automatically rather than from direct
player commands. This avoids double-animating actions like Archery
that are already animated in PerformAction.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add HoveringTooltipTextProvider to all 6 province event images
(Festival, Blizzard, Epidemic, Flood, Drought, Beasts)
- Add docs/province-event-3d-effects.md with ideas for replacing
2D shader effects with 3D particle systems
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ProvinceIDLoader now exposes the decompressed bytes via DecompressedBytes
property, and MapController uses this instead of decompressing separately.
- ProvinceIDLoader: store decompressed bytes, expose via property
- MapController: remove duplicate decompression, use provinceIDLoader reference
- Gameplay.unity: wire MapController.provinceIDLoader to ProvinceIDLoader component
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Chia: Connected the large island to the main peninsula via land bridge
- Chia: Thickened the peninsula by 15 pixels to better fit province label
- Pozia: Assigned 199 previously uncolored pixels in southeast region
- Regenerated map_borders.png (borders only, transparent elsewhere)
- Updated centroids.json with recalculated province centroids
- Added edit_provinces.py tool for province map editing
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add epidemic particle effect with skulls and green haze
- ProvinceEpidemicController: Spawns effects at province centroids
when EpidemicEvent is active (similar to ProvinceFestivalController)
- EpidemicEffect: Particle system with configurable skull texture
reference and green haze/smoke rising from the ground
- Integrate with MapController to update effects when model changes
Skull texture needs to be assigned in Unity Inspector.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up epidemic particle effect in Unity scene
- Add EpidemicEffect.prefab with configured particle systems
- Add ProvinceEpidemicController to Map GameObject
- Wire mapContainer, epidemicEffectPrefab, and centroidsJson references
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add test code to show epidemic effect over Musland
Temporary test code - spawns epidemic effect at province ID 5 (Musland)
on Start() for visual testing. Remove after testing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Improve epidemic skull particle effect
- Fix transparency (proper alpha blend settings)
- Remove spinning, add slight random starting tilt
- Skulls grow as they rise (25% to 100%)
- More horizontal sway for drifting motion
- Use white color so texture renders as-is
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add rocking motion, spawn offset, and remove 2D epidemic effect
- Remove epidemic from ProvinceWeatherController (now handled by particles)
- Add vertical spawn offset so skulls rise through province center
- Add noise-based rotation for gentle rocking/swaying motion
- Skulls now sway back and forth ~11° as they rise
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove epidemic test code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add proto enum value and client support for a new recruitment status that
indicates a hero would never join a particular faction. This is plumbing
only - the logic to set this status will come in a follow-up PR.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace hex background with text outlines and image backing
Instead of drawing a white translucent background over the entire hex
when a unit is present:
- Text labels now use TMP outline + underlay for readability
- UnitType and Profession images have soft circular backing
- The hex-wide background color is no longer applied
This provides the same readability with less visual obstruction of the
terrain underneath.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix readability with soft circular backings for text and images
- Replace TMP underlay (box-shaped) with soft circular backing images
- Add backing images behind all text labels (upper, lower, overlay)
- Fix z-order bug where UnitTypeImage was moved behind its backing
- Add configurable labelBackingScale and imageBackingScale
- Show/hide backings dynamically based on content presence
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix backing z-order by moving all backings to front of sibling order
Backings from different cells were interleaving with foreground elements,
causing white blur to appear in front of text/images. Moving all backings
to the front of the sibling list ensures they render behind everything.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use separate container for backings to fix z-order
Create a BackingsContainer as the first child of overlayContent.
All backings are parented to this container, ensuring they render
behind all foreground elements (images and labels) regardless of
cell creation order.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make text backing elliptical to match text box proportions
Instead of a circle sized to the max dimension, the backing is now
an ellipse that matches the text box width/height ratio.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Configure HexGrid backing settings in scene
Set tuned values for label and image backings:
- Black outline (0.2 width) for text
- Cream-colored backings (1, 1, 0.74) for both labels and images
- Label backing scale 2.1, image backing scale 1.8
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a new column `never_join_faction_ids` to heroes.tsv and wire it
through the hero loading pipeline:
- LoadedHero: add neverJoinFactionIds field
- FixedHeroes: parse the new column from TSV
- LoadedHeroConversion: pass the field to HeroC and Hero proto
Set initial values:
- Bregos Fyar: never joins faction 1 (The Fracture Covenant)
- Ikhaan Tarn: never joins faction 2 (The King's Loyalists)
- The Eagle: never joins faction 2 (The King's Loyalists)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a new field to track faction IDs that a hero will never join under
any circumstances. This field is added to:
- hero.proto (repeated int32 never_join_faction_ids)
- HeroT trait (def neverJoinFactionIds: Set[FactionId])
- HeroC case class (neverJoinFactionIds: Set[FactionId] = Set.empty)
- HeroConverter (toProto and fromProto)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Shardok overlay rendering behind bridges
Set Overlay Container Pos Z to -100 to ensure overlay UI (hero names,
unit info, battalion icons) renders in front of 3D bridge meshes in
the ScreenSpaceCamera canvas.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Name dynamically created HexGrid GameObjects with coordinates
Adds coordinate suffixes (e.g., _3_5) to all dynamically created
GameObjects in HexGrid, making debugging easier. Objects are now
named like Terrain_0_0, UnitType_3_5, PrimaryLabel_2_4, etc.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Rockets were invisible when their Z velocity was positive (moving away
from camera in ScreenSpaceCamera canvas). Fixed by:
- Moving rocket particle system to child GameObject (not on RectTransform)
- Disabling shape module and setting velocity explicitly in EmitParams
- Using negative Z velocity (-1) to keep particles in front of camera
- Adding X variation (±15) for natural angle spread
- Adding velocityOverLifetime Y deceleration (-20) for arcing path
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Changes:
- Replace vertical GridLayoutGroup container with HorizontalLayoutGroup
- Remove provinceEventsContainer field and dynamic resizing code
- Event icons now shown/hidden individually without container wrapper
- Adjust icon sizes: min 12x12, preferred 24x24
- Add spacers for layout spacing
- Connect festivalController reference in MapController
This simplifies the layout code - no more dynamic cell size adjustments
based on wide/narrow screen mode.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Rockets were sometimes invisible because the particle system was
attached directly to a RectTransform (UI element). Particle systems
don't render reliably on UI GameObjects.
Fixed by creating the rocket particle system on a child GameObject,
matching how the burst system was already set up.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The low food warning was incorrectly triggering in December even when
taxes would provide enough food in January. This was caused by an
off-by-one error in calculating consumptions until taxes arrive.
Taxes arrive at the START of January, before January's food consumption.
So in December there are 0 more consumption events before taxes, not 1.
The fix changes from `monthsUntilJanuary` (1 for December) to
`consumptionsUntilTaxes` which is `12 - month` (0 for December).
Added tests for:
- December with sufficient support: no warning (taxes will save)
- December with low support: warning (won't receive taxes)
- November with sufficient food/support: no warning (can survive until taxes)
- November without sufficient food: warning (will starve before taxes)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the Canvas render mode is changed from ScreenSpaceOverlay to
ScreenSpaceCamera (needed for particle system rendering), world
coordinates must be converted to screen coordinates for hit testing.
- PanelPositions: Convert world corners to screen space using camera
- MapPinchZoomHandler: Use canvas camera for coordinate conversion
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Disable TutorialManager.DebugLogging in scene (was flooding console)
- Remove music loading debug logs from SoundManager
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Call UpdateLayoutForAspectRatio() at end of Start() to set up the
correct layout before the scene becomes visible, avoiding a jarring
rearrangement on first battle load.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ocean texture support to ProvinceMapShader
Instead of returning transparent for ocean pixels (ID 0/255), sample
an ocean texture with a tint color. This allows MapBWImage to contain
only border lines, reducing its size.
Properties added:
- _OceanTex: tileable ocean texture (or use white for solid color)
- _OceanColor: tint color for the ocean
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add ocean animation (scroll + wave distortion)
New properties:
- _OceanScrollSpeed: slow drift direction (default: 0.02, 0.01)
- _OceanWaveStrength: ripple distortion amount (default: 0.01)
- _OceanWaveSpeed: ripple animation speed (default: 1.0)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add border-only map image generated from rawGray
map_borders.png contains only province border pixels (88 KB vs 6.6 MB
for map_bw.png which includes ocean fill). Generated by checking each
pixel's neighbors for province ID changes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Y-axis orientation in map_borders.png
Flip Y coordinate when reading rawGray to match PNG orientation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove old map_bw images (replaced by map_borders.png)
Delete map_bw.png (6.6 MB) and map_bw_labels.png - ocean is now
rendered by the shader, so only border lines are needed.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use hardcoded rawGray dimensions for hit testing
MapController now uses constant dimensions (3786x1834) instead of
reading from mapBWImage.texture, which fixes hit testing after
switching to map_borders.png.
Also includes scene/material updates for ocean texture configuration.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix province ID 1 (Shumal) being treated as ocean
Province ID 1 = 1/255 = 0.00392, which was less than the 0.004
threshold. Lower threshold to 0.002 to only catch ID 0 (ocean).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused mapBWImage field from MapController
No longer needed since dimensions are hardcoded and ocean is
rendered by the shader.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace 43 province RawImages with single-draw province rendering
Use rawGray province ID map as source of truth for province boundaries,
eliminating 43+ draw calls and 43 mask textures.
New architecture:
- ProvinceMapShader samples rawGray for province ID, looks up color from
256x1 texture
- ProvinceColorManager manages the color lookup texture
- ProvinceIDLoader loads rawGray.gz as GPU texture
- ProvinceWeatherMapShader uses same approach for weather overlays
Benefits:
- 1-2 draw calls instead of 43+
- Deleted 43 mask PNG files (~6MB)
- Deleted 43 per-province materials
- Single source of truth (rawGray)
- Simpler, more maintainable code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add _MainTex property to province shaders for Unity UI compatibility
Unity's RawImage component expects shaders to have a _MainTex property.
Add it as a hidden, unused property to satisfy this requirement.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Adjust layer ordering: move MapBWImage in front of province layers
Ensure province borders render on top of province colors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Scale province label font size based on area
Larger provinces now get proportionally larger text labels:
- Reduce baseFontSize from 11 to 7 for smaller overall labels
- Scale font using sqrt(area/avgArea) for gentle area-based sizing
- Clamp area scale to 0.6x-2.5x range
- Still cap font size to fit within province width
This makes large provinces like Yuetia and Berkorszag more readable
while keeping small provinces from having oversized labels.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Keep baseFontSize at 11 for area-scaled labels
The area-based scaling with baseFontSize=7 made labels too small.
Keeping baseFontSize=11 with the sqrt(area) scaling provides better
readability for both large and small provinces.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Adjust label rotation and curvature for map aspect ratio
When the map is displayed at a different aspect ratio than the source
(3786x1834), the label angles and curves now adjust dynamically:
- Store source orientation and curvature per label
- Detect aspect ratio changes in Update()
- Transform angles using atan2 to account for horizontal stretch
- Scale curvature inversely with stretch factor
This fixes labels appearing at wrong angles when the map is stretched
to fit different screen resolutions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix NaN rotation when map rect is zero during initialization
- Guard UpdateLabelAngles() against zero rect dimensions
- Apply initial rotation from source orientation during label creation
- Defer aspect ratio correction to first Update() when rect is valid
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add settings slider for province label font size
- Add provinceLabelFontSizeSlider to SettingsPanelController
- Store province data (area, length, name length) for dynamic recalculation
- Add CalculateFontSize() method to compute font size from current baseFontSize
- RefreshLabels() now recalculates all font sizes when base size changes
- Font size persisted to PlayerPrefs
To use: Wire up slider (range ~5-20) in Unity to OnProvinceLabelFontSizeSliderChange
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up province label font size slider in settings panel
- Add slider UI to settings panel (range 0-24, default 11)
- Connect slider to SettingsPanelController.OnProvinceLabelFontSizeSliderChange
- Wire provinceLabelsController reference for live updates
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Scale province label font size based on area
Larger provinces now get proportionally larger text labels:
- Reduce baseFontSize from 11 to 7 for smaller overall labels
- Scale font using sqrt(area/avgArea) for gentle area-based sizing
- Clamp area scale to 0.6x-2.5x range
- Still cap font size to fit within province width
This makes large provinces like Yuetia and Berkorszag more readable
while keeping small provinces from having oversized labels.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Keep baseFontSize at 11 for area-scaled labels
The area-based scaling with baseFontSize=7 made labels too small.
Keeping baseFontSize=11 with the sqrt(area) scaling provides better
readability for both large and small provinces.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add curved text support for province labels
- Create CurvedText component that warps TMP mesh vertices along an arc
- Calculate curvature for each province based on shape deviation from principal axis
- Update centroids.json with curvature values for all provinces
- Update Rust map generator to calculate and include curvature
Provinces with notable curvature:
- Berkorszag: -30 (curves downward)
- Yuetia: 30 (curves upward)
- Tegrot: -30 (curves downward)
- Fluria: 25.8 (curves upward)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix curved text direction (was inverted)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Reduce curve intensity by 75% (scale factor 0.25)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make labels grow at half the zoom rate
Labels now scale with 1/sqrt(zoom) instead of 1/zoom, so they
grow slightly as you zoom in rather than staying constant size.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add distance-weighted centroid calculation for province labels
Implements BFS-based distance calculation from province boundaries to
weight interior pixels more heavily when computing centroids. This
produces label positions that are more visually centered within
irregular province shapes.
- Add calculate_weighted_centroid() to Rust map generator
- Update centroids.json with distance-weighted positions
- Key improvements: Yuetia centered (was at north coast), Berkorszag
now properly within its bounds
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add design doc for programmatic map image generation
Documents the plan to:
- Create a Python tool for regenerating map images with more equalized province sizes
- Implement dynamic province name rendering that scales with zoom
- Preserve topology and neighbor relationships while enlarging small provinces
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Python map generator tool for province size equalization
Implements Part 1 of the programmatic map generation plan:
- Python script using weighted dilation to grow small provinces
- Preserves jagged/natural edges using noise fields
- Allows small provinces to claim limited ocean pixels (20% max)
- Maintains neighbor relationships and realistic coastlines
Results:
- Province size ratio reduced from 110x to 22x
- Smallest province (Kojaria) grew 5x (3,262 → 16,590 pixels)
- All 43 province masks and centroids.json generated
Usage:
cd tools/map_generator
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python generate_map.py --generate
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add ProvinceLabelsController for dynamic map labels
Part 2 of programmatic map generation - dynamic province name rendering:
- Creates TextMeshPro labels at province centroids
- Labels scale inversely with zoom level for readability
- Only visible when zoom >= 1.5x (configurable)
- Font size scales with province area for visual hierarchy
- Includes centroids.json with province positions
To use:
1. Add ProvinceLabelsController component to a GameObject in the scene
2. Assign zoomHandler, mapContent (RectTransform), and centroidsJson TextAsset
3. Optionally configure font, colors, and zoom threshold
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add topology documentation and tendril removal to map generator
- Document new province adjacencies caused by equalization
- Add priority_provinces parameter for boosting specific province growth
- Add remove_tendrils() post-processing to eliminate thin extensions
- Note: Will be restructured to use Bazel in next commit
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Bazel Python support and parallelized map generator
- Add rules_python to MODULE.bazel with Python 3.13
- Move map generator to src/main/python/net/eagle0/eagle/
- Add parallel processing using ProcessPoolExecutor
- Provinces are now grown in parallel across multiple CPU cores
Usage:
bazel run //src/main/python/net/eagle0/eagle:map_generator -- --generate
bazel run //src/main/python/net/eagle0/eagle:map_generator -- --workers 8
The old tools/map_generator/ script remains for reference but the Bazel
version is now the primary implementation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Rust map generator and equalized province maps
- Add Rust map generator (src/main/rust/net/eagle0/eagle/map_generator/)
- Uses parallel graph coloring for non-adjacent province processing
- Generates equalized province sizes (smallest ~7x larger than before)
- Outputs: rawGray.gz.bytes, map_bw.png, province masks, centroids.json
- Run with: bazel run //src/main/rust/net/eagle0/eagle/map_generator -- --generate
- Update province_map.tsv with neighbor changes from equalization:
- Usvol gains: Laufarvia, Hella, Grytrand
- Chapellia loses Nikemi, gains Kojaria
- Kojaria gains Motcia, Chapellia
- Motcia gains Kojaria
- Update Unity assets with equalized map:
- New rawGray.gz.bytes with equalized province boundaries
- New map_bw.png with textured water, transparent land, dark borders
- Updated province mask images (1-43.png)
- Updated centroids.json with orientation data for label rotation
- Update ProvinceLabelsController.cs to support label rotation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix coordinate system for rawGray and province labels
- Fix save_raw_gray to not flip Y coordinates (was causing click detection
to be inverted)
- Fix ProvinceLabelsController to not flip Y coordinates (was causing
labels to appear in wrong positions)
- Update rawGray.gz.bytes with correct orientation
- Update Gameplay.unity with ProvinceLabelsController setup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Improve province label rendering
- Reduce max font size scale from 2.0 to 1.3
- Disable word wrapping, allow text overflow
- Fix orientation sign (remove negation)
- Allow near-vertical labels by expanding angle range to [-90, 90]
- Recalculate province orientations with new range
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Scale province label font size based on province dimensions
- Add principal_length and perpendicular_width to centroids.json
- Update ProvinceLabelsController to scale font based on principal_length
- Update Rust map generator to calculate province dimensions via PCA
eigenvalues (4 * sqrt(eigenvalue) for ±2 std dev coverage)
- Disable text wrapping on labels to prevent unwanted line breaks
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Reduce province label base font size to 11
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update neighbor positions and hex map starting positions for new borders
TSV changes (neighborPositions for new neighbors):
- Laufarvia: Usvol attacks from dir 4 (E)
- Hella: Usvol attacks from dir 2 (NE)
- Chapellia: Kojaria attacks from dir 4 (E)
- Usvol: Laufarvia=3(W), Hella=5(SW), Grytrand=4(E)
- Kojaria: Chapellia=1(N), Motcia=3(W)
- Grytrand: Usvol attacks from dir 3 (W)
Hex map changes (new attackerStartingPositions):
- Motcia.e0mj: Added direction 0 (NW) for Kojaria
- Chapellia.e0mj: Added direction 4 (SE) for Kojaria
- Kojaria.e0mj: Added directions 1 (N) and 3 (E) for Chapellia/Motcia
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert outline width change (made text smaller)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add TMP underlay effect for better label visibility on dark backgrounds
The underlay expands outward (unlike outline which shrinks text inward),
providing a white glow around labels without reducing text size.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The save_raw_gray function was incorrectly flipping Y coordinates,
causing the output to be vertically inverted compared to the input.
This caused click detection in Unity to select the wrong provinces.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace Python map generator with Rust implementation
- Add Rust map generator at src/main/rust/net/eagle0/eagle/map_generator/
- Parallel province equalization using graph coloring
- Generates equalized province sizes with ~7x improvement
- Run with: bazel run //src/main/rust/net/eagle0/eagle/map_generator -- --generate
- Remove Python map generator (src/main/python/)
- Rust version is faster and more maintainable
- Removes rules_python dependency from MODULE.bazel
- Add build test to verify Rust code compiles
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove Python from CI test workflow
Python map generator was replaced with Rust in this branch.
Rust tests are already covered by //src/test/...
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Replace multi-step dilation with single-step contiguous growth
- Prioritize boundary pixels with more same-province neighbors
- Use np.argpartition for efficient top-k selection
- Add two-phase cleanup (3x3 + 5x5 neighborhoods)
- Add neighbor analysis and comparison with TSV
- Update province_map.tsv with new neighbor relationships:
- Usvol now borders Laufarvia, Hella, Grytrand (was only Faluria)
- Laufarvia/Faluria no longer neighbors (Usvol between them)
- Al Raala now borders Tumala
- Results: 12.9x size ratio (from 110x), 2,263 cleanup pixels
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add tutorial battle system C++ implementation and docs
Brings in the C++ tutorial battle controller and documentation from the
tutorial-scenario-system branch:
- TutorialBattleController for managing scripted battle scenarios
- Documentation describing the tutorial battle system design
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add proto definitions and ShardokEngine integration for tutorial battles
Proto changes:
- Add TUTORIAL_ENEMY_FLED and TUTORIAL_REINFORCEMENTS_ARRIVED action types
- Add TutorialBattleConfig message to player_info.proto
- Add tutorial_battle_config field to NewGameRequest
C++ changes:
- Remove unused defenderCanFlee field from TutorialBattleController
- Integrate TutorialBattleController into ShardokEngine
- Add SetTutorialBattleConfig method to configure tutorial mode
- Check for scripted flee at end of each round in HandlePlayerTurnEnd
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix visibility for player_info_proto
Allow game_setup_info_proto in //src/main/protobuf/net/eagle0/common
to depend on player_info_proto from shardok/common.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix fire_test by moving early return inside tutorial flee block
The if (GameIsOver()) { return; } check was placed before NewRoundAction,
which caused STRUCTURES_BURNED to be skipped even for normal games. The
early return should only happen when the tutorial flee actually triggers
and ends the game.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert "Fix fire_test by moving early return inside tutorial flee block"
This reverts commit b0b08020873cefeaf411c07d6223ade5baad3c4e.
* Reapply "Fix fire_test by moving early return inside tutorial flee block"
This reverts commit 8038d73b57b1cf4e29c4e79e91dcb9e11b49e44d.
* Redesign tutorial system to be event-driven with configurable triggers
Replace hardcoded flee triggers with a flexible event system that supports:
- Trigger types: RoundTrigger, UnitsLostTrigger, DamageTakenTrigger, UnitKilledTrigger
- Action types: FleeAction, ReinforcementsAction
- Events fire in config order and at most once (tracked by event_id)
Key changes:
- Proto: Replace TutorialBattleConfig fields with repeated TutorialEvent
- Controller: Replace ShouldTriggerScriptedFlee/ExecuteScriptedFlee with CheckAndExecuteEvents
- Engine: Update HandlePlayerTurnEnd to use new CheckAndExecuteEvents API
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update tutorial battle doc: vassals, 600 cavalry, new event API
- Change defender heroes from "sworn brothers" to "vassals"
- Update attacker heavy cavalry from 400 to 600 troops
- Update proto reference to show new event-driven TutorialBattleConfig
- Update C++ API docs to show CheckAndExecuteEvents method
- Remove completed ShardokEngine integration from "Remaining" section
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move tutorial config to separate proto file
Extract TutorialBattleConfig and related messages from player_info.proto
into tutorial_battle_config.proto to avoid the code smell of having
game_setup_info.proto depend on two different player_info.proto files.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move tutorial_battle_config.proto to common/ package
Since the proto is used by both Eagle (game_setup_info.proto) and
Shardok (TutorialBattleController), it belongs in common/, not
shardok/common/.
Updated package from net.eagle0.shardok.common to net.eagle0.common
and updated all C++ references accordingly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Change ReinforcementsAction to use CommonUnit instead of hero names
This allows the reinforcement units to be fully specified with all
their attributes (hero stats, battalion type/size, etc.) rather than
just referencing heroes by name.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add tutorial_battle_config.proto to Unity C# project
The new proto was missing from protos.csproj, causing Unity builds to fail.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add go_package option and include proto in Go build
- Add go_package option to tutorial_battle_config.proto for Go code gen
- Add tutorial_battle_config_proto to common_go_proto target
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Fix layout of Organize Troops panel
- Add Unity Smart Merge (.gitattributes) for better merge handling of
Unity scene files (.unity, .prefab, .asset, etc.)
Note: To use Smart Merge, developers need to configure their ~/.gitconfig:
[merge]
tool = unityyamlmerge
[mergetool "unityyamlmerge"]
trustExitCode = false
cmd = '/Applications/Unity/Hub/Editor/*/Unity.app/Contents/Tools/UnityYAMLMerge' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Bazel Python support and parallelized map generator
- Add rules_python to MODULE.bazel with Python 3.13
- Create map generator at src/main/python/net/eagle0/eagle/
- Add parallel processing using ProcessPoolExecutor
- Provinces are grown in parallel across multiple CPU cores
Usage:
bazel run //src/main/python/net/eagle0/eagle:map_generator -- --analyze
bazel run //src/main/python/net/eagle0/eagle:map_generator -- --generate
bazel run //src/main/python/net/eagle0/eagle:map_generator -- --workers 8
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add smoke test for map_generator
Basic unit tests that verify:
- Constants are correct
- Land mask generation works
- Border detection works
- Border pixel filling works
Run with: bazel test //src/main/python/net/eagle0/eagle:map_generator_test
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Include Python tests in CI test job
Add //src/main/python/... to the bazel test command so Python tests
are run alongside other tests in CI.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Screenshot button onClick in Bug Report panel
The screenshot button's onClick handler was incorrectly pointing to
AttributionsController.Hide with a null target. Update to correctly
call BugReportPanelController.OnScreenshotButtonClicked.
Note: The screenshotButton reference in BugReportPanelController is
still null - this adds a scene-based onClick as a workaround. The
reference should be restored in Unity to fully fix the issue.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Restore BugReportPanelController screenshot references
Restore all the null references that were lost in a previous merge:
- screenshotButton
- screenshotStatusText
- screenshotPreviewContainer
- screenshotPreview
- settingsPanel
This allows the programmatic onClick listener in Awake() to work
correctly, and enables screenshot preview functionality.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix screenshotPreviewContainer reference
Point to "Screenshot Container" (the parent with LayoutElement) instead
of "Screenshot Preview" (the RawImage). This ensures the container is
properly hidden when no screenshot has been captured.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Bug Report panel layout
Restore proper positions and sizes for Bug Report panel UI elements
that were incorrectly zeroed out. Panel is now hidden by default.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add RectMask2D clipping support to ProvinceWeatherShader, matching
the fix applied to maskShader. Weather effects (drought, blizzard,
flood) now properly clip to the map bounds during zoom and pan.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The reportBugButton and bugReportPanel references were null ({fileID: 0}),
causing the Report Bug button to not function on the exception popup.
Properly assign these references in the Unity scene.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The rawGray province lookup map compresses extremely well due to
repeated values. Decompress at load time using GZipStream.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use Mathf.SmoothDamp to interpolate toward target zoom level instead
of applying zoom changes immediately. This eliminates jumpiness from
noisy touch input.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The names.tsv file was being generated but only names.json is actually
used by BattalionNameGenerator. The TSV was just an intermediate format.
- Update downloadTsvs.sh to send checker output to /dev/null
- Remove .gitignore entry for names.tsv
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The HTMX form was using hx-swap="outerHTML" which replaced the form element
with the response body. When the response body was empty (just HX-Redirect
header), the form would disappear before the redirect could trigger.
Fixed by:
1. Wrapping the form in a container div for proper targeting
2. Using hx-swap="innerHTML" to replace contents, not the container
3. Returning a success message body that HTMX can swap while processing
the redirect
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add pinch-to-zoom support for Eagle map view on iPad
Implements pinch-to-zoom and pan gestures for the strategic map:
- New MapPinchZoomHandler component for touch and scroll input
- Updates MapController.ProvinceFromPoint() to handle zoom/pan offsets
- Supports two-finger pinch/pan on iOS, scroll wheel on desktop
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add pinch-to-zoom for Eagle map using ScrollRect
- Rewrite MapPinchZoomHandler to use ScrollRect for proper clipping/panning
- Update maskShader to support RectMask2D clipping while preserving colors
- Configure Gameplay.unity with Map Container (ScrollRect + RectMask2D)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix zoom to center on mouse pointer position
Calculate scroll position adjustment to keep the point under cursor
stationary when zooming in/out.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert ProvinceFromPoint to original working logic
The original PanelPositions.MapRect approach works correctly at zoom
level 1. Zoom adjustment still needed.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix province click detection when map is zoomed
Calculate MapRect size from world corners instead of local rect size,
so it correctly includes the zoom scale from ScrollRect content scaling.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up zoomHandler reference in MapController
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
New quest command choosers:
- SpendOnFeastsQuestCommandChooser: Selects FeastCommand when quest active
- RestProvinceQuestCommandChooser: Rests in target province
- SendSuppliesQuestCommandChooser: Sends food to target province
- ExecutePrisonerQuestCommandChooser: Executes specific prisoner
- ReturnPrisonerQuestCommandChooser: Returns prisoner to faction
- StartEpidemicQuestCommandChooser: Starts epidemic in target province
- SwearBrotherhoodQuestCommandChooser: Swears brotherhood with target hero
Also adds Execute and Return helper methods to ManagePrisonersCommandSelector.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Unity warnings (like audio device changes) are informational and
shouldn't interrupt gameplay with alert popups. Only show alerts
for actual errors and exceptions.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
SendSuppliesCommand was missing quest fulfillment logic. This adds
the incrementing of SendSuppliesQuest progress using the existing
QuestFulfillmentUtils pattern from RestCommand.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add narrow layout support for March command battalions
- Add battalionsColumnNarrow view for narrow screen layouts
- Add availableBattalionsContentNarrow to EventBasedUnitSelector for dual population
- Add PopulateBattalionsContent() helper to populate both content areas
- Toggle between original and narrow battalion views based on aspect ratio
- Keep heroes column always visible (user may march from different province)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix selectionChanged event binding on March unit selector
Clear the erroneous binding to AddTargetedBattalion - the original
SelectionChanged method was removed in 2022 and the binding should
be empty.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove the separate Overlay Canvas and move overlay content into the
main Shardok Canvas. This eliminates the need to duplicate view hierarchy
between the two canvases.
- Remove overlayCanvas reference from HexGrid
- Remove Start() that was setting overlayCanvas.overrideSorting
- Move overlay content to be a sibling after hex grid in Shardok Canvas
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add quest fulfillment logic to ControlWeatherCommand:
- StartDroughtQuest is fulfilled when a Mage uses ControlWeather to start
a drought in the target province
- StartBlizzardQuest is also now fulfilled via the same mechanism
- Both quest types were previously marked as "action quest - fulfilled in
ControlWeatherCommand" but the implementation was missing
Changes:
- ControlWeatherCommand now extends ProtolessSequentialResultsAction instead
of ProtolessSimpleAction to support returning quest fulfillment results
- Added QuestFulfillmentChecker and allProvinces parameters to ControlWeatherCommand
- Updated CommandFactory to pass new parameters
- Updated tests to use .results instead of .immediateExecute
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add quest creation logic for StartDroughtQuest, following the same pattern
as StartBlizzardQuest:
- Requires a Mage in the faction
- Targets provinces reachable by Control Weather (faction provinces + neighbors)
- Filters out provinces that already have a drought
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Implement prisoner management quest choosers that enable the AI to
complete ReleasePrisonerQuest, ExilePrisonerQuest, and
ReleaseAllPrisonersQuest.
New components:
- ManagePrisonersCommandSelector: Reusable selector for prisoner
management commands (release, exile, return)
- ReleasePrisonerQuestCommandChooser: Releases specific prisoner
- ExilePrisonerQuestCommandChooser: Exiles specific prisoner
- ReleaseAllPrisonersQuestCommandChooser: Releases any available
prisoner to make progress toward quest completion
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
HexMetrics: Properly center the grid vertically by setting center.y to
0.75*OuterRadius. Previously the grid center ended up at -0.75*OuterRadius
instead of 0, requiring a compensating Y=32 offset on GameObjects.
Added detailed comments explaining the centering math for future reference.
HexGrid: Remove the OuterRadius/2 vertical offsets from overlay elements
(text labels, unit type images, profession images, secondary images,
terrain modifier images) that were compensating for the old centering bug.
Unity: Set Hex Grid and Overlay Mesh Y positions to 0.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update Shardok layout for widescreen support
Unity scene changes for Shardok layout variants.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Set Weather Canvas screen overlay alpha to 0 in editor
The overlay color is set programmatically at runtime, so setting it
transparent in the editor makes it easier to work with the scene.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Shardok layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Shardok layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add overlay container spacer references for widescreen layout
Add overlayContainerTopSpacer and overlayContainerLeftSpacer references:
- Top spacer active on normal/narrow screens (with top row)
- Left spacer active on widescreen (with left column)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add vertical offset to overlay elements for new layout
Move all overlay elements (labels, unit type images, profession images,
secondary images, terrain modifier images) up by metrics.OuterRadius/2
to align with hex cells in the new layout group structure.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify Shardok mesh and hierarchy structure
- Remove -90 rotation requirement from HexMesh by swapping Y/Z coordinates
- HexMesh now uses Y for vertical position, Z for depth (was reversed)
- HexMetrics corners updated to match new coordinate system
- Replace gridCanvas with gridTransform reference in HexGrid
- Update all animators to work with simplified hierarchy
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Consolidate HexGrid onto HexMesh GameObject
- Merge HexGrid component onto the HexMesh GameObject (renamed to Hex Grid)
- Remove redundant separate Hex Grid GameObject
- Move Overlay Mesh under Overlay Content to match Hex Grid hierarchy
- Fix Overlay Mesh transform to match Hex Grid (Y=32, no rotation)
- Update component references
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Implement TotalDevelopmentQuestCommandChooser that enables the AI to
complete TotalDevelopmentQuest by improving the province until the
total development reaches the quest target.
The chooser:
- Prioritizes repairing devastation if total devastation >= 4
- Otherwise improves the province's lowest development stat (economy,
agriculture, or infrastructure)
- Uses existing ImproveCommandSelector for command generation
Also reorders quest choosers in FulfillQuestsCommandSelector:
- Move TotalDevelopment right after Improve (higher priority)
- Move Alliance to end of list (lower priority)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the primary LLM provider returns a 5xx error (like 503 Service
Unavailable), the system now automatically fails over to backup providers.
Key changes:
- Add ServerError case to ExternalTextGenerationError for 5xx detection
- OkHttpSseListener now creates appropriate error types based on HTTP status
- ApiKeys gains hasOpenAI/hasAnthropic/hasGemini and availableProviders methods
- LlmResolver tracks provider health with circuit breaker pattern:
- 3 consecutive 5xx failures marks provider unhealthy for 5 minutes
- Automatic failover tries providers in order: primary → openai → claude → gemini
- All failover events are logged with [LLM] prefix
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add StartDroughtQuest similar to StartBlizzardQuest and StartEpidemicQuest.
This is PR1 of the 3-PR pattern for new quests (types only).
Changes:
- Proto: Added StartDroughtQuest message and field to QuestDetails oneof
- Scala: Added StartDroughtQuest case class
- Converter: Added toProto/fromProto cases
- Fulfillment: Added case (returns false - action quest)
- Failure: Added case (returns false - never fails)
- LLM prompts: Added descriptions for DivineMessage and QuestEnded
- DivineCommand: Added province ID extraction for notifications
Note: Also added missing StartEpidemicQuest cases in DivineCommand.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
On stream disconnect, scheduleReconnect was passing None for newGameRequest
instead of using the stored request from pendingBattles. This caused an
infinite error loop when Shardok lost the game state (e.g., after a pod
restart) because it couldn't recreate the game without the map path.
The fix retrieves the stored NewGameRequest and passes it on reconnect,
allowing Shardok to recreate the game from scratch if needed.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The UI is too small on iPhone screens. Change targetDevice from 2
(Universal) to 1 (iPad only) so the app only appears in the App Store
for iPad users.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace deprecated altool (removed in Xcode 14) with xcodebuild
-exportArchive using App Store Connect API authentication.
Changes:
- upload_testflight.sh: Use xcodebuild with destination=upload
and API key authentication instead of altool
- ios_testflight.yml: Pass xcarchive path and use new API key secrets
Required new GitHub secrets:
- APP_STORE_CONNECT_API_KEY_ID
- APP_STORE_CONNECT_API_ISSUER_ID
- APP_STORE_CONNECT_API_KEY (contents of .p8 file)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents which quests the AI proactively attempts to complete via
FulfillQuestsCommandSelector and which quests are not handled.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, Feast was only available when at least one hero had vigor < constitution
or loyalty < 100. This prevented players from completing SpendOnFeastsQuest when
all heroes were already at full stats.
Now Feast is available whenever the province has enough gold.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Override TargetedProvince to allow selecting the Move destination by
right-clicking a province on the map. When a valid move destination is
clicked, the Move toggle is enabled and the dropdown is set to that
province.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix RestProvinceQuest not incrementing when resting
RestProvinceQuest is a ComponentQuest that should increment progress when the
player uses the Rest command in the target province. The increment logic was
missing.
Changes:
- RestCommand: Added quest increment logic to check for RestProvinceQuest
where targetProvinceId matches the resting province
- CommandFactory: Pass factionProvinces to RestCommand.make
- BUILD.bazel: Added quest and unaffiliated_hero dependencies
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix RestCommandTest to pass factionProvinces parameter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The "secure borders" quest (BorderSecurityQuest) now counts provinces owned
by allies (actual alliances, not just truces) towards fulfillment. Previously,
only provinces directly controlled by the quest faction would count.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a recon succeeds, the quest counter wasn't being updated. Added logic to
PerformReconResolutionAction to increment quest progress using the existing
QuestFulfillmentUtils.withCountersIncremented pattern.
- ReconProvincesQuest: increments by 1 for any successful recon
- ReconSpecificProvincesQuest: increments by 1 if the reconned province is a target
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The failure check had inverted logic - it was checking if the target
province is owned by the same faction (which is always true at creation)
instead of checking if we no longer own it. Now uses the same logic as
RestProvinceQuest.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ImproveCommand was not updating quest progress when repairing devastation.
Added quest progress tracking similar to how FeastCommand handles
SpendOnFeastsQuest - increments componentsFulfilled for any unaffiliated
hero with a RepairDevastationQuest across all faction provinces.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add hero backstory popup on hover for free heroes
Add LongHoverRowChanged handler to FreeHeroesTableController to show
the hero backstory popup when hovering over unaffiliated heroes, matching
the behavior of resident heroes in HeroesAndBattalionsPanelController.
Unity wiring needed:
- Wire popupPanel, popupPanelDetailsController, popupPanelBackstory refs
- Set LongHoverRowChangedHandler on unaffiliatedHeroesTable to call
FreeHeroesTableController.LongHoverRowChanged
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up free hero backstory popup in Unity scene
Connect popup panel references and LongHoverRowChangedHandler for
unaffiliated heroes table.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add TableRowHoverDetector to UnaffiliatedHeroRow prefab
Required for hover detection to work on free hero rows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Blizzard quests (StartBlizzardQuest) now require the faction to have a
Mage hero, and epidemic quests (StartEpidemicQuest) require a Necromancer.
This matches the profession requirements for the corresponding actions.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instead of throwing NoSuchElementException when a userId is not found
in userIdToFactionId (which can happen after server restart), send an
UNAUTHENTICATED error to the client so it knows to reconnect.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Defer mesh collider assignment to the next frame to avoid PhysX cooking
errors during initial Shardok setup. The error only occurs on the first
battle after launch, suggesting a timing issue with mesh initialization.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update Please Recruit Me quest UI layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Ransom Offer Panel UI layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Alliance, Break Alliance, and Truce offer panel layouts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Quest descriptions now show both the current province count and months
completed, e.g., "Maintain 3 provinces with Develop orders for 4 months
(2/3 provinces, 1/4 months)"
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert Manage Prisoners to button-style toggles
- Add ToggleGroup and ConfigureToggle for consistent toggle styling
- Remove individual RawImage icon references
- Use CanvasGroup alpha for disabled state styling (35%)
- Default selection priority: Release > Move > Exile > Execute > Return
- Move dropdown only visible when Move option is selected
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Ransom Panel layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add reconSpecificProvincesQuests to QuestCreationUtils
- Creates quests with 2-4 specific province IDs to recon
- Targets provinces not controlled by the faction
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add SpendOnFeastsQuest generation
- Add spendOnFeastsQuests to QuestCreationUtils that creates quests
requiring 2-4 feasts spending 200-500 gold total
- Modify FeastCommand to track quest progress when feasts are held
- Pass factionLeaderProvinces to FeastCommand from CommandFactory
- Add quest_fulfillment_utils, quest, and unaffiliated_hero deps
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix FeastCommandTest to pass factionLeaderProvinces parameter
The FeastCommand.make method was updated to require a factionLeaderProvinces
parameter for quest progress tracking, but the test file wasn't updated.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify SpendOnFeastsQuest to track total gold spent
- Remove factionLeaderProvinces parameter from FeastCommand
- Track gold spent (componentCount = totalGold, increment by goldCost per feast)
- Only check current province for quest progress
- Simplify quest creation (single random for totalGold target)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Change SpendOnFeastsQuest target gold range to 500-1000
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Check each notification's field associations individually while ignoring
affectedProvinceIds, which varies based on random quest assignment. This
prevents the test from failing every time a new quest type is added.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add repairDevastationQuests to QuestCreationUtils
- Creates quests requiring repair of 30-100 devastation points
- Update test expectations for changed random sequence
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ReconProvincesQuest generation
- Add reconProvincesQuests to QuestCreationUtils
- Creates quests requiring reconnaissance of 3-6 provinces
- Update test expectations for changed random sequence
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix DivineCommandTest expected affectedProvinceIds
The random sequence changed with the addition of reconProvincesQuests,
causing the quest generation to produce different quests.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1. Speed up check_build_deps.sh:
- Cache expensive `bazel query deps(...)` results
- Reuse cached deps for all three checks instead of running
separate bazel query commands
- Use grep filtering on cached results instead of bazel intersect
2. Run lint job in parallel with test job:
- Split bazel_test.yml into separate lint and test jobs
- Jobs run concurrently, reducing total CI time
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents why SparklePlugin is built in a separate workspace due to
rules_swift version conflicts, and provides a checklist for when/how
to reintegrate it into the main workspace.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add RestProvinceQuest generation
- Add restProvinceQuests to QuestCreationUtils
- Creates quests requiring 2-4 months of rest on faction provinces
- Update test expectations for changed random sequence
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix GrandArmyQuest test to use >= instead of >
The test was using `> 5700` but with the changed random sequence from
adding RestProvinceQuest, the generated value is exactly 5700. Using
`>=` is still valid since the test checks that the quest requests an
army larger than the current count.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Crack Down UI
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add button-style toggles to Free For All Decision selector
Match the Improve command selector pattern with ToggleGroup and
CanvasGroup for proper disabled state styling.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Convert Attack Decision to button-style toggles
- Add ToggleGroup and ConfigureToggle for consistent toggle styling
- Replace individual icon/slider/label references with tributeContainer
- Use CanvasGroup alpha for disabled state styling (35%)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Adjust Attack Decision spacing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Rename ReconSpecificProvincesQuest to ReconProvincesQuest and add new ReconSpecificProvincesQuest
- Rename existing ReconSpecificProvincesQuest (count-based) to ReconProvincesQuest
- Add new ReconSpecificProvincesQuest that tracks specific province IDs to recon
- Update proto definitions, Quest.scala, QuestConverter.scala
- Update LLM prompt generators for both quest types
- Update CheckForFulfilledQuestsAction comment
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update C# client for ReconProvincesQuest rename and new ReconSpecificProvincesQuest
- Update DisplayNames.cs with renamed and new quest types
- Update UnaffiliatedHeroRowController.cs to handle both quest types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Layout improvements to Issue Orders panel
- Update tutorial to explain focus province receives excess vassal supplies
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add sendSuppliesQuests to QuestCreationUtils
- Creates quests to send 1000-3000 food to other faction provinces
- Only available if faction has at least 2 provinces
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add startEpidemicQuests function to QuestCreationUtils to generate
StartEpidemicQuest for provinces that don't already have an epidemic.
Update DivineCommandTest expectations for new random sequence.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add display names and quest descriptions for:
- StartEpidemicQuest: "Start Epidemic" - Start an epidemic in {province}
- SpendOnFeastsQuest: "Host Feasts" - Host feasts costing X gold
- SendSuppliesQuest: "Send Supplies" - Send X food to {province}
- RestProvinceQuest: "Rest Province" - Keep {province} at rest for X months
- ReconSpecificProvincesQuest: "Recon Provinces" - Recon X provinces
- RepairDevastationQuest: "Repair Devastation" - Repair X devastation
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add bug report system enhancements
- Add "Report Bug" button on exception popup with pre-populated info
- Add prompt to include recent exceptions when opening bug report manually
- Add screenshot capture button in bug report panel with Discord upload
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify exception handling: auto-include instead of prompting
Remove exception prompt UI and automatically include recent exceptions
in the bug report description when opening from settings menu.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up bug report UI components in Gameplay scene
- ErrorHandler: reportBugButton, bugReportPanel reference
- BugReportPanelController: screenshot button, status text, preview
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix screenshot preview layout: hide container not just RawImage
Add screenshotPreviewContainer reference to hide/show the entire
container with LayoutElement, so layout group collapses the space
when no screenshot is present.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire screenshotPreviewContainer in Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add new quest type definitions
Add proto messages, Scala case classes, and converters for:
- StartEpidemicQuest: Start an epidemic in a target province
- SpendOnFeastsQuest: Spend gold on feasts (component-based)
- SendSuppliesQuest: Send food supplies to a province (component-based)
- RestProvinceQuest: Keep a province in rest orders (component-based)
- ReconSpecificProvincesQuest: Recon a target number of provinces
- RepairDevastationQuest: Repair devastation across provinces
Also adds failure conditions for StartEpidemicQuest, SendSuppliesQuest,
and RestProvinceQuest. Other quests have no specific failure conditions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add StartEpidemicQuest to CheckForFulfilledQuestsAction
Fix pattern match exhaustivity warning by adding case for
StartEpidemicQuest (action quest - fulfilled in ControlWeatherCommand).
Also add clarifying comment for other new ComponentQuest types.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add LLM prompt cases for new quest types
Add pattern match cases for StartEpidemicQuest, SpendOnFeastsQuest,
SendSuppliesQuest, RestProvinceQuest, ReconSpecificProvincesQuest,
and RepairDevastationQuest to both DivineMessagePromptGenerator
and QuestEndedGeneratorUtilities.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Bazel 8 changed module extension repository naming from `~~` and `~` to
`++` and `+`. Update workflows to find crane dynamically instead of
using hardcoded paths that break with Bazel version changes.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Upgrade rules_java to 9.3.0 (enables JDK 25 toolchain support)
This adds an explicit dependency on rules_java 9.3.0, which includes
support for JDK 25 remote toolchains. The build continues to use
Bazel's embedded JDK 24 by default, but JDK 25 toolchains are now
available for future use.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Mark MCTS basic test as flaky
The shardok_mcts_ai_basic_test uses Monte Carlo Tree Search which
has inherent non-determinism. Tests like PrefersArcheryOverEndTurn
and DoesNotPreferStartFireWhenNotBeneficial assert on AI decisions
that may vary between runs depending on random exploration paths.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add client UI support for ApprehendOutlawQuest (PR 2/3)
- DisplayNames.cs: Added "Apprehend Outlaw" display name
- UnaffiliatedHeroRowController.cs: Added quest description with
dynamic hero name lookup for the target outlaw
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Enable ApprehendOutlawQuest generation (PR 3/3)
Add quest generation for ApprehendOutlawQuest. The quest is generated
when there are outlaws in the faction's territory:
- Finds all outlaws across all faction-controlled provinces
- Creates one quest candidate per outlaw
- Quest requires apprehending that specific outlaw
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- DisplayNames.cs: Added "Apprehend Outlaw" display name
- UnaffiliatedHeroRowController.cs: Added quest description with
dynamic hero name lookup for the target outlaw
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Move SparklePlugin to separate Bazel workspace to resolve rules_swift conflict
The problem: grpc 1.76.0.bcr.1 requires rules_swift 3.x, but rules_apple 4.x
requires rules_swift 2.x. These have different compatibility levels, causing
bzlmod resolution to fail when both are in the same workspace.
The solution: Move SparklePlugin (the only thing using rules_apple) to a
separate Bazel workspace at `sparkle_workspace/`. This workspace has its own
MODULE.bazel with only rules_apple and Sparkle dependencies, completely
isolated from the grpc/flatbuffers dependency tree.
Changes:
- Create sparkle_workspace/ with isolated MODULE.bazel
- Move SparklePlugin source files to sparkle_workspace/
- Update build_sparkle_plugin.sh to build from subworkspace
- Remove rules_apple from main MODULE.bazel
- Add single_version_override for rules_swift 3.1.2 (for grpc/flatbuffers)
All 315 tests pass.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add WORKSPACE.bazel to sparkle_workspace to prevent parent workspace detection
Without this file, Bazel may walk up the directory tree and find the parent
workspace's MODULE.bazel, causing dependency conflicts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This target was a development utility that's no longer used.
Removing it helps reduce rules_apple dependencies.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ApprehendOutlawQuest type (PR 1/3)
Add a new quest type where unaffiliated heroes require the faction to
apprehend a specific outlaw currently in their territory. The quest:
- Proto: Added ApprehendOutlawQuest message with outlaw_hero_id field
- Scala: Added case class with quest fulfillment logic
- Failure: Quest fails if the outlaw is no longer in faction territory
- Fulfillment: Completed when ApprehendOutlawCommand targets the outlaw
- Updated ApprehendOutlawCommand to check for quest fulfillment and
return multiple results (main result + quest completion if matched)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ApprehendOutlawQuest failure condition
Only fail if the hero is no longer an outlaw anywhere. Moving to a
different province is fine - quest remains valid. Quest fails only
when someone else apprehends them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Enable WinBattlesQuest generation (PR 3/3)
Adds WinBattlesQuest to the available quest generators. The quest
requires winning 2-4 battles (randomly determined).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix DivineCommandTest for new quest type random sequence
Adding WinBattlesQuest changed the random sequence during quest creation,
causing hero 12 to get a quest with a province target. Updated test
expectation to match the new (correct) notification output.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add WinBattlesQuest type (PR 1/3)
Adds a new quest type where a hero asks the player to win X battles
(where X is 2-4). This is a ComponentQuest that tracks progress.
- Proto: Add WinBattlesQuest message and field
- Quest.scala: Add WinBattlesQuest case class extending ComponentQuest
- QuestConverter: Handle WinBattlesQuest toProto/fromProto
- ResolveBattleAction: Increment quest counter when winning battles
- CheckForFulfilledQuestsAction: Quest is fulfilled via ComponentQuest
handling when componentsFulfilled >= componentCount
This quest never fails.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add WinBattlesQuest cases to prompt generators
The exhaustive pattern matching on Quest types requires adding cases
to all prompt generators when a new quest type is added.
Also updates CLAUDE.md to require running full test suite before pushing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- BorderSecurityQuest: Creates one quest per border province (provinces
with neighbors controlled by other factions)
- WinBattleOutnumberedQuest: Simple quest with no parameters
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds quest generation for BetrayAllyQuest, which is available when
the faction has at least one ally.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instead of a generic "betray any ally" quest, BetrayAllyQuest now
targets a specific faction with `targetFactionId`. This makes the
quest more meaningful and allows players to plan strategically.
Changes:
- Proto: Add target_faction_id field to BetrayAllyQuest message
- Quest.scala: Change from case object to case class with targetFactionId
- QuestConverter: Handle new targetFactionId field
- CheckForFailedQuestsAction: Update failure logic to check if specific
target faction no longer exists or is no longer an ally
- BreakAllianceResolutionHelpers: Check if broken alliance matches
quest's target faction for fulfillment
- DivineMessagePromptGenerator & QuestEndedGeneratorUtilities: Update
quest descriptions to mention specific ally name
- UnaffiliatedHeroRowController (C#): Display specific ally name in UI
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix reconnecting status stuck after sleep/wake
Race condition: when device wakes from sleep, CheckForIdleTimeout and
HandleStreamingCall both try to initiate reconnection, leading to
conflicting state updates and duplicate retry timers.
Fix: Cancel thread token in CheckForIdleTimeout before disposing the
streaming call, and check the token in HandleStreamingCall exception
handlers to skip ScheduleReconnect if another path is handling it.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix threadToken scope to be accessible in catch blocks
Move threadToken declaration outside try block so exception handlers
can check if the token was cancelled.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updates .bazelrc to use Java 21 for compilation.
Requires PR #5865 (URL deprecation fix) to be merged first, otherwise
the build will have deprecation warnings from the old URL constructor.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds quest generation for SwearBrotherhoodWithHeroQuest, which targets
vassal heroes in the province who are not already faction leaders.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds C# client display support for:
- SwearBrotherhoodWithHeroQuest: "Swear brotherhood with {heroName}"
- BetrayAllyQuest: "Break an alliance with an ally"
- BorderSecurityQuest: "Control all provinces bordering {provinceName}"
- WinBattleOutnumberedQuest: "Win a battle while outnumbered"
Changes:
- DisplayNames.cs: Add quest type strings
- UnaffiliatedHeroRowController.cs: Add quest descriptions in SetQuestText and ShortQuestString
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds server-side support for:
- SwearBrotherhoodWithHeroQuest: fulfilled when player swears brotherhood with the target hero
- BetrayAllyQuest: fulfilled when player breaks an alliance with an ally
- BorderSecurityQuest: fulfilled when all neighboring provinces of the target province are controlled
- WinBattleOutnumberedQuest: fulfilled when winning a battle while having fewer troops
Each quest has proto definitions, Scala case classes, converter logic, fulfillment/failure
checks, and LLM prompt generators. PR 2 will add client UI and PR 3 will add quest generation.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds two utility functions for checking hero faction membership:
- heroIsInFaction(heroId, factionId, heroes): Efficiently checks if a
specific hero belongs to a faction by checking hero.factionId
- heroIdsInFaction(factionId, heroes): Returns Set of all hero IDs
belonging to a faction
These functions check the hero's factionId field, which is the
authoritative source for faction membership. Heroes retain their
factionId when in a province, in a moving army, or imprisoned. They
lose it when exiled, become outlaws, or depart.
This is more reliable than checking province.rulingFactionHeroIds
which misses heroes in moving armies.
Includes comprehensive tests for various scenarios:
- Heroes in provinces
- Heroes in moving armies (retain factionId)
- Imprisoned heroes (retain factionId)
- Exiled heroes (factionId cleared)
- Outlaws (factionId cleared)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Upgrade Eagle server and JFR sidecar from Java 21 to Java 25.
Java 25 is the latest LTS release (September 2025). Scala 3.7.2
fully supports JDK 25.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Initial backstory: 50 → 100 words
- Growth per update: 20 → 50 words
- Soft cap: 150 → 300 words
This allows battalion histories to build up more narrative over time
while still keeping them from growing unbounded.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Upgrade Eagle server and JFR sidecar from Java 17 to Java 21.
Benefits:
- Better garbage collection (Generational ZGC available)
- General JVM performance improvements
- Foundation for future virtual threads adoption
- Supported until 2031
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use JsonConvert.ToString() from Newtonsoft.Json for proper JSON string
escaping instead of manual character replacement. This handles all
control characters and edge cases correctly.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Eagle and admin server use different S3 buckets:
- Eagle: eagle0 bucket (game data) - uses DO_SPACES_* secrets
- Admin: eagle0-assets bucket (What's New) - uses ACCESS_KEY_ID/SECRET_KEY secrets
The previous change (#5847) broke Eagle by giving it credentials that only
have access to eagle0-assets. This fix:
- Reverts Eagle to use DO_SPACES_* secrets (eagle0 bucket access)
- Adds ADMIN_S3_* env vars for admin server (eagle0-assets bucket access)
- Admin container receives ADMIN_S3_* values as DO_SPACES_* env vars
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Eagle server only needs to run Java, not compile it. Using the JRE
base image instead of JDK reduces the image size from ~273MB to ~120MB.
The JFR sidecar still uses JDK because it needs jcmd for JFR dumps.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
TrySetCanceled() can trigger synchronous continuations that re-enter
the lock (C# locks are reentrant) and call Remove() on the dictionary
while the foreach is still iterating, causing InvalidOperationException.
Fix by snapshotting values and clearing the dictionary before cancelling
the TaskCompletionSources.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The docker_build workflow was using DO_SPACES_ACCESS_KEY/SECRET_KEY
secrets which don't have write access. Use ACCESS_KEY_ID/SECRET_KEY
instead, which are the same secrets that work for CI builds.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add quest creation logic to QuestCreationUtils.scala:
- Only available if faction's provinces have at least 3 prisoners
- Returns ReleaseAllPrisonersQuest when condition is met
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add QuestTypeString case in DisplayNames.cs
- Add ShortQuestString case in UnaffiliatedHeroRowController.cs
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add ReleaseAllPrisonersQuest that requires the faction to release all
prisoners held in their provinces. The quest fails if any prisoner is
executed, moved, or traded away in ransom.
- Add proto definition and Scala case object
- Add proto converter for serialization
- Add fulfillment check (succeeds when 0 prisoners/moving prisoners)
- Add failure handling in ManagePrisonersCommand (Execute, Move)
- Add failure handling in RansomResolutionHelpers (ransom acceptance)
- Add LLM prompts for divine message and quest ended narratives
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The controller was on the What's New Panel itself, which starts disabled.
This meant Awake() wouldn't run until the panel was first enabled, but
the WhatsNewManager tries to call Show() before that happens - resulting
in the modal blocker appearing without the panel.
Moving the controller to the always-active Settings object ensures it
initializes properly at startup.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Enable battalion diversity quest generation (PR 3/3)
Adds quest creation logic for BattalionDiversityQuest to QuestCreationUtils.
Quest availability:
- Only if province currently has 1-2 battalion types
- This encourages players to diversify their army composition
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Clarify PR dependency structure in quest docs
PR 2 (client) and PR 3 (generation) both depend on PR 1 (types),
but are independent of each other. They can be developed in parallel
and merged in either order after PR 1.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The existing cleanup only removed dangling images. Now also removes
images older than 24h to prevent disk space exhaustion while keeping
recent images available for rollback.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The S3 API requires credentials with bucket access, but the file is
publicly accessible at assets.eagle0.net. Use HTTP GET for reading
(no credentials needed) and keep S3 API only for writing.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add long-press support for right-click on touch devices
On iPad and other touch devices, right-click is not available. This adds
long-press detection to GeneralClickDetector (used by the strategic map)
to trigger right-click behavior after holding for 0.5 seconds.
- Tracks pointer down time and position
- Triggers right-click after 0.5s if finger hasn't moved >10 pixels
- Suppresses normal left-click if long-press was triggered
- Cancels long-press detection if finger moves too far
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Only enable long-press for touch input, not mouse
Mouse users can right-click, so long-press should only trigger for
touch devices. Check pointerId >= 0 to distinguish touch (0+) from
mouse (-1, -2, -3 for left, right, middle buttons).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Restore support for ACCESS_KEY_ID/SECRET_KEY env vars (used by CI)
while also supporting DO_SPACES_* env vars (used by docker-compose).
Priority order:
1. ~/.s3cfg file
2. DO_SPACES_* env vars
3. ACCESS_KEY_ID/SECRET_KEY env vars
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds BattalionDiversityQuest - a quest where unaffiliated heroes want
to see three different types of battalion at near-full strength (80%
capacity) in the current province.
This PR adds:
- Proto message definition
- Scala case object
- Proto converter
- Fulfillment check (near UpgradeBattalionQuest)
- LLM prompt generators
Does NOT generate the quest yet - that will be in PR 3 after client
handling is added in PR 2.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace duel_challenged sound with licensed asset
Replace duel_challenged.mp3 (unknown license) with Weapon Draw Metal 1.wav
from the purchased Medieval Combat Sounds Unity Asset Store pack.
The dramatic sword unsheathing sound fits well for a duel challenge.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up Weapon Draw Metal 5 as duel_challenged sound
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add S3 credentials to admin server container
The admin server needs DO_SPACES_* environment variables to access
S3/Spaces for What's New data storage. Without these, the What's New
Management page fails with "static credentials are empty".
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix S3 credentials: use DO_SPACES_* env vars
The aws package was looking for ACCESS_KEY_ID/SECRET_KEY env vars,
but docker-compose passes DO_SPACES_ACCESS_KEY/DO_SPACES_SECRET_KEY.
Updated ReadAwsConfig() to use the correct env var names and also
respect DO_SPACES_ENDPOINT.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add stale PR refs cleanup to iOS CI workflows
Self-hosted runners persist .git between runs. When a PR is updated,
old local refs may point to commits that were never fetched, causing
"Could not scan for Git LFS files" errors.
Applied to:
- ios_addressables_build.yml
- ios_testflight.yml
(Same fix already present in mac_build.yml and unity_build.yml)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fetch LFS after checkout to avoid stale ref issues
Move LFS fetching out of actions/checkout and into a separate step.
This ensures our stale PR refs cleanup runs before any LFS operations,
avoiding "Could not scan for Git LFS files" errors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix LFS fetch order in mac_build and unity_build workflows
Apply the same fix as ios_* workflows: set lfs: false in checkout
and fetch LFS files manually afterward to avoid stale ref issues.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The admin server needs DO_SPACES_* environment variables to access
S3/Spaces for What's New data storage. Without these, the What's New
Management page fails with "static credentials are empty".
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add quest creation logic for DevelopProvincesQuest and MobilizeProvincesQuest
to QuestCreationUtils.
Quest parameters:
- Availability: Only if faction has >= 3 provinces
- Target province count: Random 3-5, capped at current province count
- Target months: Random 3-6
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add ADDING_NEW_QUESTS.md documentation explaining the three-PR
strategy and listing all files that need modification for new quests
- Add DevelopProvincesQuest and MobilizeProvincesQuest cases to
DisplayNames.QuestTypeString()
- Add quest description strings to UnaffiliatedHeroRowController
showing progress (e.g., "3/5 months")
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add click-to-dismiss on modal blockers as safety net
If a modal panel somehow becomes invisible while its blocker remains
active, users can now tap the dimmed area to dismiss it. This prevents
getting stuck with an unresponsive UI.
Applied to all three controllers that share the modal blocker:
- WhatsNewPanelController
- BugReportPanelController
- SettingsPanelController
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Give each modal panel its own dedicated modal blocker
Previously Settings, Bug Report, and What's New panels shared a single
modal blocker, which could cause state conflicts if one panel's blocker
state affected another.
Now each panel has its own dedicated blocker:
- Settings Panel Modal Blocker
- Bug Report Modal Blocker
- What's New Modal Blocker
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The fetchWhatsNewData function was silently swallowing ALL errors and
returning empty data, not just "file not found" errors. This masked
real issues like permission errors.
Now:
- Only returns empty data for actual "NoSuchKey" (file not found) errors
- Logs and returns other errors so they appear on the page
- Logs successful fetches with entry count for debugging
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The htmx form was receiving both an HX-Redirect header and HTML body content.
This caused htmx to swap the content before processing the redirect, making
the button appear to do nothing.
Fix: Return only the HX-Redirect header without body content on success.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Province Order Quests (Develop/Mobilize)
Add two new quest types where unaffiliated heroes want the faction to
maintain a certain number of provinces in specific order states
(Develop or Mobilize) for a cumulative number of months.
Quest mechanics:
- Availability: Only if faction has >= 3 provinces
- Target province count: Random 3-5, capped at current province count
- Target months: Random 3-6 (componentCount)
- Progress: Each round, if faction has >= X provinces with the required
order type, componentsFulfilled increments by 1
- Completion: When componentsFulfilled >= componentCount
- Failure: If faction falls below X total provinces
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove ProvinceOrderQuestProgress action result type
Fold the province order quest progress changes into the EndVassalCommandsPhase
action result instead of creating a separate action result type.
Also improve LLM prompt descriptions to explain what Develop and Mobilize
orders mean (Develop = improve agriculture/economy/infrastructure,
Mobilize = organize/train/arm troops for war).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove quest generation for province order quests
Keep the proto definitions, Scala types, converters, and handling code
(fulfillment, failure, progress tracking, LLM prompts) but remove the
actual quest creation logic from QuestCreationUtils.
This allows the client to be updated to understand these quest types
before the server starts generating them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change costLabel to only show the numeric cost value
- Add separate costWarningLabel for "(only X available)" warning
- Warning label only shown when insufficient gold available
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update hero and battalion backstory prompt generators to explicitly
request flowing narrative prose rather than dated lists. The LLM
should weave events into a cohesive story that reads as a biography
or unit history, not a timeline.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add underProvinceNameText/underProvinceOwnerText to MapController
- Update SetUpCenterText to populate both regular and under text fields
- Add clickedProvincePanel/underClickedProvincePanel refs to EagleGameController
- Switch panel visibility based on screen width in ArrangeLayout
- Organize EagleGameController Inspector fields with Header attributes
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Notify connected clients when a new version is available after CI deploy.
Flow:
1. CI deploys new build and waits 60s for CDN cache
2. CI calls admin server HTTP endpoint with shared secret
3. Admin server calls Eagle via gRPC
4. Eagle broadcasts to all connected lobby users
Components:
- Proto: ClientUpdateAvailable message, NotifyClientUpdate RPC
- Eagle: notifyClientUpdate() broadcasts to lobby users
- Admin server: /notify-update HTTP endpoint with secret auth
- CI: Notify steps in mac_build.yml and unity_build.yml
- Docker: NOTIFY_SECRET env var passed to admin container
Client-side handling will be added in a follow-up PR.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add battalion backstory to LLM prompt generators
Add descriptionWithBackstory methods to BattalionDescriptions that include
the battalion's backstory text alongside the basic description (name, type,
size). This gives LLM prompts richer context about the units involved.
Updated prompt generators:
- SuppressBeastsPromptGenerator (Failed and Succeeded): Include battalion
backstory when describing beast hunting attempts
- HeroBackstoryUpdatePromptGenerator: Include battalion backstory in
FoughtInBattle, SuppressedBeasts, and SuppressedRiot events
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix battalion backstory usage in prompts
Use basic description inline in sentences and include backstory as
separate context block, matching how hero backstories are handled.
- In SuppressBeastsPromptGenerator: use basic description in
"$heroName took [battalion]" and add fullDescription as context
- In HeroBackstoryUpdatePromptGenerator: use basic description only
in event text (hero's story doesn't need battalion backstory detail)
- Renamed methods in BattalionDescriptions for clarity:
- backstoryText: just the backstory
- fullDescription: "Name is a battalion... $backstory"
- optionalFullDescription: for optional battalions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix duplicate code blocks from rebase
The rebase introduced duplicate code blocks in FoughtInBattle,
SuppressedBeasts, and SuppressedRiot cases that caused syntax errors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused BattalionView methods from BattalionDescriptions
Since EventForHeroBackstory now uses BattalionId instead of BattalionView,
we no longer need the BattalionView overloads in BattalionDescriptions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The FoughtInBattle, SuppressedBeasts, and SuppressedRiot events now store
Option[BattalionId] instead of Option[BattalionView]. This allows looking
up the battalion from GameState at prompt generation time, providing access
to the battalion's current backstory text rather than a snapshot.
Changes:
- Proto: Changed battalion field from BattalionView to optional int32
- EventForHeroBackstoryT.scala: Changed battalion type to Option[BattalionId]
- EventForHeroBackstoryConverter: Updated toProto/fromProto conversion
- ResolveBattleAction, SuppressBeastsCommand, HandleRiotCrackDownCommand:
Now pass battalion.map(_.id) when creating events
- HeroBackstoryUpdatePromptGenerator: Looks up battalion from GameState
using gameState.battalions.getOrElse(id, gameState.destroyedBattalions(id))
- Updated tests and removed unused BattalionViewFilter deps
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update Notification Panel layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Additional Notification Panel layout tweaks
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update button text styling
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix empty.tail crash in BattalionBackstoryUpdatePromptGenerator
When a battalion had no previous backstory versions, calling .last on
an empty vector caused a NoSuchElementException. Use lastOption with
a match to safely handle the empty case by returning an empty string.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Generate initial backstory for newly raised battalions
When a player raises a new battalion via OrganizeTroopsCommand, now
generates a BattalionInitialBackstoryRequest so the battalion gets
its first backstory. Previously only battalions created at game start
(in NewGameCreation) received initial backstories.
This ensures all battalions have backstories from their creation,
complementing the empty.tail fix which handles the transitional case.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Update Suppress Beasts command panel layout in Unity
- Only show "(only X available)" when insufficient gold, with newline
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add references for battalion name (GeneratedTextUpdater) and type icon
(RawImage) to the battalion popup panel header. Populate them when
hovering over a battalion row.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When creating backstory events for changed battalions, the code assumed
every battalion in changedBatt would also be in changedBattalions. This
is not true when a battalion is modified indirectly - for example, when
troops are transferred OUT of it to another battalion.
The fix uses find() and handles the None case with default values (0 for
hired, dismissed, and transferred in counts).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add git corruption detection to Unity CI workflows
Self-hosted runners can accumulate corrupted git state over time.
Add a pre-checkout step that runs git fsck and nukes the repo if
corruption is detected, allowing a fresh clone.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix CI git corruption: check target ref objects instead of full fsck
Self-hosted runners can have stale refs (e.g., refs/remotes/pull/*/merge)
pointing to commits whose objects weren't fetched when the PR was updated.
Running git fsck is too broad and slow. Instead, check if the specific
target ref's objects are complete before checkout.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix CI: prune stale PR refs instead of checking all objects
Self-hosted runners persist .git between runs. When a PR is updated,
actions/checkout doesn't prune old local refs like refs/remotes/pull/*/merge.
These stale refs may point to commits whose objects were never fetched,
causing "missing object" errors during checkout.
Fix by explicitly removing PR refs before checkout. This is faster and
more targeted than validating object completeness.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add four new battalion backstory event types:
- SuppressedRiot: Records when a battalion helps suppress a riot, including
the riot size, casualties, and whether the suppression succeeded
- Trained: Records training sessions by a hero, with training improvement
- Armed: Records equipment upgrades with armament improvement and cost
- SurvivedStarvation: Records when battalions suffer casualties due to
food shortage, with loss proportion and whether on march or in garrison
The LLM prompt generator includes guidance to not over-emphasize routine
training and arming events in favor of more dramatic events like battles
and riots.
Also includes per-battalion cost tracking in ArmTroopsCommand to properly
attribute gold spending for each battalion's backstory event.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add What's New feature to Unity client
- Add WhatsNewManager singleton for fetching and tracking changelog
- Add WhatsNewPanelController for modal display with category styling
- Integrate CheckAndShow() call in ConnectionHandler after OAuth login
The manager fetches entries from assets.eagle0.net/whats-new.json,
tracks last seen date in PlayerPrefs, and shows new entries on login.
UI prefab setup required in Unity Editor.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up What's New UI in Unity
- Move WhatsNewEntryUI to separate file for Unity component discovery
- Add What's New Entry prefab
- Wire up WhatsNewManager and WhatsNewPanelController in Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add battalion backstory infrastructure
Adds support for battalions to have backstory events similar to heroes:
- New proto message EventForBattalionBackstory with event types:
- OrganizedTroops: tracks size changes, hiring, dismissals, transfers
- SuppressedBeasts: records beast suppression operations
- ApprehendedOutlaws: records outlaw apprehension (for future use)
- FoughtInBattle: records battle participation and casualties
- New Scala enum EventForBattalionBackstoryT matching the proto
- Updated BattalionT trait and BattalionC with:
- backstoryVersions: Vector[BackstoryVersion] for LLM-generated text
- backstoryEvents: Vector[EventForBattalionBackstoryT] for events
- Helper methods: withBackstoryVersions, withBackstoryEvents, addBackstoryEvent
- New converters:
- BackstoryVersionConverter (extracted for reuse)
- EventForBattalionBackstoryConverter
- Updated BattalionConverter to handle backstory fields
- Wired up backstory events in:
- SuppressBeastsCommand: adds SuppressedBeasts event to battalion
- ResolveBattleAction: adds FoughtInBattle event to battalions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add LLM-driven battalion backstory updates
This commit adds the system to actually use battalion backstory events
to trigger LLM-generated backstory updates, similar to the hero backstory
update system.
New components:
- BattalionBackstoryUpdateAction: finds battalions with events, generates
LLM requests, adds new backstory versions, and clears events
- BattalionBackstoryUpdateActionGenerator: creates action from game state
- BattalionBackstoryUpdatePromptGenerator: generates prompts describing
events for LLM to update backstory
- BattalionBackstoryUpdateRequest proto message and converter
The system is wired into:
- EndPlayerCommandsPhaseAction
- EndVassalCommandsPhaseAction
- EndBattleAftermathPhaseAction
- EngineImpl (command execution)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Track previousSize/newSize for battalion events, add heroId to OrganizedTroops
Changes:
- SuppressedBeasts event: replaced 'casualties' with 'previousSize' and 'newSize'
- ApprehendedOutlaws event: added 'previousSize' and 'newSize' fields
- OrganizedTroops event: added 'heroId' for the province ruler who ordered the reorganization
- OrganizeTroopsCommand now creates battalion backstory events when troops are organized
- Updated prompt generator to include hero names and size information
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a game is deleted (via admin console or last player dropping),
cancel any pending Shardok battles to:
- Stop processing battles for non-existent games
- Prevent reconnection attempts for deleted games
- Clean up resources
The implementation removes matching entries from pendingBattles map,
which prevents scheduleReconnect from re-establishing streams and
handleStreamingResponse from processing further updates.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add CRUD handlers for managing what's-new entries stored in S3
- Add whats_new.html template with add/edit/delete forms
- Add navigation link to layout.html
- Add player-friendly summary generation to generate_changelog.sh
- Add seed JSON file for initial data
The admin console at /whats-new allows managing changelog entries
that will be displayed to players in the Unity client. The script
integration generates summaries and opens the admin console with
pre-populated content after sending weekly changelogs.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:51:53 -08:00
855 changed files with 87297 additions and 47544 deletions
@@ -110,6 +110,10 @@ bazel run gazelle # Update Go build files
1.**If you modified any BUILD.bazel file:** Run `bazel run gazelle` and stage any changes it makes
2.**If you modified C++ or C# files:** Run `clang-format -i` on the modified files
3.**If you modified Scala files:** scalafmt will run automatically via pre-commit hook
4.**ALWAYS run the full test suite for your language changes before pushing:**
- For Scala changes: `bazel test //src/test/scala/...`
- For C++ changes: `bazel test //src/test/cpp/...`
- This catches exhaustive pattern match errors and other compile-time failures that single-target builds miss
The pre-commit hook runs gazelle but only checks if it succeeds - it does NOT verify the BUILD files are in canonical format. The `gazelle_test` will fail if deps are not alphabetically sorted. **Always run gazelle manually after BUILD file changes.**
@@ -221,6 +225,9 @@ to be used for different players or game situations within the same server proce
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
- Seamless transition between strategic gameplay and hex-based tactical combat
- **NEVER add defensive null checks on Unity Inspector fields** - these hide configuration bugs. If a field isn't
linked in the editor, it should throw a NullReferenceException so the problem is immediately obvious. Silently
skipping code when a required field is null makes bugs harder to find.
**Go (Build Tools):**
@@ -329,6 +336,8 @@ dotty.tools.dotc.core.MissingType: Cannot resolve reference to type net.eagle0.e
**Configuration:** Game parameters in `/src/main/resources/net/eagle0/eagle/game_parameters.json`
**Data Files:** TSV format for battalions, heroes, and other game data
**NEVER modify hero names.** Do not change names in `heroes.tsv`, `generated_heroes`, or any other hero data files. The names are carefully chosen and are not to be altered.
## Deployment
- Bazel handles multi-language builds and dependencies
This document describes how to add new quest types to Eagle. Quests are tasks that unaffiliated heroes want factions to complete before they'll join.
## Deployment Strategy: Three-PR Approach
When adding new quest types, use a three-PR strategy to ensure clients never see quests they don't understand:
1.**PR 1 - Types Only (Server)**: Add proto definitions, Scala types, converters, and all handling logic (fulfillment, failure, LLM prompts). Do NOT generate the quests yet.
2.**PR 2 - Client Handling**: Add client-side display code (C#/Unity) that can render the new quest type.
3.**PR 3 - Quest Generation (Server)**: Add the actual quest creation logic to `QuestCreationUtils.scala`.
**PR Dependencies:**
```
PR 1 (types)
├── PR 2 (client)
└── PR 3 (generation)
```
PR 2 and PR 3 both depend on PR 1, but are independent of each other. They can be developed in parallel after PR 1 is merged, and merged in either order. The key constraint is that PR 3 should not be deployed before PR 2, so clients understand the quest type before the server starts generating it.
Add a case to `didFulfillQuest` that returns `true` when the quest conditions are met:
```scala
caseMyNewQuest(someField,anotherField)=>
// Return true if quest is fulfilled
someConditionIsMet(province,someField)
```
For `ComponentQuest` subclasses, the base case `case q: ComponentQuest => q.componentsFulfilled >= q.componentCount` handles fulfillment automatically.
Add a case to `ShortQuestString` for displaying the quest in the UI:
```csharp
caseSealedValueOneofCase.MyNewQuest:{
vardetails=quest.Details.MyNewQuest;
return$"Do something with {details.SomeField}";
}
```
For quests involving heroes (where you want dynamic name updates), add a case to `SetQuestText` instead.
### Build Files
After making changes, run:
```bash
bazel run gazelle
```
This updates BUILD.bazel files with any new dependencies.
## Quest Type Categories
### Simple Quests
Quests with fixed completion conditions (e.g., `AllianceQuest`, `SuppressRiotByForceQuest`).
### Component Quests
Quests with multi-part completion that track progress via `componentsFulfilled` / `componentCount` (e.g., `AlmsToProvinceQuest`, `DevelopProvincesQuest`). Extend `ComponentQuest` and implement `withComponentsFulfilled`.
### Action Quests
Quests completed by specific player actions rather than state checks. Return `false` in `didFulfillQuest` and handle completion in the relevant command handler (e.g., `ExecutePrisonerQuest` is fulfilled in prisoner management command).
Create a tool to programmatically regenerate the Eagle strategic map images with more equalized province sizes while preserving neighbor relationships. Also implement dynamic text rendering for province names that scales with zoom.
## Current Map Assets
| File | Description | Dimensions |
|------|-------------|------------|
| `Assets/Eagle/rawGray.gz.bytes` | Province ID lookup map (gzip compressed) | 3786 x 1834 |
| `Assets/Eagle/map_bw_labels.png` | B&W map with baked-in province labels | 3786 x 1834, RGBA |
| `Assets/Eagle/Materials/1.png` - `43.png` | Individual province mask images | 3786 x 1834, grayscale |
## Province Data
- 43 provinces defined in `src/main/resources/net/eagle0/eagle/province_map.tsv`
- Each has: id, name, neighbors, neighborPositions, hexMapName
- Problem provinces (too small): **Shumal** (id=1), **Kojaria** (id=39), **Usvol** (id=33), **Tumala** (id=35), **Chia** (id=43)
## Goals
1.**Equalize province sizes** - Reduce disparity so small provinces are easier to click
2.**Preserve topology** - Maintain all neighbor relationships
This document explains why `rules_apple` is built in a separate Bazel workspace (`sparkle_workspace/`) and what conditions need to be met before it can be reintegrated into the main workspace.
## Background
The main workspace cannot include `rules_apple` due to transitive dependency conflicts with `rules_swift`. Multiple dependencies require different major versions of `rules_swift`:
| Dependency | rules_swift Version | Compatibility Level |
Bzlmod's `single_version_override` can force a single version, but compatibility levels 2 and 3 are incompatible. Forcing `rules_swift` 3.x (required for grpc) breaks `rules_apple` and `flatbuffers`.
## Current Solution
The `SparklePlugin` (the only component requiring `rules_apple`) is built in an isolated workspace:
└── BUILD.sparkle # Build file for Sparkle framework
```
### How It Works
1.**Build script**: `scripts/build_sparkle_plugin.sh` builds the plugin from the separate workspace
2.**Output**: The built `SparklePlugin.bundle` is placed in `Assets/Plugins/macOS/`
3.**CI integration**: `mac_build.yml` calls `inject_sparkle.sh` to embed Sparkle into the Mac app
The main workspace uses `single_version_override` for `rules_swift` 3.x to satisfy grpc, and includes a comment noting that `rules_apple` is intentionally excluded.
## What SparklePlugin Does
SparklePlugin is a native macOS library that:
- Initializes the [Sparkle](https://sparkle-project.org/) auto-update framework
- Exposes C functions for Unity to call via P/Invoke:
-`SparklePlugin_Initialize`
-`SparklePlugin_CheckForUpdates`
-`SparklePlugin_CheckForUpdatesInBackground`
-`SparklePlugin_IsCheckingForUpdates`
-`SparklePlugin_GetAutomaticallyChecksForUpdates`
-`SparklePlugin_SetAutomaticallyChecksForUpdates`
-`SparklePlugin_IsRunningFromReadOnlyVolume`
-`SparklePlugin_ShowDMGWarning`
## Conditions for Reintegration
To move `rules_apple` back into the main workspace, **all** of the following must be true:
### 1. rules_swift Version Alignment
Check if grpc, flatbuffers, and rules_apple all support the same `rules_swift` major version:
```bash
# Check what rules_swift version each dependency requires
This document describes the tutorial battle system for first-time players. The system provides a scripted introductory battle that teaches combat basics while telling a story.
---
## Overview
When a new player starts their first game in tutorial mode, they experience:
1.**Narrative Intro** - Story screens introducing the scenario
2.**Immediate Battle** - Skip strategic map, start directly in combat
3.**Scripted Flee** - Enemy flees when player is "on the ropes"
This document describes which quests the AI attempts to complete proactively to recruit unaffiliated heroes.
## Overview
The AI attempts to complete quests via `FulfillQuestsCommandSelector`, which is invoked by `MidGameAIClient.chosenFulfillEasyQuestsCommand`. The AI only considers quests from unaffiliated heroes in provinces ruled by a faction leader.
## Quests the AI Actively Completes
### Diplomacy Quests
| Quest | Handler | Conditions |
|-------|---------|------------|
| `AllianceQuest` | `AllianceQuestCommandChooser` | Target faction must meet trust conditions for alliance and not already be in an alliance |
| `TruceWithFactionQuest` | `TruceWithFactionQuestCommandChooser` | Target faction must meet trust conditions for truce |
| `TruceCountQuest` | `TruceCountQuestCommandChooser` | Picks a random faction that meets trust conditions and isn't already in a truce/alliance |
### Resource Giving Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `AlmsToProvinceQuest` | `AlmsToProvinceQuestCommandChooser` | Gives food to the specified province |
| `AlmsAcrossRealmQuest` | `AlmsAcrossRealmQuestCommandChooser` | Gives food from the province with the largest surplus |
| `GiveToHeroesInProvinceQuest` | `GiveToHeroesInProvinceQuestCommandChooser` | Gives gold to the hero with the lowest loyalty in the specified province |
| `GiveToHeroesAcrossRealmQuest` | `GiveToHeroesAcrossRealmQuestCommandChooser` | Gives gold from the province with the most gold available |
### Province Development Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `ImproveAgricultureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Agriculture type |
| `ImproveEconomyQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Economy type |
| `ImproveInfrastructureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Infrastructure type |
### Other Quests
| Quest | Handler | Conditions |
|-------|---------|------------|
| `DismissSpecificVassalQuest` | `DismissSpecificVassalCommandChooser` | Only if province has more than 2 heroes AND the unaffiliated hero's power >= target hero's power * `RequiredPowerMultiplierForDismiss` |
## Quests the AI Does Not Attempt to Complete
The following quests have no handler in `FulfillQuestsCommandSelector` and must be completed naturally through gameplay:
### Combat/Military Quests
-`DefeatFactionQuest` - Defeat a specific faction
-`GrandArmyQuest` - Accumulate a large number of troops
-`UpgradeBattalionQuest` - Upgrade a battalion to minimum armament/training
-`WinBattleOutnumberedQuest` - Win a battle while outnumbered
-`WinBattlesQuest` - Win a number of battles
-`RescueImprisonedLeaderQuest` - Rescue an imprisoned leader from another faction
### Expansion Quests
-`ExpandToProvincesQuest` - Expand to control a certain number of provinces
-`SpecificExpansionQuest` - Conquer a specific province
-`BorderSecurityQuest` - Have troops in a border province
### Prisoner Quests
-`ExecutePrisonerQuest` - Execute a specific prisoner
-`ExilePrisonerQuest` - Exile a specific prisoner
-`ReleasePrisonerQuest` - Release a specific prisoner
-`ReturnPrisonerQuest` - Return a prisoner to their faction
-`ReleaseAllPrisonersQuest` - Release all prisoners
### Province Order Quests
-`DevelopProvincesQuest` - Maintain provinces in Develop order for months
-`MobilizeProvincesQuest` - Maintain provinces in Mobilize order for months
-`RestProvinceQuest` - Use the Rest command in a specific province
### Reconnaissance Quests
-`ReconProvincesQuest` - Reconnoiter a number of provinces
-`ReconSpecificProvincesQuest` - Reconnoiter specific provinces
### Economic Quests
-`TotalDevelopmentQuest` - Achieve total development level in a province
-`WealthQuest` - Accumulate gold and food
-`SpendOnFeastsQuest` - Spend gold on feasts
-`SendSuppliesQuest` - Send food to a specific province
-`RepairDevastationQuest` - Repair devastation
### Special Event Quests
-`SuppressRiotByForceQuest` - Suppress a riot by force
-`FightBeastsAloneQuest` - Fight beasts alone
-`StartBlizzardQuest` - Start a blizzard in a province
-`StartEpidemicQuest` - Start an epidemic in a province
-`ApprehendOutlawQuest` - Apprehend an outlaw hero
### Miscellaneous
-`BattalionDiversityQuest` - Have diverse battalion types
-`SwearBrotherhoodWithHeroQuest` - Swear brotherhood with a specific hero
-`BetrayAllyQuest` - Betray an allied faction
## Implementation Details
The quest completion logic is located in:
-`FulfillQuestsCommandSelector.scala` - Main entry point, iterates through choosers
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.