* Add documentation for renaming a province
Documents the files that need to be modified when renaming a province
and explains how province names flow from the server to the client.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Rename Fluria to East Faluria and Faluria to West Faluria
Updated all province name references:
- province_map.tsv: Both province rows and all neighbor references
- MapDescription.scala: LLM prompt geography description
- centroids.json: Client map rendering metadata
- RuntimeValidatorTest.scala: Test strings
- Renamed hex map files: Fluria.e0mj -> East_Faluria.e0mj,
Faluria.e0mj -> West_Faluria.e0mj
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Override (faction hover/notification): always black — visible against
any faction color
- Targeted (right-click): deep purple-red — distinct from red faction
- Selected (left-click): luminance-adaptive — dark factions get white
highlight, bright factions get black
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When hovering a faction in the Factions table or viewing a faction
notification, borders between provinces of the same faction are now
suppressed so the faction's territory appears as one contiguous
highlighted region.
Adds a cross-province ID map (computed once at startup alongside the
distance map) that stores which province is on the other side of each
pixel's nearest border, plus a 256x1 highlight group lookup texture
that the shader checks to skip highlight borders between same-group
provinces.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The tile PNGs in static/tiles/ are LFS-tracked but the Docker build
workflow checks out with lfs: false, embedding LFS pointer files
instead of actual images. Add a targeted git lfs pull for just the
admin server tile images (~736KB) before the Bazel build.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The selectCaller method was load-balancing across all available LLM
providers by selecting whichever caller had the fewest in-flight
requests. This caused requests to be distributed roughly evenly across
Gemini, Claude, and OpenAI instead of preferring the primary provider
(Gemini) and only falling back on capacity limits.
Now selectCaller first tries to find a caller from the primary provider
with available capacity, and only falls back to other providers when the
primary has no capacity.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Make province borders resolution-independent using fwidth()
Use screen-space UV derivatives to compute texel-to-pixel ratio in the
shader, so border widths specified in screen pixels remain visually
consistent regardless of render resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Reduce default border width from 0.7 to 0.5 screen pixels
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The go:embed directive used `static/*` which only matches direct
children. Change to `all:static` so subdirectories like `static/tiles/`
are included in the binary.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Copy representative PNG tile images from the Unity assets into the admin
server static directory and render them clipped to hex shapes on the
canvas. Falls back to flat CSS colors if an image fails to load.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Update province border width from 0.35 to 0.7 and adjust UI element
positioning in the Gameplay scene.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reassign 1,242 pixels from a small disconnected blob of Parnia (pid 41)
in the southwest to Ingia (pid 32), which surrounds it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Hand-tune province label positions for border-adjacent provinces
Manual adjustments on top of the algorithmic positioning for provinces
where labels were too close to borders:
- Berkorszag: +30px right
- Kezonoria: +20px right, +5px up
- Grytrand: +15px right
- Oscasland: +20px up
- Oryslia: 10px left
- Pozia: 5px up
- Chia: 5px right
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Further tweak Kezonoria and Pozia label positions
Kezonoria: +25px up total, Pozia: +15px up total (increasing y = up in
Unity's coordinate system).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace Gaussian-weighted blend with simpler, more predictable approach:
find all pixels with >= 70% of max possible edge clearance, then pick the
one closest to the geometric centroid. This keeps labels well inside province
boundaries while staying visually centered. Most labels barely move from
their current positions; labels near narrow borders (Kezonoria, Grytrand)
get nudged further inside.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add .e0mj map files as a layer in the admin server Docker image
and pass --maps-dir=/app/maps to enable the web-based map editor
added in #6186.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a game is rewound past a battle, truncateTo() cleared in-memory
shardokHistory but left .e0s files on disk. On restart, these orphaned
files got lazy-loaded when the game state still referenced the battle,
causing Eagle to re-send stale battle data to Shardok.
This caused a production crash loop where Eagle kept sending an old
Motcia battle (with pre-fix map data baked into the .e0s file) on
every restart, even after the map fix was deployed.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace geometric centroid (mean of pixels) with a distance-transform-based
approach that finds the point inside each province maximally distant from
edges, weighted by proximity to the visual center. This fixes labels that
bled into neighboring provinces or extended over water (Berkorszag, Chia,
Pozia).
Also adds --update-centroids mode to generate_map.py for re-running label
positioning without losing hand-tuned styling fields.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Uses Docker Engine API over mounted Unix socket to restart the active
Eagle container directly from the admin UI. Includes confirmation dialog
and graceful degradation when Docker socket is unavailable.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the Unity-based Shardok map editor with a browser-based editor
in the admin console. Supports terrain painting, modifiers (castle/bridge),
starting positions, weather config, undo/redo, and file download.
Enabled via --maps-dir flag pointing to the .e0mj directory.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Useful when Eagle needs to reload game state from persistence
(e.g., after a rewind) without a full blue-green deployment.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Validates that no map has attacker starting positions that overlap
with defender starting positions. This overlap causes the AI's
GameStateGuesser to misplace guessed defender units, leading to
command count mismatches during SET_UP.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Motcia's attacker starting position index 0 had 7 of 10 positions
overlapping with defender starting positions. This caused the AI's
GameStateGuesser to place a guessed defender unit on an attacker
starting position during SET_UP, blocking it and creating a command
count mismatch (27 vs 30) that crashed the server in a loop.
Moved attacker index 0 positions east into the plains (columns 3-5)
away from the defender's castle compound at (1,0)/(2,0)/(2,1).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The guessed state produces 27 PLACE_UNIT_COMMANDs vs 30 real during SET_UP.
This adds target coordinates to the command dump, identifies the specific
missing positions per unit, checks what the guessed state thinks is occupying
them, and dumps all guessed state units for full visibility.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The assert at ShardokAIClient.cpp:174 was crashing the server with
no diagnostic info when guessed and real command counts diverged.
Now logs both counts, player, round, and a side-by-side dump of
each command (type + actor unit) before throwing an exception that
the server can catch and recover from.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The HeroGenerator was being passed to EngineImpl before heroes were
consumed from it during game creation. The randomHeroes() method
consumed heroes from the generator pool but then discarded the updated
generator with .map(_._1). This meant the engine started with a
generator that still contained heroes that had already been spawned.
When new heroes spawned during gameplay, they would duplicate the
heroes already in the game.
Fix: Thread the updated HeroGenerator through the game creation flow
and pass the final generator (after all initial heroes are consumed)
to createEngine().
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a user enters a game via StreamGameRequest, they were remaining in
the lobbyUsers map, causing unnecessary lobby updates to be broadcast to
them and triggering repeated disk reads of games.e0es. Now users are
removed from the lobby when they start streaming a game. They are re-added
when they send EnterLobbyRequest upon returning to the lobby screen.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Change tutorial reinforcements to appear after round 5
Adjusts timing for better tutorial pacing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix tutorial reinforcement hero ID verification
When tutorial reinforcements cause a battle to end, the verification in
ResolveBattleAction was failing because it expected only the originally
sent hero IDs but received the reinforcement hero IDs (100, 101, 102) as
well. This adds reinforcementHeroIds to ShardokBattle and includes them
in the expected heroes set during verification.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Apply a subtle cool blue-gray tint to province colors when a blizzard
event is active, so white snowflakes are visible on light provinces.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Increase snowEmitRate from 15 to 45 (3x) in both the prefab and code
default, and raise maxParticles from 300 to 900 to accommodate the
higher emission rate.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When clicking a reserve unit during combat, _selectedGridIndex was
cleared unconditionally, losing track of which hex unit is the actor
for the Reinforce command. Now only clear the selection during setup
phase. Also add null guards in RedrawCommandOverlays to prevent NRE
when SelectedCoords or the unit at those coords is null.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Smooth province borders and reduce default border width
Switch distance-to-border computation from Chebyshev (L∞) to Chamfer
(Euclidean approximation) using √2 diagonal weights, producing smooth
circular contours instead of blocky square stepping. Enable bilinear
texture filtering for additional sub-texel smoothing. Reduce default
border width from 2.0 to 1.5 texels for a cleaner look.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Reduce default border width to 0.75 texels
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make default borders much thinner (account for double-sided drawing)
Both provinces on each side of a boundary draw their own border, so the
visual width is ~2x the configured value. Reduce width to 0.35 texels
(~0.7 total) and tighten the AA ramp from 0.75 to 0.5 for a true
hairline border.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix border width: update serialized scene value to 0.35
The scene's serialized defaultBorderWidth (2.0) was overriding the C#
default at runtime via Awake(). Update the Gameplay.unity scene to match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Warp province boundaries for organic look
Apply domain warping to rawGray.gz.bytes using Gaussian-filtered noise
displacement fields. Only land pixels near province boundaries are
affected; ocean and coastline pixels are preserved. Adds a reusable
warp_boundaries.py script with configurable amplitude, scale, and seed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Rubber-band spawned heroes (for struggling factions) now appear as
Residents instead of Travelers. This makes them more likely to stay
in the province and be recruited by the faction they were spawned to
help. Added optional unaffiliatedHeroType parameter to
UnaffiliatedHeroAppearedAction to support this.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instantiate was called with worldPositionStays=true, causing items to
retain their prefab world position instead of being laid out by the
parent layout group. Changed to false to match running/waiting games.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Sadar Rakon's battalions increased by 30% size
- Training increased from 60 to 80
- Makes the tutorial battle more challenging
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instead of hardcoding specific (row, column) coordinates in the proto
from Eagle, pass an attacker_starting_position_index that Shardok
resolves against the hex map. Uses index 6 for the tutorial battle
on Onmaa.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add PENDING_REINFORCEMENT units to game state during tutorial setup
Tutorial reinforcement units (defined in TutorialBattleConfig events)
were never added to the initial game state, so ExecuteReinforcementsAction
could not find any units to activate. Now SetUpController() extracts
CommonUnit protos from reinforcement events, converts them via ConvertUnit(),
and adds them with PENDING_REINFORCEMENT status so they exist in the game
state when the trigger fires.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Exclude PENDING_REINFORCEMENT units from PlacedUnitsForPlayer
PlacedUnitsForPlayer returned all non-RESERVE_UNIT units, which
incorrectly included PENDING_REINFORCEMENT units. This caused them
to appear during the initial unit placement phase. Now they are
excluded alongside RESERVE_UNIT so they remain invisible until
activated by the tutorial trigger.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Preserve PENDING_REINFORCEMENT status in SetupInitialGameState
SetupInitialGameState was overwriting every unit's status to
RESERVE_UNIT, which erased the PENDING_REINFORCEMENT status set
during tutorial game setup. Now units that are already marked
PENDING_REINFORCEMENT retain their status.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The games.e0es file stores factionLeaderCache with resolved names,
but gamesForWithoutBlocking() was ignoring this cache and always
showing "[Loading...]" for unloaded games. Now uses the cached
leader info when available, only falling back to placeholder when
the cache is missing or has no resolved name.
This eliminates the "Loading..." delay in the lobby after deployment.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Stage proto outputs in a temp directory and rsync with --checksum
to preserve timestamps on unchanged files. Previously, rm -rf on
the output directory forced Unity to reimport all protos (~7 min
of script recompilation) even when nothing changed.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Replace province fill-color strobing with border-width strobing
Province highlights (selected, targeted, commands available) now strobe
border thickness instead of fill color, making them visible on small
provinces and dark faction colors. A runtime distance-to-border texture
enables variable-width borders with one extra texture sample per fragment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Restore fill-color strobing, use black border for selection, remove map_borders.png
- Restore fill-color strobing alongside border-width strobing
- Change selected province border color from white to black for contrast
- Remove map_borders.png and make ClickDetector overlay transparent
(borders now rendered by shader)
- Rename MapBWImage to ClickDetector to reflect its purpose
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace the prod/QA environment dropdowns (connection panel and lobby) with
simple "Connect to QA" buttons that connect directly to localhost:40032 over
plain HTTP with no auth. Buttons are editor-only and hidden in builds.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add charge icon sprite asset and use inline in dialogue
Create TMP sprite asset for the charge horse icon and add it to
the fallback chain. Update charge tutorial instruction text to
show the icon inline instead of describing it as "horse icon".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add profession icon sprite assets for dialogue use
Create TMP sprite assets for Mage, Necromancer, Engineer, Paladin,
Ranger, and Champion profession icons. Added to the fallback chain
on the primary dragoons sprite asset. Available as
<sprite name="Mage"> etc. in rich text.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When --skip-auth is passed to Eagle server, all requests are
auto-authenticated as 'local-dev' user with admin privileges.
This bypasses JWT validation entirely for local testing.
WARNING is printed to stdout when this flag is active.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Vigor (99) was higher than constitution (91), causing a validation
error during battle resolution. Set vigor to match constitution.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Scale battalion type sprites from 1.0 to 1.25 for better readability
in dialogue instruction text.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add inline battalion type icons to dialogue instruction text
Build a TMP_SpriteAsset at runtime from EagleCommonTextures battalion
textures, enabling <sprite name="LightInfantry"> etc. in rich text.
The unit types panel now shows each battalion's icon next to its name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix read-only spriteCharacterTable/spriteGlyphTable assignment
Use existing lists via .Add() instead of assigning new ones — these
properties are read-only in TMP 3.x.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix non-readable texture error in sprite asset builder
Copy battalion textures to readable copies via RenderTexture blit
before packing into atlas, since the originals may not have Read/Write
enabled in their import settings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove runtime TMP_SpriteAsset builder in favor of editor-created asset
The runtime approach fought TMP's initialization — property getters
trigger UpgradeSpriteAsset() which crashes on freshly created instances.
A pre-built sprite asset assigned in the Inspector is the intended workflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Only trigger Start Fire tutorial when target has an enemy unit
The start_fire_available event was firing whenever the command existed,
even when targeting empty hexes. Now checks that the command's target
cell contains a non-friendly unit before triggering the dialogue.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
CANCELLED status is transient and will be handled by reconnection logic,
so there's no need to report it to Sentry alongside UNAVAILABLE.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add inline battalion type icons to dialogue instruction text
Build a TMP_SpriteAsset at runtime from EagleCommonTextures battalion
textures, enabling <sprite name="LightInfantry"> etc. in rich text.
The unit types panel now shows each battalion's icon next to its name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix read-only spriteCharacterTable/spriteGlyphTable assignment
Use existing lists via .Add() instead of assigning new ones — these
properties are read-only in TMP 3.x.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix non-readable texture error in sprite asset builder
Copy battalion textures to readable copies via RenderTexture blit
before packing into atlas, since the originals may not have Read/Write
enabled in their import settings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove runtime TMP_SpriteAsset builder in favor of editor-created asset
The runtime approach fought TMP's initialization — property getters
trigger UpgradeSpriteAsset() which crashes on freshly created instances.
A pre-built sprite asset assigned in the Inspector is the intended workflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add inspector-assigned sprite asset for dialogue instruction icons
Replace runtime sprite builder with a TMP_SpriteAsset field on
DialoguePanelController. Sprite assets are created in the editor
and wired via Inspector — the intended TMP workflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix sprite vertical alignment and adjust panel background color
Increase m_HorizontalBearingY from 128 to 230 on all sprite assets
so icons align with adjacent text instead of sitting below it.
Update instruction panel background for better icon readability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Changed agility from 70 to 75 (minimum threshold for archery).
Updated stat sum from 430 to 435.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add battle tutorial dialogues with Old Marek
Narrative dialogue panels for the Shardok tactical combat tutorial:
- Placement phase: enemy forces (red text), unit types, placement instructions
- Battle running: victory conditions, hold position / End Turn advice
- Archery: purple outlines on targets, longbow effectiveness vs armor
- Melee: Shift-click and Melee button instructions
- Charge: horse icon on move destinations, move-and-charge explanation
- Thunderstorm: prevents archery, extinguishes fires
- Start Fire: setting hexes ablaze when adjacent to enemy
Supporting changes:
- Panel positioning (top/default) via panelPosition field on DialogueScript
- Completion tracking prevents dialogues from re-triggering
- New trigger events: archery_available, melee_available, start_fire_available, thunderstorm
- Register CommitButton and EndTurnButton as tutorial targets
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Clean up repetitive dialogue text in battle opening
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The ReinforcementsAction was missing playerId and startingPositions, so
the C++ side had no positions to place units at. Set playerId to 1
(defender) and use 3 positions from attacker starting position list 7
on the Onmaa map.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>