Instead of replacing the Viper's animator controller (which broke bone
bindings for cross-FBX Generic animations), keep the original
Viper_anim_controller and add configurable state name overrides to
AnimalEffect. SnakeEffect now maps walk→glide and eat→hiss so the
Viper's native state names are used without controller replacement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Viper model's built-in animator controller uses non-standard state
names (glide/hiss instead of walk/eat) and has auto-transitions that
conflict with AnimalEffect's direct Animator.Play() calls. Create a
SnakeMapAnims controller mapping the standard state names to the correct
Viper clips: idle->Viper_Idle, walk->Viper_Glide, eat->Viper_Hissing,
attack->Viper_Attack.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each beast has TSV stats, FBX animation converted to Generic, animator
controller with idle/walk/eat/attack states, effect prefab with tuned
spawn parameters, and Addressables registration. Models sourced from
Africa Animals Pack V1 (black panther, rhinoceros), Africa Animals Pack
Low Poly V2 (gorilla, hyena, leopard, warthog), and Australia Animals
Pack V1 (emu, tasmanian devil, kangaroo).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add lion and velociraptor beast effects
Add animated lion (male + lioness) and velociraptor province beast effects
with Generic animation controllers. Includes only the specific model files
needed from Africa Animals Pack V1 and Dino Pack Low Poly V1.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix lioness animation with per-prefab animator controller
The lioness wasn't animating because the shared LionMapAnims controller
references clips from the Lion FBX, whose bone paths (LionSkeleton/...)
don't match the Lioness model (LionessSkeleton/...). Create a separate
LionessMapAnims controller referencing Lioness FBX clips and use the
per-prefab animatorControllers array to assign each model its own
controller. Also add the standalone Lion/Lioness prefabs that LionEffect
references.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add BLOW_BRIDGE_COMMAND (38), BLOW_BRIDGE (80), and
BLOW_BRIDGE_FAILED (81) enum values for the upcoming
Blow Bridge engineer command.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Import the Africa Animals Pack and wire up hippopotamus as an AnimalEffect
beast. Switch the hippo FBX import to Generic animation type for Mecanim
compatibility and enable loopTime on looping clips. AnimalEffect now strips
legacy Animation, Rigidbody, and Collider components and adds an Animator
when prefabs lack one, supporting asset packs that ship with legacy setups.
Document the per-animal setup workflow for future Africa Animals Pack imports.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add animated windsock wind direction & speed indicator to Shardok battle view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Redesign wind indicator as animated arrow, remove weather from round info
Replace windsock (pole + sock) with a simpler arrow that points in the
wind direction, shakes with layered sine waves for organic feel, and
grows thicker at higher speeds. Fix NW/SW direction mapping swap. Remove
weather text from round info label since visual indicators now convey
this. Show "No Wind" in speed label when calm.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Remove GraphicRaycaster from Weather Canvas (no interactive elements)
- Set raycastTarget=false on Odds Panel and Button Tooltip images
(display-only elements that don't need click handling)
- Use Color.clear instead of null when clearing bg highlights in HexGrid
to preserve mesh fill triangles
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Use ID-based lookup instead of object equality when diffing unaffiliated
heroes. The old code used Vector.contains (full object equality), so any
field change (profession, recruitmentInfo, etc.) caused the hero to
appear in both removed and added lists. If ordering or batching led to
the add arriving before the remove was processed, the client threw
"Duplicate unaffiliated hero" InvalidOperationException.
Now builds a Map by heroId for comparison, which correctly handles all
three cases: truly removed, truly added, and changed-in-place (emitted
as remove+add by ID so the client replaces the old entry).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
BattleFilter was setting shardokGameId to None for restricted
non-participant observers. This broke GameStateViewDiffer's
removedBattleIds computation, which uses flatMap(_.shardokGameId)
and silently drops None entries. The result: battle animations
were added to the client but could never be removed.
myPlayerId = None already prevents non-participants from joining
the Shardok game, so hiding the game ID served no purpose and
broke battle removal tracking.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a Shardok game ends (victory/defeat), the game model was
removed from ShardokGameModels but the battle was left in
ShardokBattles (OutstandingBattles). The only other removal path
was RemovedBattleIds in Eagle diffs, which may arrive late or not
at all for auto-resolved battles the player never entered.
This left the battle animation running on the strategic map
indefinitely after the battle ended.
Fix: also remove from ShardokBattles when processing ended games
in the Shardok update path.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
SwapModel returns early when both old and new model have available
commands, skipping mapController.Model assignment and all province
effect updates. This meant battle animations persisted after a
battle ended if commands were available at the time.
Fix: call mapController.UpdateBattleEffects before the early return
so battle start/end animations are always kept in sync with the
model, even when the full model swap is skipped.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a province changed hands after a battle, UpdateBattleEffects
identified the defender by looking up the current ruling faction of
the defender province. After conquest, the attacker's faction matched
the province owner and was skipped, while the old defender was
processed — with OriginProvinceIds containing the defender province
itself, producing key (Wichel, Wichel) and an animation at the
province center.
Fix: skip any origin that equals the defender province. This can
never produce a meaningful animation (midpoint = province center)
and eliminates the dependency on current province ownership for
defender identification.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The failure check treated "no longer allied" as a failure condition, but
that's the success condition. Now: failure only if the target faction no
longer exists. Also added a general fulfillment check so the quest succeeds
even if the inline check in BreakAllianceResolutionHelpers doesn't fire
(e.g. when the ambassador is imprisoned).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Non-involved players can no longer observe tactical Shardok combat. They
still see the battle-in-progress animation on the strategic map and learn
the outcome, but won't receive individual Shardok actions or be offered
the observation UI. Allied non-participants can still observe.
Controlled by the RestrictBattleObservation setting (default 1/on). Set
to 0 in the admin console to revert to the old behavior.
Also syncs settings.tsv with LLM settings that were previously only in
BUILD.bazel, and fixes the settings generator to include string_setting
dep and correct battalion_type_loader deps.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The settings generator had two issues:
- Missing string_setting dep when String-type settings exist
- Wrong battalion_type_loader deps (proto target instead of model state)
Also syncs settings.tsv with LLM settings (llmProvider, openAiModelName,
claudeModelName, geminiModelName) that were previously only manually
added to BUILD.bazel, and updates the openai_model_name target to match
the generator's snake_case convention.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Set cannotBecomeOutlaw for tutorial opening battle
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix: only set cannotBecomeOutlaw for the first battle, not all tutorial battles
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Instead of fixed directions (March slides right, Send Supplies launches
upward), both animations now compute the direction to the destination
province from the game state diff and animate toward it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add -500 trust toward the King (faction 2) for both Bridget (faction 4)
and Hedrick (faction 5) so the King's AI never sends them invitations.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add cannot_become_outlaw flag to per-faction battle request
When set on a faction's PlayerSetupInfo, the BecomeOutlaw command
is suppressed for that faction's units. Default false preserves
existing behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove default values for cannotBecomeOutlaw, require explicit specification
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tutorial Mobilization phase start logic (#6478)
When starting a tutorial at Mobilization or EagleAppears, fast-forward
the game state past the opening battle to a plausible mid-game state
where Sadar controls two provinces and King's satellites are vacated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Replace dynamic tutorial phase setup with JSON resource loading
Add an extractor tool and loader to store pre-computed tutorial phase
transition results as JSON resources instead of computing them at
runtime from game state. This eliminates fragile dependencies on
knowing exact hero IDs and post-battle state deltas.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Populate expansion_results.json with battle resolution data
Extracted from a tutorial playthrough: actions 4-7 covering battle
resolution, province held, Tarn disappearance, and end of resolution
phase. Actions 0-3 (GAME_START through FOOD_CONSUMED) are already
produced by the initial game creation flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Pretty-print expansion_results.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Increase Sadar's three starting battalions by 15% (460→529, 270→311,
346→398) to give the player a stronger opening battle position.
Raise Luke the Prank-tricker's Sadar faction bias from 80 to 110.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Increase all spatial fields (icon sizes, distances, heights, spreads,
radii) by approximately 1.3x across all province action animation types.
Durations, counts, angles, and colors are unchanged.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Match the Send Supplies behavior: goldLeftLabel and foodLeftLabel turn
red when the amount remaining in the origin province would exceed its
gold or food capacity.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Validates all maps to ensure starting positions are not placed on
impassable terrain (water, river, mountain) and that attacker starting
position sets don't overlap with each other.
Kojaria currently fails both checks and needs a map editor fix.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When an AI unit flees and ends the battle, the GameStateViewDiff
removes the unit from UnitsById before ModelUpdated processes the
history. The sound-playing code required the actor to be found in
UnitsById to determine animation coordinates, silently skipping
the entire sound block when the lookup failed.
Add a fallback else branch that plays the sound without animation
when the actor unit has been removed from the model.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The previous "battle_shout" sound felt more like rioting than
feasting. Replace with a group victory cry from freesound.org
(chripei, CC BY 4.0) that better conveys celebration.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Stop label updates from clearing hex background highlights
SetUnitInfoLabels and SetOverlayLabel were calling SetCellBaseColor
(which sets Geometry.Color = Color.clear) and hexMesh.Triangulate on
every invocation. These methods manage TMP_Text labels, not hex
geometry, so the SetCellBaseColor and Triangulate calls were
unnecessary. Removing them eliminates O(N²) Triangulate calls per
selection change (one per cell, each iterating all cells).
ClearOverlays now explicitly resets Geometry.Color for cells that had
a BackgroundHighlightColor and does a single Triangulate when needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Defer hex background highlight updates to HexGrid.Update for atomic mesh rendering
ClearOverlays was eagerly triangulating hexMesh (clearing background
highlights) while overlayMesh borders were only updated in Update().
This caused the two meshes to render out of sync for one frame when
Update order put HexGrid before MouseHandler. Now both meshes are
updated atomically in Update(), and ClearOverlays just clears the
data. Also reduces hexMesh.Triangulate calls from O(N) per overlay
cell per frame to at most one.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix overlay label/image flash by deferring all cleanup to Update
ClearOverlays was eagerly clearing labels and follow-up images while
the overlay border mesh was only updated in HexGrid.Update(). When
Unity's script execution order put HexGrid.Update before
MouseHandler.Update (which changed after the Shardok prefab
extraction in #6485), the labels/images were cleared mid-frame but
not re-set until next frame — causing a visible one-frame flash on
every unit selection.
Now ClearOverlays just clears the data list. Update() owns all visual
state: it clears stale labels, images, and background highlights from
the previous frame, sets new ones from current overlayCells, and
triangulates both meshes — all atomically regardless of script order.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Animation objects are parented to mapContainer, not the animator. When
the Map deactivates mid-animation (e.g. entering a battle), coroutines
die but the partly-faded sprites linger. Track all objects created by
CreateUIObject and destroy any survivors in OnDisable.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add elephant/mammoth beast effect using Animal Pack Deluxe v2
Wire up African and Indian elephant prefabs from the newly purchased
Animal Pack Deluxe v2 as a proper beast effect. Mammoths reuse the
same elephant models. Includes a custom ElephantMapAnims controller
to remap v2 animation state names to AnimalEffect's expected states.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix elephant animation: use v2 pack's own controller, fix prefab refs
- Use the v2 pack's African_elephant_anim_controller directly instead
of our custom ElephantMapAnims (which couldn't resolve FBX clips)
- Fix animalPrefabs fileIDs to match actual root GameObjects
- Regenerate animation FBX .meta files for Unity 6 compatibility
The v2 controller's state names don't fully match AnimalEffect's
expected names (idle A/attack A vs idle/attack), so animations are
partial. Next step: remap state names to get all 4 animations working.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Rename v2 controller states to match AnimalEffect expectations
Rename 'idle A ' → 'idle' and 'attack A' → 'attack' in the v2 pack's
African_elephant_anim_controller so all 4 animation states work with
AnimalEffect's Animator.Play() calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Action results arriving from the server while the Map game object is
inactive (e.g. during a battle) are queued and replayed 1s after the
Map reactivates, so the player sees what happened while they were away.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The full AICommandFilter was only applied during lookahead (inside
FindBestCommand), not at the root level of IterativeSearch. This
caused the AI to repeatedly choose METEOR_TARGET_COMMAND because the
re-target filter never ran on the top-level command list.
Adds FilterLoopingCommands — a minimal filter that only removes
genuinely never-useful commands (meteor re-target/cancel when a target
is already set). The full aggressive filter stays in lookahead only,
where pruning is a performance optimization. Applying the full filter
at the root could miss moves that look bad locally but prove
worthwhile with deeper search.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
METEOR_START uses ActionCost::usesAll which drains AP to 0. The
MeteorTargetCommandFactory then unconditionally generates a
METEOR_CANCEL command (cost 1 AP) for the mage in TARGET state.
When the AI evaluates this command, MutatingSpendActionPoints throws
because 1 > 0.
Two fixes:
1. Set meteorCancelActionPointCost to 0, matching #6454's fix for
meteorTargetActionPointCost.
2. Filter out meteor re-target/cancel in AICommandFilter when the
mage already has a target. Re-targeting exists for human misclick
correction; the AI commits to its first choice and could otherwise
loop endlessly re-targeting at 0 AP cost.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Mammoth had human-bandit-like stats (likelihood 0.10, power 1.5,
economy devastation) instead of large-animal stats. Update to be
rarer than elephants with appropriate power, count, and devastation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Extract Command Panel into prefab
Move the Command Panel hierarchy (37 command sub-panels) from
Gameplay.unity into its own prefab at Assets/Eagle/. This is the
largest single component in the scene at ~126K lines. Scene-level
cross-references (eagleGameController, connectionHandler, hexGrid,
shardokContainer, etc.) are preserved as PrefabInstance overrides.
Reduces Gameplay.unity from ~232K to ~102K lines (56% reduction).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Extract 35 command sub-panels into individual prefabs
Break down Command Panel.prefab (126K lines) into 35 individual
prefabs under Assets/Eagle/CommandPanels/. Command Panel itself
becomes a thin container (~6K lines) with PrefabInstance references.
Largest sub-panel (March Panel) is 12.7K lines; smallest are ~230
lines. All are independently editable and produce focused diffs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Move the Shardok Container hierarchy (Shardok Canvas + Weather Canvas)
from Gameplay.unity into its own prefab at Assets/Shardok/. External
field references (mainCamera, eagleCanvas, soundManager, audioClipSource)
are preserved as scene-level PrefabInstance overrides.
Also adds UNITY_POST_PROCESSING_STACK_V2 scripting define symbols.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates 3 duplicate instances by adding a static Instance property,
replacing all GetComponentInParent lookups and serialized field references
with EagleCommonTextures.Instance. Removes the duplicate components from
ConnectionCanvas, Shardok Container, and Eagle Canvas in the scene, and
moves the single remaining instance to Main Camera so it's guaranteed to
have Awake() called regardless of which canvases are active.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Extract Custom Battle Panel into prefab
Remove baked-in Custom Unit Row instances from the prefab since
EventBasedTable.Awake() destroys them and recreates from rowPrefab
at runtime. Also remove stale SimpleFileBrowser references from
eagle0.sln.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Convert Custom Battle Panel prefab from LFS to regular git
This prefab is actively edited line-by-line and benefits from normal
git diff/blame/merge. LFS is better for opaque binary assets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add missing Unity .meta files, HoneyBadger import artifacts, and fix_import.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove HoneyBadger import artifacts and gitignore FBX auto-generated files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Remove the SimpleFileBrowser plugin and its usage in CustomBattleHandler
(custom map loading and game request loading via file dialogs). Remove
the Custom Map Toggle, Load Button from the Custom Battle Panel, and
the obsolete Tutorial Lite button from the Lobby Panel prefab.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The Settings Panel prefab starts inactive, so Start() never runs until
the user opens it. This caused persisted values (province label font
size, audio volumes, tooltip behavior) to not be applied until Escape
was pressed, producing a visible font resize.
Split initialization: ApplyPersistedSettings() loads PlayerPrefs and
applies to game systems early via SettingsKeyListener.Start() on the
always-active PersistentCanvas. The deferred Start() just syncs UI
sliders/toggles to match the already-applied values.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Replace tutorial buttons with phase dropdown in ConnectionHandler
Replace the two separate tutorial buttons (tutorialButton, skipBattleTutorialButton)
with a TMP_Dropdown + Button combo so any tutorial phase can be selected from the lobby
without adding more buttons for each new phase.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix C# TutorialPhase enum names to match protobuf codegen
C# protobuf strips the common prefix, so the enum values are
TutorialPhase.OpeningBattle (not TutorialPhaseOpeningBattle), etc.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire tutorial phase dropdown and start button in Gameplay scene
Add missing using directive for Net.Eagle0.Eagle.Common namespace
and re-wire tutorialPhaseDropdown and startTutorialButton to the
Lobby Panel prefab instance.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reduces Gameplay.unity by ~13k lines by extracting three connection UI
panels into standalone prefabs with scene-level overrides for callbacks
that reference the scene-level ConnectionHandler.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Longbowmen can now target enemies at range 3 when shooting in the
direction the wind is blowing (±1 hex sector), provided wind speed
meets the configurable `longbowWindBonusRangeMinSpeed` threshold
(default 20 mph). The 20 mph threshold reflects the point where wind
meaningfully extends arrow range, below the 35 mph blizzard threshold
where archery is already disabled.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Replace tutorial booleans with TutorialPhase in CreateGameRequest
Replace `is_tutorial` + `skip_tutorial_battle` booleans with a single
`TutorialPhase tutorial_start_phase` field, making the API extensible
for future tutorial phases (Mobilization, etc.) without adding more
boolean flags.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add service visibility to tutorial_phase_converter BUILD target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Keep old tutorial boolean fields in CreateGameRequest proto
Keep is_tutorial and skip_tutorial_battle fields so the Unity build
doesn't break. They'll be reserved in the client-side PR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Extract Settings Panel into prefab to reduce Gameplay.unity size
The Settings Panel hierarchy (~11k lines) was inline in the scene file,
making Gameplay.unity harder to diff and merge. Extract it into a
standalone prefab with scene-level overrides for cross-references.
Fix 15 event callback targets that Unity zeroed out during extraction
(sliders, toggles, buttons all needed re-pointing to the
SettingsPanelController inside the prefab).
Add SettingsKeyListener on PersistentCanvas to handle Escape/F11 keys,
since the SettingsPanelController now lives on the (sometimes inactive)
prefab root and can't run Update() when hidden.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove scene overrides that zeroed out prefab event callbacks
The prefab instance in Gameplay.unity had overrides that set 6 event
callback targets to null, overriding the correct values in the prefab.
Remove these overrides so the prefab's own targets take effect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tiger and lion beast effects using ithappy Animals FREE pack
Import the free ithappy Animals FREE asset pack and create tiger/lion
beast effects. The tiger model is reused at 1.7x scale for lion.
A custom TigerMapAnims controller maps ithappy's idle/walk/idle_rare/run
clips to AnimalEffect's expected idle/walk/eat/attack state names.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove lion effect (scaled tiger is not convincing)
Lion falls back to generic vultures until a proper model is found.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix ithappy files stored as LFS objects breaking Unity import
The broad ithappy/** LFS rule was storing text YAML files (.controller,
.mat, .prefab, .anim, etc.) as LFS objects, causing Unity to fail with
"File may be corrupted" errors. Remove the rule and re-add files so text
files are stored as regular git objects while binaries (.fbx, .png) are
still covered by per-extension LFS rules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Strip third-party scripts/physics from animal prefabs on spawn
ithappy Tiger_001 prefab includes CharacterController, CreatureMover,
and MovePlayerInput components that conflict with AnimalEffect's own
movement system. DestroyImmediate these after instantiation to prevent
"CharacterController.Move called on inactive controller" errors.
Also wires up tigerEffectPrefab in Gameplay.unity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move beast effect prefabs to Addressables for CDN delivery (#6373)
Replace 33 serialized GameObject fields in ProvinceBeastsController with
Addressables.LoadAssetsAsync label-based loading ("beast-effects").
Prefabs stay in Assets/Eagle/Effects/ but are now delivered via CDN at
https://assets.eagle0.net/addressables/[BuildTarget] instead of being
bundled in the app binary. This removes ~80-100 MB of 3D models,
textures, and animations from the initial app download.
Loading strategy:
- Eager load in Start() (last priority — after music)
- Game state that arrives before loading completes is stashed and
applied when ready
- If loading fails (offline, no cache), beasts simply don't render
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tiger and elephant beast effects using Animal Pack Deluxe
Route "tiger" and "elephant" beast names to dedicated TigerEffect and
ElephantEffect prefabs in ProvinceBeastsController. Prefabs to be wired
up in the Unity editor using existing Animal Pack Deluxe assets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Register TigerEffect in Addressable beast-effects group
The TigerEffect prefab was added in the add-tiger-effect branch but was
never registered in the Beast Effects Addressable group, so LookupPrefab
returned null at runtime. Also removes a duplicate "tiger" switch case
from the merge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix AnimalEffect component removal errors on spawn
Instantiate prefabs inactive to prevent CharacterController stepOffset
validation errors, and remove MonoBehaviours in reverse component order
to respect [RequireComponent] dependency chains before removing
CharacterController.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix ithappy Animals_FREE material to use Built-in Standard shader
The material shipped with a URP Lit shader reference, causing solid pink
rendering on Built-in render pipeline. Swap to Built-in Standard shader
and remove the URP version tracker MonoBehaviour.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix tiger animations by using standalone .anim clips
The ithappy Animals_FREE FBX was imported with avatarSetup: 0 (no
avatar), so its embedded animation clips had no bone binding data.
Switch TigerMapAnims.controller to reference the standalone .anim files
which have explicit transform path bindings. Also build a Generic avatar
at runtime for prefabs missing one, and set up the Animator before
activation so it initializes with correct avatar and controller.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix duplicate Start/LoadBeastEffects from rebase
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove unused ithappy Animals_FREE assets
Only the tiger model, texture, material, and animations are needed.
Removes all other animals (chicken, deer, dog, horse, kitty, penguin),
demo scenes, skyboxes, scripts, floor material, animation controllers,
and render pipeline conversion files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove CharacterController and deleted scripts from Tiger prefab
The prefab still referenced CreatureMover and MovePlayerInput scripts
that were deleted in the previous commit, causing missing script errors
in the Unity editor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update Tiger_001 prefab after Unity re-serialization
Unity re-serialized the prefab after we removed the CharacterController
and missing script components. This is the clean editor-saved version.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add battle-in-progress animation on strategic map
Display looping weapon-clash effects between defender and attacker origin
provinces while battles are active. Follows the ProvinceFestivalController
pattern for centroid loading and effect lifecycle management.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Load battle sound from sfx-combat addressables instead of inspector field
Use the "melee" clip from the "sfx-combat" addressable label, matching
the pattern used by ProvinceActionAnimator and SoundManager.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Tune execution animation duration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix battle effect: orientation, sound, two weapons, test provinces
- Fix upside-down weapons by flipping weapon2 Y-scale (mirroring)
- Play melee clip as one-shot at each impact instead of continuous loop
- Use two different weapon sprites (sword + axe) for visual variety
- Change test ContextMenu to use adjacent provinces 2 and 5
- Add Gameplay.unity scene wiring
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add arrow volleys, fix weapon orientation, fix audio pattern
- Add arrow volley animations flying between province centers with
parabolic arc, random direction, and archery sound
- Fix weapon rotation: per-weapon offsets (sword blade down = +90,
mace head up = -90) so both swing business-end first
- Fix arrow sprite rotation offset (-135 for strategic map UI space)
- Fix audio: move playback to controller using Action callbacks,
matching ProvinceActionAnimator pattern (same object owns AudioSource
and calls PlayOneShot)
- Load audio in OnEnable with Dictionary<string,AudioClip> like
ProvinceActionAnimator
- Add configurable volume and rotation offset inspector fields
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add meta files for new battle effect scripts and addressable settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Expose attacker army origin provinces in the ShardokBattlePlayerInfo view so the
Unity client can draw battle-in-progress animations between defender and attacker
origin provinces on the strategic map.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Split captured hero recruitment into distinct ActionResultTypes and add captured hero animations
HandleCapturedHeroesCommand previously used RecruitHeroes (21) for both successful
and failed captured hero recruitment, making them indistinguishable on the client.
Add CapturedHeroRecruited (162) and CapturedHeroRecruitmentRefused (163) so the
Unity client can play the correct animation for each outcome. Also add province
animations for all 6 captured hero dispensations (recruited, refused, imprisoned,
exiled, executed, returned).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add sound effects for captured hero animations
Map each captured hero ActionResultType to an appropriate sound clip,
reusing the same clips as the animations they share patterns with.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
HandleCapturedHeroesCommand previously used RecruitHeroes (21) for both successful
and failed captured hero recruitment, making them indistinguishable on the client.
Add CapturedHeroRecruited (162) and CapturedHeroRecruitmentRefused (163) so the
client can play the correct animation for each outcome.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix tutorial duplicate hero bug by changing Tarn from Fled to Outlawed
PR #6464 set fleeProvinceId to None for the tutorial attacker army, but
TutorialBattleAutoResolve still marked Tarn as Fled. With no flee
province, unitReturned() returned true for Fled, triggering the
shattered army path which created outlaws for ALL attacker heroes —
duplicating the heroes already created via defenderCP's newOutlaws.
Changing Tarn to Outlawed avoids the shattered army path entirely while
preserving the tutorial storyline: Tarn escapes capture and doesn't
appear in HandleCapturedHeroes, then EagleAppearsAction picks him up.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update TutorialTarnDisappearsAction to handle Outlawed status
With Tarn now marked as Outlawed (instead of Fled), he ends up as an
unaffiliated hero in the battle province rather than in an incoming
army to Nikemi. Update the disappears action to remove him from
unaffiliated heroes so he's properly hidden until EagleAppearsAction.
Also remove the now-dead fled/incoming-army logic and unused MovingArmy
import.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Set speakerImagePath directly in tutorial_strategic.json for John Ranil
and Elena Fyar instead of relying on dynamic resolution, which can fail
due to timing issues with the Eagle model.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tutorial indicator to lobby running game items
Add a tutorialIndicator GameObject field to RunningGameItem that is
shown/hidden based on the game_type from the server. Pass GameType
through from ConnectionHandler so the client can distinguish tutorial
games in the lobby list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire tutorial indicator in Running Game prefab
Connect the Tutorial TextMeshPro label to the tutorialIndicator
field on RunningGameItem so it shows/hides based on GameType.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds one-shot visual animations at province centers for all remaining
player commands: Rest, Train, OrganizeTroops, ArmTroops, RecruitHeroes,
Trade, OrdersIssued, SentSupplies, SuppressBeasts (success/fail),
Divined, March, SwearBrotherhood (success/fail), ControlWeather, Alms,
HeroGift, ApprehendOutlaw, Recon, ExileVassal, and StartEpidemic.
Each animation includes inspector-tunable parameters, a ContextMenu test
method, and a sound effect mapping wired through the actionSounds array.
21 audio clips added to the sfx-combat Addressables group.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Plumb the existing GameType enum (NORMAL/TUTORIAL) through to the lobby
response so clients can distinguish tutorial games from normal games.
- Add game_type field to RunningGame (persistence) and GameInfo (API) protos
- Add gameType to GamePlayerInfo case class and populate it in all 4
construction sites (gamesFor cached/full-load, gamesForWithoutBlocking
loaded/unloaded) plus save()
- Pass gameType through to GameInfo in EagleServiceImpl
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The tutorial attacking army had fleeProvinceId set to its origin province, which
allowed Tarn's forces to flee during the opening battle. Since canFleeHeroIds was
always Set.empty, the flee logic fell back to fleeProvinceId.isDefined (true).
Fix by setting fleeProvinceId = None for the tutorial army, and remove the unused
canFleeHeroIds field entirely from ShardokBattle, its proto, converter, and all
call sites.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add TutorialPhase to GameType ADT to track tutorial progression
Converts GameType from a simple Normal/Tutorial enum into an ADT where
Tutorial wraps a TutorialPhase (OpeningBattle, Expansion, Mobilization,
EagleAppears, Complete). This separates "what kind of game" from "what
phase of the tutorial", enabling phase-aware event logic and future
ability to start at a specific phase.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix C# tutorial check to match on Tutorial specifically
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use TUTORIAL_PHASE_ prefix for proto enum values
Follow Google's protobuf enum naming best practice of prefixing all
values with the enum name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Pale yellow/white particle chain connects necromancers to their
controlled undead units. Shimmers in and out when not actively
controlled this round, becomes solid translucent when controlled.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Increase destinationScale from 45 to 60 for a wider cone at the target.
Track the mage's meteor phase per beam and apply a 3x speed multiplier
to vibration and shimmer during the Cast phase.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add sound effect for meteor start animation
Map ActionType.MeteorStart to Fire Spelll 01 from the magic spells
sound pack so the meteor preparation animation has an accompanying
magic charging sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add sound effect for meteor target animation
Map ActionType.MeteorTarget to Fire Spelll 02 so the targeting
crosshairs animation also plays a fire magic sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
SetupLobbyUI ran at startup before login, so IsAdmin was stale.
Re-evaluate admin-gated button visibility in OnOAuthLoginSuccess.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a feast animation to ProvinceActionAnimator that spawns food items
(drumstick, beer, wine, grapes, cheese, bread) tossed into the air on
parabolic arcs with spin and fade. Plays a battle_shout cheering sound.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Skip auto-selecting mage unit that already has a meteor target
When a mage already has a CastTarget set, don't auto-select them
via SelectAppropriateDefaultCommand. The player can still manually
click the unit to re-target, but we no longer default to showing
commands for a unit that has already targeted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Find untargeted mage when multiple have MeteorTarget available
Instead of checking the first unit with MeteorTargetCommand and
skipping the entire command type if that unit already has a target,
search through all available commands of the type to find a unit
that hasn't targeted yet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Wire ProvinceActionAnimator in Gameplay scene
Add ProvinceActionAnimator component to the scene with Inspector
references and action sound configuration for Improve animations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update ProvinceActionAnimator scene wiring
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Log game ID, player ID, round, and stack trace to stderr before
re-throwing, so AI thread crashes are diagnosable without a debugger.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add province action animations to Eagle strategic map
Play one-shot animations at province centers when action results arrive.
Starting with the Improve action, which shows a hammer-strike animation
with sparks. Sound effects loaded from sfx-combat Addressables by name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix hammer head alignment and restore rotation defaults
Offset tool position upward by half the tool size so the hammer head
(bottom of rotated image) aligns with the province center where sparks
appear. With Image+RectTransform the pivot is at center, unlike
SpriteRenderer which uses the sprite's imported pivot — without the
offset the middle of the hammer sat on the province.
Restore hammerBaseRotation=180 and hammerFlipHorizontal=true to match
ToolAnimator defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Set hammer rotation default to 90, fix sound loading robustness
Change hammerBaseRotation default to 90 to match tested working value.
Sound effects were silently failing — the Addressables coroutine started
in Awake() could be interrupted if the GameObject was deactivated during
scene setup. Now uses lazy-load pattern: retries loading on first play
attempt if clips aren't ready. Added diagnostic logging for load
success/failure and missing clip names.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove defensive null checks on inspector fields
Let missing inspector wiring throw NullReferenceException immediately
rather than silently skipping. This was hiding the sound bug — the old
guard clause bailed before any logging could fire.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix sound loading: move coroutine to OnEnable, remove silent null guard
The LoadSoundEffects coroutine started in Awake() was being killed when
the GameObject gets deactivated during scene setup. Moving it to
OnEnable() ensures it retries on reactivation. Also removes the
_loadedClips null guard so failures are visible via NullReferenceException
rather than silently skipping sound playback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Play metal strike sound per hammer impact, add to Addressables
Switch from repair sound to Metal Weapon Hit Stone 2_1 for Improve
animation. Sound now plays once per hammer strike at impact rather
than once at the start of the animation. Added the clip to the
sfx-combat Addressables group.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
PR #6450 allowed meteor re-targeting, but it fails at runtime because
the initial target uses ActionCost::usesAll which drains all AP to 0,
leaving nothing for the re-target. Setting the target cost to 0 fixes
this: usesAll with minimum 0 always succeeds (0 >= 0), still drains
remaining AP (ending the mage's turn), and removes the vigor penalty.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously province 6 (Soria) had battalions but no heroes, and province
stats were hardcoded only for province 32. Now The Eagle's faction matches
the normal game: Tarn defects from faction 2, random vassals are generated
(5 for province 32, 4 for province 6), per-province stats are correct,
faction is hostile to Bregos with trust -100, and battalion names use
"Tarn's" prefix for province 6.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The validateAtMostOneAttackingAlliance check was firing during
UncontestedConquest phase, before PerformUncontestedConquestAction could
resolve the situation (largest army conquers). Skip the validation during
UncontestedConquest, matching the existing AttackDecision exemption.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add is_admin field to UserInfo proto and populate it through Go auth
handlers, Scala fallback handler, and C# client storage. Replace the
hardcoded display name check with the server-provided admin flag.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously, once a mage selected a meteor target, the command was
locked in with no way to correct a misclick. Now the target/cancel
commands remain available after targeting, but as optional (not
required to end turn), so the player can re-target, cancel, or
simply end their turn keeping the current target.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
All two-stage actions now have distinct success/failure animations.
Remove the completed status tracking table and animation descriptions,
keeping only the 7-step wiring guide for adding new two-stage actions.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for BraveWater
Split the shared BraveWater animation into two-stage success/failure variants:
- Success: Strong upward splash with bright droplets, expanding ripples, blue-white flash
- Failure: Droplets rise partway then fall back, darkening color, foam churns turbulently
Updates ShardokGameController enum, mapping methods, and dispatch.
Adds AnimateBraveWaterSuccess/AnimateBraveWaterFailed to WaterEffectAnimator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add BraveWater success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use distinct splash sound for BraveWater success
Attempt uses braved_water (wading in), success now uses splash
(satisfying water crossing) so the two phases sound different.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make BraveWater failure animation more dramatic and violent
Rewrote AnimateBraveWaterFailedEffect with 4 phases: high promising rise
(14 large particles with wide spread), brief stall with shuddering and
color flickering at peak, violent cubic-acceleration crash back down past
center with particles growing on impact, and aggressive turbulent churn
with large oscillations darkening to murky water.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for DismissUnit
Split the shared DismissUnit animation into two-stage success/failure variants:
- Success: Golden particles rise upward with warm golden glow pulse
- Failure: Particles expand then snap back inward, turning red with rejection flash
Updates ShardokGameController enum, mapping methods, and dispatch.
Adds AnimateDismissSuccess/AnimateDismissFailed to DismissAnimator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add DismissUnit success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Double the spread width of DismissUnit success and failure effects
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use undead_break_control sound for DismissUnit failure
The undead have broken free — use the dramatic control-broken sound
instead of the generic failure horn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Scale up DismissUnit effects: attempt 50% wider/taller, particles 2x
Attempt: particle spread multiplied by 1.5x, rise height 1.5x.
Success and failure: particle sizes doubled (3-6 to 6-12, 3-5 to 6-10).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix attempt particles: use visible sizes (4-8) instead of near-zero (0-4)
The original Random.Range(4f, 0f) produced values 0-4 with many near
zero, making particles invisible. Now uses 4-8 for consistently visible
particles that shrink to zero as they drift.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for RaiseDead
Split the shared RaiseDead animation into two-stage success/failure variants:
- Success: More figures rise quickly with bright green glow and pulse
- Failure: Figures rise partway then collapse back, glow flickers green to grey
Updates ShardokGameController enum, mapping methods, and dispatch.
Adds AnimateRaiseDeadSuccess/AnimateRaiseDeadFailed to RaiseDeadAnimator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add RaiseDead success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use distinct success sound for RaiseDead (undead_grew instead of raise_undead)
Attempt uses raise_undead (eerie summoning), success now uses
undead_grew (undead power surge) so the two phases sound different.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make RaiseDead failure more dramatic and use impactful success sound
Failure: figures now rise 75% of the way (was 40%), fully visible at
near-full opacity, stall with flickering glow shifting green to grey,
then get yanked back down with cubic acceleration. Much more visible
contrast between "almost worked" and sudden collapse.
Success sound: use holy_wave_damage for a darker, more impactful surge
instead of undead_grew.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct Scout success/failure animations
Success: cone fully extended, glow pulses bright and expands at target.
Failure: cone extends partway, hits a dark barrier with red flash,
cone recoils, eye dims and shrinks to nothing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add Scout success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use boo sound for Scout failure instead of generic horn
Scout failure plays "boo" — a disappointed reaction when the scout
can't see through the barrier.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Rework Scout failure animation as dark reversal of attempt
Replace the red-barrier-and-recoil failure animation with a reversal:
glow disk at target turns dark, then cone and glow retract back toward
the source eye, which darkens and shrinks away. Also change failure
sound from boo to generic negative effect horn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove leftover merge conflict marker from TWO_STAGE_ANIMATIONS.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct FreezeWater success/failure animations
Success: crystals solidify with bright white flash and intense shimmer.
Failure: crystals form then crack, flicker, and shatter outward as
spinning shards fading from icy blue to dull grey.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add FreezeWater success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use splash sound for FreezeWater failure instead of generic horn
Ice shatters and reverts to water — splash conveys the ice losing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make freeze success 30% bigger in both spread and crystal scale
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct ExtinguishFire success/failure animations
Success: heavy water deluge with thick rising steam cloud.
Failure: droplets evaporate mid-fall, fire flares back with
orange/red burst.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add ExtinguishFire success/failed test buttons to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make ExtinguishFire failure more dramatic with fire flare-up
When extinguish fails, also trigger FireEffectAnimator.AnimateFireSuccess
so the fire blazes back up with tall flickering flames and rising embers
alongside the evaporating water droplets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use distinct sounds for ExtinguishFire attempt/success/failure
- Attempt: splash (water thrown at fire)
- Success: fire_extinguish (satisfying sizzle/hiss)
- Failure: raging_fire (fire flares back up)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Captured units' battalions are destroyed during BattleEnded processing,
but the multi-victor branch was storing those destroyed IDs in
AftermathClaimantUnit. This caused invalid battalion references when
claimants withdrew. Filter them out when building claimants and
notFledDefenders, and exclude them from armySize calculation.
Also update ALLIED_VICTORY_BATTLE_RESOLUTION.md to reflect PR 6429
changes (LastAllianceStanding for attackers, allied co-attacker added
to winners for solo castle holder) and document the dead-ally
GameOverResponsePopulator bug fixed in PR 6435.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Remove ClipRectMeshUnlit shader replacement for 3D beast models
The shader replacement multiplied texture color by vertex colors, turning
honey badger white stripes red. RendererViewportClipper already handles
viewport clipping for all 3D models generically, making the shader
approach redundant and harmful.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Set render queue on beast models to render above province texture
The old ClipRectMeshUnlit shader used Queue=Transparent+100 and
ZTest Always to draw above the province colour layer. Now that models
keep their original shaders, set mat.renderQueue = 3100 in
RendererViewportClipper.Awake() to match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix spelling: colour -> color
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reserve removed fields (ally_victory=2, draw=3) and delete the
DrawType enum to match the Eagle-side proto cleanup. Update C++
conversion code: ALLY_VICTORY now maps to victory, DRAW now throws.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When your alliance wins a battle but your own units were destroyed,
you should still get a victory — not an internal error. Change the
else-if branch from throwing to assigning victory for allied
non-winners.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The buttonTooltip and oddsPanel were setting transform.position/localPosition
using screen-space coordinates directly, which placed them incorrectly when
the canvas uses Screen Space Camera mode. Convert screen positions to canvas
world space via ScreenPointToWorldPointInRectangle.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When GetGameHistory returns a non-empty error field (game failed to load),
the admin server now transparently falls back to GetRawGameHistory. The UI
shows a warning banner and uses raw-rewind instead of normal rewind. This
lets admins view and fix broken games without any manual switching.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix allied attacker victory conditions
Allied attackers couldn't win by elimination because LastAllianceStanding
wasn't in their victory conditions. When one attacker held all castles,
the ally was treated as a loser because they weren't in winning_shardok_ids.
- Add LastAllianceStanding to attacker victory conditions in RequestBattlesAction
- Add mutually-allied co-attackers to winning_shardok_ids when one player holds all castles
- Add tests for both fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Extract PopulateGameOverResponse into testable library and add tests
Move PopulateGameOverResponse and its helpers (IncludeUnitProtoInReturn,
FromUnitFb, FromInternalStatus) from EagleInterfaceGrpcServer.cpp into a
separate GameOverResponsePopulator library so they can be unit tested
directly. Add 7 tests covering victory/loss assignment, allied player
validation, unit filtering, and status conversion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Shardok now emits Victory for all winners instead of distinguishing
AllyVictory. Remove the AllyVictory case from EndGameCondition, simplify
all isWinOrAllyVictory checks to isVictory, reserve the ally_victory
proto field, and update the allied victory documentation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a game's action history contains a broken action result, the game
can't load because formAwrs throws during state replay. This means the
admin console can't view history or rewind to fix it.
These two new RPCs operate directly on persisted chunk files, bypassing
the formAwrs state-replay pipeline:
- GetRawGameHistory: reads ActionResult protos from .e0a chunks and
returns them as JSON without replaying game state
- RawRewindGame: truncates chunk files to a target action count without
loading the game into memory
Also adds an error field to GetGameHistoryResponse so load failures are
reported instead of returning empty results.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
PR #6425 removed DrawType from the Eagle common proto but didn't
update the C++ conversion code in EagleInterfaceGrpcServer, causing
a linux-aarch64 cross-compile failure.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Every battle must have exactly one player with WIN_AFTER_MAX_ROUNDS.
This adds startup validation in StartGame and replaces the unreachable
DRAW result with a ShardokInternalErrorException.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Draws are dead code left over from removed Free For All battles. Shardok
battles always have exactly one defender with WinAfterMaxRounds, so if
max rounds is exceeded the defender wins — a draw is never produced.
- Remove DrawType enum and Draw case from EndGameCondition
- Remove isDraw method and all isDraw checks in unitReturned
- Remove Draw proto conversion code from ShardokBattleConverter
- Remove DrawType from victory_condition.proto (Eagle common only)
- Add internalRequire validation that exactly 1 player has
WinAfterMaxRounds when building shardokPlayers
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix tooltip positioning for Screen Space Camera canvas
HoveringTooltip.SetPosition() was mixing world-space and screen-space
coordinates. With Screen Space Camera, GetWorldCorners returns world
coordinates (not screen pixels), and the tooltip was placed at the
wrong position. Now routes all positioning through screen space using
RectTransformUtility, with the correct canvas plane z for projection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use world-space positioning instead of screen-space roundtrip
Work directly in world space using lossyScale for offsets and
canvas GetWorldCorners for overflow, avoiding broken coordinate
space conversions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Convert tooltip providers to pass screen-space Rects
The camera looks downward, so GetWorldCorners returns points where
the visual "height" is along z, not y. A 2D Rect from world coords
loses the z axis entirely (height=0). Fix by converting world corners
to screen space in the providers via WorldToScreenPoint, then
converting back to canvas world space in HoveringTooltip via
ScreenPointToWorldPointInRectangle for final placement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Players with no active games were being automatically thrown into a
new 7-faction game. Remove this behavior so they always land in the
lobby and can choose what to do.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix allied battle victory: treat AllyVictory as winner in ResolveBattleAction
The multi-victor battle aftermath code (added in #6411) was dead code because
the winner/loser partition used `isVictory` which excludes `AllyVictory`.
Allied winners were classified as losers, so only one faction ever "won"
and the province was awarded immediately without a claim decision.
Changes:
- Add `isWinOrAllyVictory` to EndGameCondition
- Use it for the winner partition, backstory, and quest fulfillment
- Fix `unitReturned` so AllyVictory heroes stay at the battle province
as claimants instead of being sent home
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update tests to expect multi-winner behavior for allied victories
Tests previously expected AllyVictory units to be sent home. With
the partition fix, allied winners now stay as claimants for the
BattleAftermath claim decision.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The auto-bounce results set hostile armies to Withdrawing status, but
WithdrawnArmiesReturnHomeAction received the original provinces where
those armies were still AwaitingDecision. This caused a validation
error when transitioning to DefenseDecision because non-allied factions
remained in hostileArmies. Apply bounce status changes to the province
list before passing to WithdrawnArmiesReturnHomeAction so it properly
removes the bounced armies.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When multiple non-allied factions attack the same occupied province and
none makes an AttackDecision, they all remain AwaitingDecision. The
existing auto-bounce logic only triggers when an army has Attacking
status. Add a new branch that detects non-allied undecided factions and
bounces all of them to Withdrawing, preventing the
validateAtMostOneAttackingAlliance runtime error.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Three correctness fixes for the battle aftermath decision system:
- Cap withdrawal gold/food at what each faction brought into battle (broughtGold/broughtFood)
- Auto-keep last undecided claimant when nobody has kept yet (prevents broken province state)
- Serialize PendingConquestInfo to proto so server restarts during BattleAftermath don't lose state
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Destroy previous label container before creating new ones when
OnMapLoaded fires multiple times. Add OnDestroy to unsubscribe
from the event.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add FinalizeAftermathAction to handle the all-decided case (run
ProvinceConqueredAction for keeper, create withdrawal armies, clear
pendingConquestInfo), fix hero-battalion pairing by replacing separate
heroIds/battalionIds vectors with paired AftermathClaimantUnit, and
remove dead destroyedBattalions field from MultiVictorBattleSetupAction.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add BattleAftermathDecisionCommandSelector for Unity client
Adds the UI component for the battle aftermath decision system. When
allied factions jointly conquer a province, each winning faction chooses
to Keep Province or Withdraw (with destination and supply sliders).
The selector follows the AttackDecisionCommandSelector pattern with
toggle group, province dropdown, and gold/food sliders. It is inert
until the server command infrastructure sends the corresponding
available command.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add missing EagleGUIUtils using for GUIUtils.DarkColoredProvinceName
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up BattleAftermathDecisionCommandSelector in Gameplay scene
Add the command selector as a child of CommandPanelController with
all Inspector fields connected: toggles, toggle group, withdraw
container, destination dropdown, gold/food sliders and labels, info
text, and button image. Unity events wired for ToggleChanged,
GoldSliderChanged, and FoodSliderChanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Wire up the full command pipeline for battle aftermath decisions so
winning allied factions can choose Keep Province or Withdraw after a
multi-victor assault.
- Add proto messages for available/selected command (field 41) and command type
- Add Scala model types: CommandType, AvailableCommand, SelectedCommand cases
- Add proto converters for all new command types
- Add AvailableBattleAftermathDecisionCommandFactory (sequential by army size)
- Add BattleAftermathDecisionCommand (keep triggers conquest, withdraw creates army)
- Add AutoResolveBattleAftermathAction (last claimant auto-keeps)
- Add BattleAftermathDecisionCommandChooser for AI (always keeps when possible)
- Update AvailableCommandsFactory and RoundPhaseAdvancer for aftermath phase
- Update CommandFactory routing and AIClient integration
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Remove Map Editor scene from build to eliminate duplicated hex tile assets
The Map Editor scene directly references hex tile textures via RawImage
components, pulling ~4.6 MB of textures into the player build that are
already served via Addressables from CDN.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove Map Editor button from connection screen
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove EditorButtonClicked and unused SceneManagement import
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Delete Map Editor scene and MapEditorController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Enable the toggle GameObject in the scene but hide it at runtime
via Application.isEditor, so it only appears in the settings panel
when running inside the Unity Editor. The checked state still comes
from PlayerPrefs as before.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When multiple allied attackers win a province assault, defer province
assignment by storing PendingConquestInfo on the province instead of
immediately running ProvinceConqueredAction. This lays the groundwork
for the battle aftermath decision command (next PR).
- Add PendingConquestInfo, AftermathClaimant, and AftermathDecision types
- Add pendingConquestInfo field to ProvinceT/ProvinceC/ChangedProvinceC
- Branch ResolveBattleAction for multi-victor case via MultiVictorBattleSetupAction
- Add ActionResultType values for aftermath actions
- Update proto converters to handle new province field
- Remove unused validate method from ResolvedEagleUnit (breaks dep cycle)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for Repair
Repair previously used the same hammer-strike animation for both attempt
and result. Now it follows the StartFire/Fear two-stage pattern: attempt
shows hammer strikes, success shows a golden shimmer beam rising, and
failure shows debris collapsing with a dust puff.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Increase two-stage animation delay to let attempt animations finish
The delay cap between attempt and result phases was 0.5s (or 0.3s with
no sound), but the hammer animation runs ~0.6s and fire runs ~0.8s.
Raised the cap to 0.8s and the no-sound fallback to 0.6s so result
animations don't overlap with attempt animations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct repair success sound and make debris explosive
Sound: Wire up a "repair_success" clip for the result sound so it
doesn't repeat the hammer attempt sound. Falls back to the hammer
clip until the audio asset is added to sfx-combat.
Debris: Replace gentle falling animation with a violent explosion —
pieces burst outward from center with physics-driven trajectories
(initial velocity + gravity), fast spin, an initial flash burst,
and a drifting dust cloud.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use "Positive Effect 6" as repair success sound
Add the clip to the sfx-combat addressable group and load it as the
repair success result sound so it plays a distinct chime instead of
repeating the hammer attempt sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When multiple allied players each occupy different critical tiles (castles)
such that all castles are covered, this now triggers a HOLDS_CRITICAL_TILES
victory for all occupants — provided they all have the victory condition set
and are mutually allied. Previously only a single player holding all castles
could trigger this win condition.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add distinct success/failure animations for StartFire
Split StartFire into three-stage visuals following the Fear pattern:
attempt shows ignition, success shows fire blazing up (taller/brighter
flames, hotter colors, outward burst), failure shows fire dying out
(shrinking flames, cooler colors, smoke puff).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move StartFire doc to repo-root docs/TWO_STAGE_ANIMATIONS.md
Update the existing doc instead of creating a duplicate in the Unity
client directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Thread client_id from StreamGameRequest through EagleServiceImpl →
GamesManager → GameController → HumanPlayerClientConnectionState.
Update withHumanClient eviction logic to always evict connections with
the same client_id (same device reconnecting/refreshing), while keeping
connections from different client_ids alive (multi-device support).
Cancelled connections for the same faction are still cleaned up.
Changed client_id type from int64 to string for future extensibility
(e.g. embedding device name, platform, or version).
This fixes duplicate updates when a single client calls
StreamOneGameAsync for a refresh on the same gRPC stream, which caused
the "Duplicate unaffiliated hero" crash.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a stable string client_id to StreamGameRequest so the server can
distinguish same-device reconnects from multi-device connections. The
C# client generates a GUID per PersistentClientConnection instance and
sends it with every StreamOneGameAsync request. The server currently
ignores this field; server-side eviction logic follows in a subsequent
PR. Using string instead of int64 for future extensibility (e.g.
embedding device name, platform, or version).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Guard UpdateUnaffiliatedHeroSelections against count mismatch by
rebuilding the table when the model changes between rebuilds.
Keep the throw on server-side duplicate as a safety net.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add post-Eagle tutorial events: Ranil & Elena depart and form factions
After the Eagle appears, John Ranil departs the player's faction 2 rounds
later and Elena Fyar departs 4 rounds later. At 7 rounds after Eagle,
both form their own factions in isolated provinces.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add tests for TutorialHeroDepartureAction and TutorialHeroFactionsAction
Tests cover action result creation, province selection, hero changes,
battalion/faction creation, unaffiliated hero removal, and edge cases
(missing heroes, no suitable province, hero in different province).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Eagle hero missing backstoryVersions causing validation failure
EagleAppearsAction.createEagleHero was creating the hero without
setting backstoryVersions, which defaults to Vector(). The runtime
validator requires backstoryVersions.nonEmpty for all heroes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Eagle appearance crash: split entity creation from province changes
The action result applier processes changedProvinces before newHeroes,
so when EagleAppearsAction put both in a single ActionResultC, the
province change tried to look up the Eagle hero (via
ProvinceUpdateHelpers.applyRulingAndBattalions) before it existed in
the game state, causing a HashMap key-not-found exception.
Split into two sequential action results: first creates the faction,
hero, and battalions; second applies the province changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix duplicate Eagle hero: only place in province 32, not both provinces
The Eagle hero was added to rulingFactionHeroIds for both provinces 32
and 6, violating the one-hero-per-province invariant and causing a
validation crash on game load. Province 6 now gets the Eagle's faction
and battalions but not the hero itself.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace blind `Add(AddedUnaffiliatedHeroes)` with per-hero loop that
throws InvalidOperationException when a hero already exists in the
province's unaffiliated list. This surfaces the root cause of duplicate
rows in the free heroes table instead of masking it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When two clients connected for the same faction (e.g., iPad + Unity
editor), withHumanClient evicted all existing same-faction connections.
Now only cancelled connections are removed, allowing multiple active
clients to coexist with independent update streams.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix province effects missing when CDN centroids arrive after model
Since #6396 moved centroids.json to CDN, it loads asynchronously and
often arrives after MapController.Model is set. All province effect
controllers (festival, epidemic, blizzard, drought, flood, beasts)
silently skipped spawning because _provinceCentroids was empty.
Re-apply effects in OnProvinceMapLoaded when the model is already set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix province effects missing when CDN centroids arrive after model
Since #6396 moved centroids.json to CDN, it loads asynchronously and
often arrives after MapController.Model is set. All province effect
controllers silently skipped spawning because _provinceCentroids was
empty.
Add lazy centroid loading to each controller's update method so they
load centroids on-demand from ProvinceIDLoader.CentroidsJsonText. This
eliminates the callback ordering dependency with OnMapLoaded. Also
re-apply effects in OnProvinceMapLoaded when model is already set.
Remove defensive null checks on Inspector-wired controller references
in UpdateProvinceEffects — missing wiring should fail loudly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sequential _colorIndex caused all firework instances to show correlated
color sequences. Use Random.Range per launch for independent colors.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reduce sync mismatch grace period (60s→5s), HTTP/2 keepalive interval
(15s→5s), and add per-command response timeout (10s) to dramatically
cut recovery time when the client misses a game update.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GamesManager.save() writes games.e0es to S3 synchronously on the gRPC
executor thread via CompletableFuture.join(), blocking all user
operations for the duration of the upload. JFR profiling showed this
blocking for 28-65ms per command, with potential for much longer spikes.
Wrap the top-level S3Persister in AsyncS3Persister, matching what
per-game persisters already do in LocalGamePersisterCreation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The exportArchive step was relying on the Xcode-Token in the keychain
for App Store Connect authentication. This token expires periodically,
breaking headless CI builds. Pass the App Store Connect API key (already
used by the upload step) to the export step as well.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fetch centroids.json from CDN via EagleMapInfo instead of baked TextAsset
ProvinceIDLoader now fetches both the map data and centroids JSON from
CDN using SHA256 hashes from the server-provided EagleMapInfo. OnMapLoaded
fires only after both arrive and validate. All controllers subscribe to
OnMapLoaded for centroid data instead of loading in Awake().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Guard MapController.ProvinceFromPoint against null map bytes
OnMapLoaded now fires later (after both map + centroids arrive),
so Update() can run before _mapIdBytes is set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The SHA256 hash is used as the content-addressable filename for CDN
fetches, making map_name redundant. Reserve field 1 in the proto to
prevent accidental reuse.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Extends EagleMapInfo with centroidsSha256 so the client can validate
centroids.json after CDN download, matching the existing rawGray pattern.
Bumps migration to v3 so existing saves get backfilled.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Centralize centroidsJson reference in ProvinceIDLoader
All 7 province controllers now read centroidsJson from ProvinceIDLoader
instead of each having their own Inspector-linked TextAsset field.
This prepares for making centroids server-driven in the future.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up provinceIDLoader references and remove stale centroidsJson links
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The province ID map is now fetched from CDN at runtime, so it no longer
needs to be a Unity TextAsset. Move it to the resources directory
alongside other game data files and delete the .meta file.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Load province map dimensions and hash from server-provided EagleMapInfo
Replaces hardcoded map dimensions (3786x1834) across 8 Unity client files
with server-driven values from the EagleMapInfo proto added in #6381.
ProvinceIDLoader no longer auto-loads in Awake() — instead, EagleGameController
calls Initialize(EagleMapInfo) on first GameStateView, which looks up the map
asset by name, validates SHA256 integrity, and fires OnMapLoaded for dependents
(MapController, ProvinceBorderDistanceGenerator).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Keep rawGrayCompressed as direct Inspector field instead of map lookup
The server provides dimensions and SHA256 but the client only has one
map asset — no need for a name-based lookup table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fetch province map from CDN instead of bundled TextAsset
ProvinceIDLoader now uses ResourceFetcher.mapFetcher to download the
map data at runtime rather than reading a baked-in TextAsset. The server
provides the map name, dimensions, and SHA256 via EagleMapInfo.
Also fixes:
- Duplicate `using Net.Eagle0.Eagle.Common` in EagleGameController
- ProvinceLabelsController timing: defers CreateLabels to OnMapLoaded
to avoid division by zero when mapWidth/mapHeight are still 0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use eagle0-assets CDN for map fetcher instead of headshots CDN
Make ResourceFetcher base URI an instance field so each fetcher can
point at a different CDN. Map data is at:
https://eagle0-assets.sfo3.digitaloceanspaces.com/strategic-maps/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use SHA256 as CDN fetch path instead of map_name
Content-addressable: the SHA256 hash serves as both the CDN filename
and the integrity check, so map_name is not needed for fetching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* new sha
* Update EagleMapInfoMigration to read from game_parameters.json
Replace hardcoded map info with GameParametersUtils.defaultExpandedGameParameters
so the migration stays in sync with the config file. Bump version to 2 to
re-migrate existing saved games with the new map hash.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When Eagle returns an error during command processing (e.g. battalion
over-capacity validation), the warmup tool now logs the server error
message and dumps its full state (game ID, province, hero, token,
command) instead of silently timing out.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add smooth_boundaries.py for natural province boundary smoothing
Post-processing tool for rawGray.gz.bytes that replaces jagged
province boundaries with smooth, natural-looking ones using:
1. Majority-filter smoothing (iterative mode filter on boundary pixels)
2. Fragment cleanup (BFS expansion preserving ocean-separated islands)
3. Domain warping (Gaussian displacement for organic character)
Includes automated verification (province preservation, neighbor
adjacency, connectivity) and before/after visualization output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Apply boundary smoothing to province map
Run smooth_boundaries.py --seed 42 to smooth jagged province boundaries.
Uses pixel-based adjacency instead of TSV for neighbor verification.
Updates centroids.json with new label positions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add targeted boundary smoothing for West Faluria borders
Adds --targeted-smooth mode to smooth_boundaries.py that applies a
bilateral majority filter to specific province border pairs. Smooths
the 40-37 (West Faluria/Yuetia) and 40-11 (West Faluria/Garholtia)
boundaries to reduce bubble protrusions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Redraw West Faluria borders via manual edit with roughened edges
Hand-edited province boundaries for West Faluria (40) expanding into
Usvol (33) and Hofolen (17), then applied localized domain warp to
roughen the new borders for a natural look. Added rawgray_editor.py
for PNG export/import workflow and roughen_borders.py for localized
border roughening.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Old saved games predate the EagleMapInfo proto fields and will break
once the client stops hardcoding map info. This adds a migration
framework that patches saved game files at server startup, plus the
first migration (version 1) to backfill eagleMap on all existing games.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
EagleMapInfo (map name, dimensions, SHA256) was hardcoded in GameStateViewFilter.
Now it flows through the event-sourcing pipeline: JSON config → GameParameters proto
→ ActionResultC → ActionResultApplierImpl → GameState → GameStateViewFilter, matching
the existing gameType pattern. The proto definition moves from views/ to common/ so
both internal and views packages can reference it.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Server now sends map metadata (name, dimensions, SHA256) to clients
via the EagleMapInfo field. Clients will initially ignore this unknown
field until a follow-up client PR adds CDN-based map fetching.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Laufarvia (3) and West Faluria (40) are listed as neighbors in the TSV
but are 113 pixels apart on the actual map -- they haven't been adjacent
since an earlier map revision. Remove the stale adjacency from both
provinces' neighbor/neighborPositions/neighborNames fields.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Weather effect controllers (flood, drought, blizzard) each independently
decompressed the same 6.9 MB rawGray texture. Replace with a shared
reference to ProvinceIDLoader, eliminating 3x duplicate decompression.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Move combat sound effects to Addressables for CDN delivery
All 41 SoundManager AudioClip serialized fields replaced with
Addressables loading via single "sfx-combat" label. Clips are loaded
asynchronously and matched by name to build the ActionType dictionary.
Sound effects gracefully degrade (no sound) if loading hasn't completed.
GauntletThrowAnimator.impactSound and WeatherEffectAnimator audio clips
retain their serialized Inspector references (separate from SoundManager).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Route weather and gauntlet sounds through SoundManager
Move WeatherEffectAnimator's rain/thunder/blizzard audio clips and
GauntletThrowAnimator's impact sound to load via SoundManager instead
of serialized Inspector fields. This routes all game audio through
Addressables for CDN delivery.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Some terrain labels may load successfully but return 0 textures (e.g.
when Addressable bundles haven't been built yet or CDN cache is stale).
Return null instead of crashing on images[random % 0].
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Move tactical combat assets to Addressables for CDN delivery
Fire effect prefab, permanent bridge models (5), and constructed bridge
models (4) are now loaded via Addressables instead of serialized Inspector
fields. TacticalAssetLoader singleton pre-loads in EagleGameController.Start()
so assets are cached before the first battle. Empty arrays as defaults
provide graceful degradation if loading hasn't completed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix HexGrid NRE when cells not yet initialized, re-render on late load
HexGrid.Update() could NRE when cells array was null (grid not yet set
up due to async asset loading). Added null guard matching the pattern
used in all other HexGrid methods.
Also added LoadTacticalAssetsAndRefresh coroutine so bridges and fire
effects re-render if tactical assets finish loading after the initial
grid draw.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix IndexOutOfRange when tactical assets load before model setup
The LoadTacticalAssetsAndRefresh coroutine called SetModifiers() when
Model was non-null but _randomNumberForCellIndex was still empty. Guard
on the list being populated; the normal model-update paths will call
SetModifiers() once setup completes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove SetModifiers() call from asset loading coroutine
The coroutine's SetModifiers() crashes because _randomNumberForCellIndex
is populated in row-order but MapCoordsToGridIndex flips rows, so the
list indices don't match. This was a latent bug exposed by async loading.
SetModifiers() is already called from ModelUpdated() on every model
update, so modifiers will render correctly without the coroutine call.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wait for Addressables to load before setting up hex grid
SetUpGame() called GetImageForTerrain() immediately, but after moving
hex tiles to Addressables the textures load asynchronously. In custom
battles the shardok container activates and SetUpGame runs on the same
frame, before textures are ready — crashing with NullReferenceException
and leaving no hex grid visible.
Fix: split SetUpGame into a sync preamble + SetUpGameAfterLoad coroutine
that waits for IsLoaded. Also change _randomNumberForCellIndex from List
to int[] indexed via MapCoordsToGridIndex so indices match SetModifiers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Move 118 hex tile PNGs from Assets/Resources/Hex Tiles to
Assets/Hex Tiles and register them as Addressables with 22
per-directory labels (hex-plains, hex-forest, hex-winter-hills,
etc.). This removes them from the app binary so they're hosted on
CDN and only redownloaded when content changes.
ImageForTerrainTracker is now a shared singleton that loads textures
via Addressables.LoadAssetsAsync instead of Resources.LoadAll. The
load kicks off in EagleGameController.Start() (strategic scene) so
textures are cached before the player's first tactical battle.
ShardokGameController and MapEditorController reuse the same instance
with a load guard that prevents double-loading.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace 33 serialized GameObject fields in ProvinceBeastsController with
Addressables.LoadAssetsAsync label-based loading ("beast-effects").
Prefabs stay in Assets/Eagle/Effects/ but are now delivered via CDN at
https://assets.eagle0.net/addressables/[BuildTarget] instead of being
bundled in the app binary. This removes ~80-100 MB of 3D models,
textures, and animations from the initial app download.
Loading strategy:
- Eager load in Start() (last priority — after music)
- Game state that arrives before loading completes is stashed and
applied when ready
- If loading fails (offline, no cache), beasts simply don't render
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously the Tutorial button was gated to nolen/tarn but the Tutorial
Lite (skip-battle) button was editor-only. Now both buttons appear for
testers on any platform (iOS, macOS, etc.), and Tutorial Lite also
remains visible in the editor.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The nightly iOS TestFlight build already builds and uploads iOS
addressables as part of its pipeline. The standalone workflow added
unnecessary complexity and forced platform switches on the unity-mac
runner. Manual TestFlight dispatches cover any need for immediate
addressable updates.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes for iOS deep link handling:
1. CI: Remove UIApplicationSceneManifest from the generated Info.plist
during archive. Unity 6 adds this key, which opts the app into the
scene-based lifecycle. iOS then routes deep links through
UISceneDelegate instead of UIApplicationDelegate, but Unity doesn't
implement that path, so deep link URLs are silently dropped.
2. OAuthManager: Don't call Application.Quit() on iOS — it's documented
as having no effect. Instead, let the app go to background when the
browser opens and handle the deep link callback when iOS brings it
back via OnDeepLinkActivated.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
BuildMacPlayer and BuildWindowsPlayer called BuildAddressables() without
first switching the active build target. On self-hosted runners where
Unity state persists between runs, this caused Addressables to be built
for whatever platform ran last (e.g. iOS), resulting in incompatible
bundles at runtime.
Extract SwitchBuildTarget helper from BuildiOSAddressables and call it
in all three build methods before building Addressables.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a stored account's token expires and refresh fails, automatically
initiate a fresh OAuth login with the same provider instead of just
showing a brief error. On startup, show "Session expired" message
instead of silently clearing the status text.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Set roundIdJoined=1 for heroes with per-hero loyalty overrides, pushing
their earliest possible departure back by one month. This gives the
player more time before The Tumbler, Aldric the Overlooked, and similar
low-loyalty heroes leave.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
SubscribeToGame only caught NewGameException, so any other exception
(e.g. file-not-found during map loading) propagated uncaught through
gRPC as a generic "UNKNOWN" error with no logging. Now matches the
PostCommand/PostPlacementCommands pattern: catches ShardokClient,
ShardokInternal, and std::exception with stderr logging and proper
gRPC status codes.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Previously, FromPath silently returned garbage data when a file didn't
exist (tellg() returns -1, leading to std::bad_alloc from size_t(-1)).
Now throws std::runtime_error with the file path for clear diagnostics.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Have Ranil and Elena explain Improve/Alms instead of Marek
Replace Marek's combined Improve/Give Alms tutorial step with two
separate steps: John Ranil explains province improvement (highlighting
the Improve button) and Elena Fyar explains giving alms (highlighting
the Alms button). Each step notes that any hero can use the command
but the specialist gets a bonus.
Register each command button as a dynamic tutorial target in
CommandButtonPanelController so individual buttons can be highlighted.
Target IDs are derived from the button tooltip (e.g. "ImproveProvinceButton",
"GiveAlmsButton").
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix speaker portraits, highlight visibility, and notification timing
- Resolve speaker headshots from the Eagle model (strategic map) when
the Shardok model is unavailable, fixing blank portraits for Ranil
and Elena in post-battle dialogue
- Add override sorting Canvas (sortingOrder 999) to dialogue highlights
so they render above sibling UI elements like adjacent command buttons.
Increase default thickness from 3 to 5 and minimum strobe alpha from
0.2 to 0.5 for better visibility
- Delay tutorial_rebuild_support trigger until all notifications are
dismissed, so the Marek/Ranil/Elena dialogue sequence starts cleanly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use public GameModel accessor instead of private Model
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix NRE: Canvas already adds RectTransform, use GetComponent
Adding a Canvas component automatically creates a RectTransform, so
AddComponent<RectTransform>() returned null. Use GetComponent instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Re-check deferred tutorials when notifications are dismissed
CheckTutorialRebuildSupport only ran during OnModelUpdated, but after
dismissing notifications no model update occurs. Add OnAllDismissed
callback to NotificationPanel, and have TutorialTriggerRegistry
subscribe to it so deferred tutorials fire once notifications clear.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Filter nightly iOS TestFlight by relevant paths
Instead of rebuilding whenever any file changes, compare changed files
against paths that actually affect the iOS build (C#, protobuf, resources,
C++ plugins, scripts, CI config). Scala, Go, and test-only changes no
longer trigger unnecessary nightly builds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove src/main/cpp/ from iOS-relevant paths
Shardok C++ code is server-side only. Native Unity plugins come from an
external Bazel dep (@net_eagle0_unity_godice), not the in-repo C++ code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The Eagle faction is created mid-game via EagleAppearsAction but
withHandledEngineAndResults only removed AI clients for destroyed
factions — it never added AI clients for newly created ones. The
Eagle faction appeared on the map but never took any turns.
After filtering out removed AI clients, detect new factions from
action results and register AI clients for any that aren't already
human-controlled or AI-controlled.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds a check-changes job that compares HEAD against the last successful
scheduled run. If the SHA matches, the build is skipped. Manual
workflow_dispatch triggers always build regardless.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Flee and retreat commands have no target in their protobuf (only actor),
so the Unity client treated them as untargeted and called
PerformUntargetedCommand without triggering any animation or sound.
Add PlayUntargetedCommandAnimation which looks up the animation type
for the command, finds the nearest enemy to use as the flee direction,
and calls PlayAnimationAndSound with the two-stage flow so both the
attempt and result animations play correctly.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The game type check compared GameType (non-optional) against
Some(GameType.Tutorial), which always evaluates to true in Scala,
causing checkForEvents to always return empty.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Runs at 9 PM Pacific (5 AM UTC) every night, alongside the existing
manual workflow_dispatch trigger.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds a JSON-configurable faction bias that gets applied when a tutorial
hero becomes unaffiliated (imprisoned/exiled). Currently used to give
Luke the Prank-tricker a +30 bias toward Sadar's faction, making him
more willing to join after capture.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Include province name in ready-to-join hero tutorial dialogue
Marek now says which province the recruitable hero is in, e.g.
"there's a hero in Onmaa who's ready to join our cause."
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Advise taking most gold and food when expanding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Rework tutorial pacing: delay expansion until taxes, add town tutorials
- Change "support reached 40" dialogue to congratulate and encourage
continued development until January instead of immediately pushing expansion
- Move expansion instructions to the "taxes collected" dialogue
- Add tutorial_ready_to_join: fires when a free hero with WouldJoin status
appears in the player's province, instructs them to Travel and Recruit
- Add tutorial_in_town: fires when player enters town (ReturnCommand
available), explains all town activities and that Return advances the month
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refine tutorial expansion advice and remove redundant Give Alms
Remove Give Alms from the support tutorial instructions (already covered
earlier). Update the expansion panel to advise taking most heroes
(especially those with professions) and leaving only one or two behind.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add second hero departure tutorial when King loses another follower
Fires tutorial_hero_departed_again on the second month a King's hero
departs (must be a different month than the first departure). Marek
reacts with alarm that the King is losing authority across the realm.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Adjust second hero departure dialogue to hint at something strange
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Rewrite second departure dialogue in Marek's voice
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Raise loyalty from 30 to 35 for distant provinces (15% departure
chance) and from 25 to 30 for Nikemi/Pieksa (20% departure chance).
Heroes were departing too early even with the roundIdJoined fix.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
LoadedHeroConversion.toHeroC was not setting roundIdJoined, leaving it
as None. This meant hero.roundIdJoined.exists() returned false in
PerformHeroDeparturesAction, so the MinimumRoundsBeforeLoyaltyDegrades
check was silently skipped for all tutorial heroes. Move the
roundIdJoined = Some(0) assignment into toHeroC and remove the now-
redundant set in NewGameCreation.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Polytope Studio characters (rebels, peasants, knights, etc.) use a
complex zone-coloring shader that can't be replaced by ClipRectMeshUnlit.
ShaderUtils.ReplaceWithClipRect now skips materials without _MainTex and
returns false. When any material is skipped, the effect attaches a new
RendererViewportClipper component that clips by toggling renderer.enabled
based on the global _ViewportClipRect/_PopupClipRect each frame.
Also force vertex color alpha to 1 in ClipRectMeshUnlit to prevent 3D
mesh models with zero vertex alpha from becoming invisible.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a roundIdJoined check to PerformHeroDeparturesAction so heroes
that joined within MinimumRoundsBeforeLoyaltyDegrades rounds cannot
depart. This mirrors the existing loyalty degradation gating in
NewYearAction.
Also raise tutorial hero loyalty from 25 to 30 for outer King's
provinces (20% departure chance), keeping 25 for Nikemi and Pieksa
which are adjacent to Onmaa (25% departure chance).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds Inspector wiring for the skip_tutorial_battle button in lobby prefabs
and includes PopupClipper.cs.meta for the popup effect clipping component.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The .bazelrc flags prefer_prebuilt_protoc and
incompatible_enable_proto_toolchain_resolution were set but the
prebuilt binary repos were never made available. Add the required
use_extension/use_repo calls so Bazel can actually find and use the
prebuilt protoc instead of compiling it from source.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds editor-only "Skip Battle Tutorial" button that creates a tutorial
game with skip_tutorial_battle=true. Fixes CheckTutorialBattleEnded to
detect server-side auto-resolved battles (no RunningShardokGameModels
were ever observed) and fire tutorial_battle_ended normally.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When skip_tutorial_battle is set in CreateGameRequest, the server
creates the tutorial game normally then immediately resolves the
opening battle with a synthetic BattleResolution — defender wins,
Tarn flees, other attackers captured, defender battalions at ~70%.
The existing ResolveBattleAction pipeline runs so all post-battle
logic executes authentically without Shardok involvement.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Render hero backstory popup above map effects
The hero popup panel was rendering behind 3D effects (dragons, animals,
monsters) because effects use sortingOrder=103 / renderQueue=4000 while
the popup was on the root Canvas at sortingOrder=0. Add an override
sorting Canvas (sortingOrder=200) to the popup panel at startup so it
draws above all map effects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Lower effect renderQueue so popup Canvas override can draw above them
The effects used renderQueue=4000 (Overlay), which draws after all
standard UI regardless of Canvas sortingOrder. Changed the
ClipRectMeshUnlit shader queue from Overlay to Transparent+100 and
removed explicit renderQueue=4000 from effect classes. The shader's
ZTest Always + ZWrite Off + sortingOrder=103 is sufficient to render
above the map without needing the Overlay queue.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use shader-based popup masking instead of Canvas override sorting
Canvas override sorting doesn't affect MeshRenderer draw order in
Screen Space - Overlay mode. Instead, use the same clip rect pattern
as viewport clipping: PopupClipper broadcasts the popup's screen-space
bounds as _PopupClipRect, and all effect shaders discard fragments
inside it. The popup rect is cleared on disable so effects render
normally when the popup is hidden.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add popup clip rect to weather shaders (drought/flood overlay)
The ProvinceWeatherShader and ProvinceWeatherMapShader render weather
overlays as UI elements using custom shaders, so they also need the
_PopupClipRect discard check to avoid rendering over the hero popup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
ViewportClipper broadcasts the viewport's screen-space bounds as a global
shader property (_ViewportClipRect). Each effect shader discards fragments
outside those bounds, preventing particles, 3D models, and sprites from
overflowing the map viewport during zoom/pan.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a quest grants a free hero, both the quest notification and
PleaseRecruitMe command arrive in the same batch. Both panels call
SetAsLastSibling() during setup, but PleaseRecruitMe was set up last,
hiding the notification behind it. After the blocking command switch,
re-raise the notification panel if it has pending info so the player
sees the quest completion first.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Self-hosted runners persist .git/lfs/objects/ between runs, and LFS
objects are now served from a local Gitea mirror on the LAN. The
actions/cache step was downloading ~1.8 GB from GitHub on every run,
adding ~35s and consuming transfer quota unnecessarily.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add Flee as two-stage animation with captured-while-fleeing variant
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move TWO_STAGE_ANIMATIONS.md to docs/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move TWO_STAGE_ANIMATIONS.md to repo root docs/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up flee attempt and run away sound effects in Unity scene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add Flee Captured test button to AnimationTestController
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Split Flee into three distinct animations: attempt, success, captured
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix test buttons to play full two-stage sequence for result animations
Result variant test buttons (Fear Success/Resisted, Flee Success/Captured,
Duel/Declined) now play the complete attempt→result sequence instead of
only the attempt sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make flee attempt animation more visible and cap two-stage delay at 0.5s
Flee attempt now uses full dust burst and multiple blur trail afterimages
instead of minimal dust and single blur. Two-stage delay between attempt
and result capped at 0.5s in both PlayTwoStageSequence and
PlayTwoStageSoundSequence (affects other players' actions and test buttons).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Tune flee animation parameters in Unity scene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a battle ends mid-animation, coroutines stop but GameObjects parented
to the hex grid survive and leak. This extracts the Track/OnDestroy pattern
(originally fixed in FearAnimator for the skull persistence bug) into a
shared base class and applies it to all 22 Shardok animators.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The King was consistently getting overwhelmed in the early game. Add a
Royal Lancers heavy cavalry unit (800 troops) to province 37 to give
him a stronger opening position. Hero count already matches at 7.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
HeroGenerator.getHero returned NoNameRequest for pregenerated heroes,
so no name text request was created when they appeared mid-game. The
name existed in the in-memory pregenerated store but was never written
to SQLite, and getCompleteTextsAccessibleTo only queries SQLite. The
client received the hero's nameTextId but never the actual text,
showing "Hero" as a fallback.
Now getHero assigns an hn_ nameTextId and returns FixedName, so the
name goes through the normal FixedHeroName pipeline: added to SQLite
by foldInLlmRequests, immediately resolved by UnrequestedTextHandler,
and delivered to the client like any other hero name.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
FearAnimator creates GameObjects (skull, tendrils, shield, etc.) as
children of the hex grid and only cleans them up at the end of each
coroutine. If the battle ends mid-animation, the coroutine is
interrupted and cleanup never runs, leaving orphaned sprites visible
in subsequent battles.
Track all created GameObjects and destroy them in OnDestroy().
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When rewinding a game and replaying to the same battle, the Shardok
server returned cached results instead of fighting fresh. The C++ server
kept completed game controllers in runningControllers indefinitely, so
ControllerForGame returned the old controller and SubscribeToGame
immediately sent back game_over_response with stale results.
Fix: detect rewind by checking for an existing game + NewGameRequest +
knownResultCount == 0, then replace the old controller with a fresh one.
Also cancel pending Eagle-side battle subscriptions on rewind.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Quest creation only checked if neighbors were owned by a different faction,
ignoring alliances. Fulfillment logic counted allied neighbors as secured.
This mismatch caused quests to be offered and immediately fulfilled. Now
creation also checks for alliances, matching the fulfillment logic.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Take the lower of the Inspector-configured count and the actual beast
count from BeastsEvent.Count. Effect classes now initialize in Start()
instead of Awake() so SpawnBeastsEffect can cap the count field after
Instantiate but before creatures are spawned.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
RoundPhaseConverter maps Unrecognized(13|14|15) to UncontestedConquest
instead of throwing, allowing saved games from removed FFA phases to
load. CommandTypeConverter maps Unrecognized(12) to None.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Remove Free-For-All battles, replace with sequential AttackDecision
When multiple hostile factions march on the same province, instead of a
separate FFA decision/battle phase, the AttackDecision phase now handles
them sequentially: armies sorted by troop count (largest first), only
the largest undecided army gets the command. After someone advances,
allied armies can join while hostile armies are auto-bounced (occupied)
or can contest (unoccupied).
Phase flow: HostileArmySetup → UncontestedConquest → AttackDecision →
DefenseDecision → Battle (removed FreeForAllDecision,
FreeForAllBattleRequest, FreeForAllBattleResolution phases).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove FFA protos, simplify unoccupied province attack logic
- Remove FFA proto fields from available_command, selected_command,
command_type, round_phase, and army protos (mark as reserved)
- Remove corresponding converter match arms (ArmyConverter,
SelectedCommandConverter, CommandTypeConverter, RoundPhaseConverter)
- Simplify isMyTurnToDecide: hostile armies are always blocked when
another army has advanced (no more unoccupied province special case)
- Replace preBattleConquestResults with uncontestedConquestResults:
handles single/allied/hostile armies on unoccupied provinces with
conquest + bounce of hostile AwaitingDecision armies
- Add 6 tests covering tiebreak, hostile blocking, allied joining,
auto-bounce, and uncontested conquest scenarios
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove FFA references from Unity C# client
Delete FreeForAllDecisionCommandSelector and its .meta file,
remove FFA tutorial entry from TutorialContentDefinitions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix alliance check and simplify uncontested conquest logic
Fix exists→forall bug in AvailableAttackDecisionCommandFactory: armies
must be allied with ALL advancing armies, not just any one. Move all
unoccupied province conquest into UncontestedConquest phase (largest army
always wins) and remove redundant logic from EndAttackDecisionPhaseAction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Improve army tiebreaking: troop count, then power, then pseudo-random
When choosing the "largest army" (for attack decision ordering and
uncontested conquest), tiebreak by total unit power after troop count,
then by factionId XOR randomSeed for a pseudo-random final tiebreak
that varies across games instead of always favoring lower factionIds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Materialize LINQ chain to avoid multiple enumeration in
HandleOverlays, simplify redundant return in HandleLongHover,
and replace always-matching UnitId type pattern with var.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When a non-defender attacker lost a multi-faction battle, their surviving
units were silently dropped. ProvinceConqueredAction only processed the
winner's and defender's units, so a third-party loser's Normal units were
not returned (unitReturned=false for Loss), not captured (not a defender),
and not outlawed — they just vanished. Fixed by changing the filter from
defenders-only to all non-winning players.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The AllyVictory check in PopulateGameOverResponse compared the current
player's own ID (shardokPid) against their own allies list — which is
always false since a player is never listed as their own ally. Fixed by
using the lambda's `pid` parameter (the winning player's ID, previously
unused) instead of `shardokPid`.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GamesManager.leaderCache looked up faction heads with
gameState.heroes(faction.factionHeadId), which throws
NoSuchElementException if the hero died in battle and was moved to
killedHeroes. Use getOrElse fallback to killedHeroes, matching the
pattern used elsewhere (HeroInfoUtilities, ChronicleEventTextGenerator).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When an army retreats from battle, its return destination is set at march
time. If that province is conquered while the army is fighting, the
retreating army arrives at a now-hostile province, causing a "Mismatched
factionIds" validation error. Fix by detecting these stranded armies in
PerformProvinceMoveResolutionAction and shattering them (heroes become
outlaws) instead of letting them fall through to hostile army processing.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When reconnecting due to idle timeout, deadline exceeded, or normal
stream end, keep the displayed connection status unchanged instead of
briefly showing "Connecting..." or "Retry in Xs".
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The two animalPrefabs entries had their fileIDs swapped between the
scorpion_prefab and yellow_fattail_scorpion_prefab GUIDs, causing
MissingReferenceException at runtime.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Matches the existing Trade command selector behavior where food/gold
amounts turn red when they exceed the province's storage cap.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Two-stage actions from the opponent were playing the attempt sound
sequenced correctly but only one animation immediately. The attempt and
result animations were not sequenced.
Fear and Duel exposed this most visibly since they have distinct result
animations (FearSuccess/FearResisted, Duel/DuelDeclined), but the
pattern was wrong for all two-stage actions.
Replace the separate PlayTwoStageSoundSequence + PlayAnimation calls with
a unified PlayTwoStageSequence coroutine that sequences both animations
and sounds: attempt animation+sound → wait → result animation+sound.
Add AttemptAnimationType() to map result animation types back to their
attempt type (FearSuccess/FearResisted → Fear, Duel/DuelDeclined →
DuelChallenge). For actions where attempt and result use the same
animation, the result animation phase is skipped.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Decouple non-Mac builds from Xcode via BAZEL_NO_APPLE_CPP_TOOLCHAIN
Set BAZEL_NO_APPLE_CPP_TOOLCHAIN=1 in .bazelrc so apple_support skips
Xcode detection (xcode-locator) entirely for Scala/C++/Go builds.
Only mactools builds (Mac/Sparkle) re-enable the Apple CC toolchain.
This means Xcode version changes no longer affect Eagle, Shardok, test,
auth, docker, or Unity Windows builds — no expunge, no cache invalidation,
no rebuild. The sync script and its expunge/cache-key logic are now scoped
to mactools only and removed from all non-Mac CI workflows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert BAZEL_NO_APPLE_CPP_TOOLCHAIN; keep Apple CC with stable environ
BAZEL_NO_APPLE_CPP_TOOLCHAIN=1 broke builds because apple_support
registers platforms (e.g. macos_x86_64) that require Apple CC toolchains
for resolution — even in non-Mac builds.
Instead, keep Apple CC enabled but ensure its repo rule never
re-evaluates on non-Mac builds by keeping DEVELOPER_DIR pinned in
common:macos. The sync script only runs for mactools builds and
overrides DEVELOPER_DIR there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add actions/cache for .git/lfs in all workflows that fetch LFS objects
(unity_build, mac_build, ios_testflight, ios_addressables_build).
The cache key is based on .gitattributes so it updates when LFS tracking
rules change. Subsequent runs restore from cache and git lfs pull only
fetches new/changed objects, avoiding re-downloading the full 6+ GB.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Match the version that Gazelle's MVS already resolved transitively,
silencing the "implicitly updated to higher versions" debug warning.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Tag shardok_mcts_ai_basic_test as "manual" so it's skipped by
wildcard test patterns. MCTS is not in active use and this test
takes ~75s. It can still be run explicitly when needed.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The grpc 1.78.0 upgrade eliminated the rules_swift version conflict
that required SparklePlugin to live in a separate Bazel workspace.
Move it back into the main workspace under src/main/objc/.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add giant beast type using 3x-scaled peasant model
Reuse Polytope peasant prefabs at 3x scale (animalScale 45) with a single
wandering giant per province. Uses HumanMapAnims controller for idle/walk/
attack animations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix giant test method to use correct province name (Elekes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up GiantEffect prefab in Gameplay scene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Bazel Xcode version caching causing CI build failures
When Xcode updates on self-hosted runners, Bazel's cached
local_config_xcode still references the old version, causing
"xcode-locator <old_version> failed" errors.
Add sync_bazel_xcode.sh which detects Xcode version changes
and runs `bazel sync --configure` to refresh the config without
nuking the build cache. Added to all 8 CI workflows that use Bazel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use rm of cached repo instead of bazel sync --configure
bazel sync --configure requires WORKSPACE mode and fails with
bzlmod. Instead, delete the stale local_config_xcode from the
output base directly — the next bazel build regenerates it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use bazel clean --expunge instead of targeted repo deletion
Deleting just local_config_xcode is insufficient — the stale Xcode
version is also embedded in cached actions and other toolchain repos.
The only reliable fix is a full expunge. Since Xcode updates are
infrequent, the one-time rebuild cost is acceptable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use action_env to bake Xcode version into cache keys
Instead of expunging the cache (which doesn't help with the remote
cache anyway), write the Xcode build version to .bazelrc.xcode via
--action_env. This makes the version part of every action's cache
key, so stale entries in both local and remote caches are naturally
ignored after an Xcode update — no deletion needed anywhere.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add expunge back alongside action_env for belt-and-suspenders fix
action_env alone isn't enough — local_config_xcode is a cached
repository rule that bakes in the old Xcode version before any
actions run. Need expunge to clear the local toolchain config,
plus action_env to prevent stale remote cache hits afterward.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add Xcode sync to mac_build build-and-sign job
The sync step was only in the deploy job, but build-and-sign calls
bazel via build_protos.sh before that. This runs on the unity-mac
runner which also has the stale Xcode cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
It uses the Butcher 2D sprite via ButcherBeastNames HashSet,
but was missing from the documentation table.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
grpc 1.78.0 dropped its rules_swift dependency, resolving the version
conflict (rules_swift 3.x vs 2.x) that forced SparklePlugin into a
separate workspace. This removes the rules_swift single_version_override.
Includes a patch for a known BCR bug (grpc/grpc#41438) where grpc 1.78.0
requests Python 3.14.0b2 but rules_python >= 1.6.0 replaced it with 3.14.
The patch backports the upstream fix (commit 459279b).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace the 2D PSD-based DragonRed sprite with a 3D FBX model from 3dFoin
Dragon. The dragon now alternates between flying (circling at altitude with
fly/flyAttack/flyBreathFire/flyFast/flyHit actions) and grounded (wandering
with walk/run/attack/breathFire/whipTail/hit/stand/idle actions) modes.
- Add DragonMapAnims.controller with 15 animation states
- Configure dragon_skin.FBX for Generic animation with avatar
- Rewrite DragonEffect.cs with 4-mode state machine
- Delete old 2D DungeonMonsters2D dragon assets
- Add Dragon/ to LFS rules and clang-format excludes
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add 5 Polytope character archetypes for human beast animations
Replace the single Butcher sprite for all 34 human-type beasts with
5 thematic 3D archetypes using Polytope Studio Lowpoly Medieval
Characters: Knight (6 beasts), Soldier (8), Archer (3), Militia (8),
and Peasant (9). All share a HumanMapAnims controller that retargets
Clown animation clips via Unity's Humanoid system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Rename humanEffectPrefab to butcherEffectPrefab; route psychopath to it
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Move butcherEffectPrefab under Human Effect Prefabs header
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up Polytope prefabs in scene; rename HumanEffect to ButcherEffect
Wire all 6 human effect prefabs (butcher, knight, soldier, archer,
militia, peasant) in Gameplay.unity scene. Rename HumanEffect.prefab
to ButcherEffect.prefab to match the field rename.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Exclude third-party asset packs from clang-format hook
Skip DungeonMonsters2D, Polytope Studio, RRFreelance-Characters,
and Raccoon directories — these are vendor code that should not
be reformatted.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* LFS-track all files under third-party asset pack directories
Track all files under DungeonMonsters2D/, Raccoon/, Polytope Studio/,
and RRFreelance-Characters/ via Git LFS. These are imported third-party
assets that are never manually edited.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Convert DungeonMonsters2D and Raccoon assets to Git LFS
Re-add all files under DungeonMonsters2D/ and Raccoon/ through
the LFS filter so they're stored as LFS objects going forward.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The warmup-binary artifacts from docker_build were accumulating and
exceeding the 500 MB storage threshold. GitHub's retention-days cleanup
wasn't keeping up with deploy frequency. Add explicit artifact deletion
after deploy, matching what mac_build already does.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add raccoon beast animation using Sketchfab 3D model
Adds Raccoon.fbx (WildMesh 3D, CC BY 4.0) with textures, wires up
raccoonEffectPrefab in ProvinceBeastsController, and updates both
attributions.json files and BEAST_EFFECTS.md.
Editor steps still needed: create RaccoonMapAnims controller, create
RaccoonEffect prefab (duplicate HoneyBadgerEffect), wire in Gameplay.unity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up raccoon animation with Blender-converted FBX
Convert GLB to FBX via Blender (original FBX had no animation data).
Add RaccoonMapAnims controller, RaccoonEffect prefab, materials with
textures, scene wiring, and fix province name (Hella, ID 19).
Known issue: gray sphere artifact under each raccoon.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove Icosphere/hair artifacts from raccoon FBX and update scene
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Smooth 3D animal turning with Quaternion.Slerp
Animals now rotate smoothly toward their target instead of snapping.
UpdateFacing is called every frame during walking, and uses Slerp
with a configurable turnSpeed (default 5) for gradual rotation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates the duplicate attributions.json by making the Unity copy
a symlink to src/main/resources/net/eagle0/attributions.json, ensuring
a single source of truth that can never diverge.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add honey badger 3D model and wire up AnimalEffect
Import HoneyBadger FBX (63 animations) and texture from Sketchfab.
Add honeyBadgerEffectPrefab field and switch case in
ProvinceBeastsController. Still needs Unity Editor setup: Animator
Controller, effect prefab, and scene wiring.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix honey badger test province name (ID 1 is Shumal, not Onmaa)
Onmaa is province ID 14, already used by TestWildPig.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up HoneyBadgerEffect prefab and animator controller
- HoneyBadgerMapAnims controller with idle/walk/eat/attack states
mapped to HoneyBadger FBX clips
- HoneyBadgerEffect prefab with AnimalEffect referencing the model
- Wired into Gameplay.unity ProvinceBeastsController
- Includes Unity-generated .meta files for imported assets
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix HoneyBadger FBX avatar setup to enable animations
avatarSetup: 0 (No Avatar) prevented the Animator from driving the
skeleton. Changed to avatarSetup: 1 (Create From This Model) to match
how the Animal Pack Deluxe models are configured.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add attribution for recently added Unity asset packs
Add Animal pack deluxe (janpec), 2D Monster Pack Basic Bundle/PSB
(SP1/DungeonMonsters2D), and Honey Badger 3D model (WildMesh 3D) to
ASSET_AUDIT.md. Update 3D model count in summary table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add 3D model attributions to in-app/website credits
Add Animal pack deluxe, 2D Monster Pack, and Honey Badger
to both server and Unity client attributions.json files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Prevents Claude from combining bash commands with &&, ||, or ;.
Each command must be a separate tool call, which makes permission
rules work correctly per-command.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add clown beast animation using both Clown Pack models
- Add per-prefab animator controller overrides to AnimalEffect
(animatorControllers[] array, indexed to match animalPrefabs[])
- Create ClownMapAnims and FatClownMapAnims controllers mapping
idle/walk/eat/attack states to each model's animation clips
- Create ClownEffect prefab using both Clown and Fat Clown models
- Add clown case to ProvinceBeastsController, remove from HumanBeastNames
- Wire up in Gameplay.unity scene
- Update BEAST_EFFECTS.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix ClownEffect prefab (recreated via Unity editor)
The hand-written prefab YAML was rejected by Unity's PrefabImporter.
Recreated by duplicating the working OgreEffect prefab in the editor
and configuring the AnimalEffect fields there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Update BEAST_EFFECTS.md: ogre/troll now have custom animation
Reflects the addition of the Orc-Ogre pack AnimalEffect for ogre and
troll beast types (PR #6287). Removes them from the "no custom
animation" list and adds them to the custom animations table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add complete beast coverage details to BEAST_EFFECTS.md
- Add missing animal effect entries (crab, frog, rabbit, salamander,
scorpion, stag, wild goat, wild pig) to custom animations table
- Add human-type beast breakdown by likelihood with thematic clusters
for potential visual differentiation beyond the generic Butcher prefab
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace unsafe asInstanceOf casts with pattern matching via helper
methods. This is more idiomatic Scala and will give clearer error
messages if the map contains unexpected types.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Fixes two issues with beast balance:
1. Demons and vampires were impossible to defeat - reduced maxCountMultiplier
from 0.5 to 0.15 so an elite battalion (1000 troops, 100/100 stats) can
just barely defeat them.
2. Clowns had a 39% chance of being deadlier than demons due to their huge
power variance (0.8-50). Added a powerExponent field to beasts.tsv that
controls how the random roll translates to power. Default is 4 (quartic),
clowns use 20 which reduces the chance of deadly clowns to ~10%.
The casualties formula is: casualties = beastsPower² / totalPower
Where beastsPower = count × relativePower
This means overwhelming force results in minimal casualties (intended),
but also means high beast power causes quadratically more damage.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ProvinceWasSelected accessed Model.AvailableCommandsByProvince and
Model.Provinces without checking if Model was null. This caused a
crash when the user clicked a province before the game model was
received from the server on first load.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Add animatorController override field to AnimalEffect so it can swap
the prefab's controller at runtime (needed because the Orc-Ogre pack
uses state names like Ogre_Idle1 instead of the expected idle/walk/etc)
- Create OgreMapAnims.controller with idle/walk/eat/attack states mapped
to Orc-Ogre FBX animation clips
- Create OgreEffect.prefab using AnimalEffect + controller override
- Add ogre/troll cases to ProvinceBeastsController.GetEffectPrefab()
- Wire OgreEffect in Gameplay.unity scene
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Wire up animations for 8 new beast types
Add effect prefabs and scene wiring for crab, frog, rabbit,
salamander, scorpion, stag, wild goat, and wild pig using
Animal Pack Deluxe 3D models with AnimalEffect controller.
Multi-variant prefabs: frog (3), scorpion (2), rabbit (2),
stag (2), wild goat (2), wild pig (2).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix ContextMenu test labels to use real province names
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix animal effect prefab references to use root GameObjects
Each Animal Pack prefab contains many child objects (bones, meshes, etc).
The effect prefabs were incorrectly referencing the first child object
instead of the root prefab object (the one with m_Father: {fileID: 0}).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Tune animal effect scales per beast type
Adjusted animalScale values from the Unity editor after visual testing:
- Frog, salamander, scorpion, crab: 50 (small creatures need more zoom)
- Rabbit, stag, wild goat, wild pig: 20 (medium animals)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Order beasts without custom animations by how commonly they appear,
making it easier to prioritize which to add next.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Document all three effect types (MonsterEffect, AnimalEffect,
DragonEffect), list all beasts with custom animations vs vulture
fallbacks, and add 3D rendering tips.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add animal wandering effect for bear, boar, crocodile, snake, wolf
New AnimalEffect.cs drives Animal Pack Deluxe 3D prefabs using their
lowercase state names (idle, walk, run, eat, attack) and Y-rotation
for facing direction. Wolverine reuses the wolf prefab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add animal effect prefabs and scene wiring for bear, boar, crocodile, snake, wolf
Wire AnimalEffect prefabs to ProvinceBeastsController and include the
Animal Pack Deluxe asset pack. RatEffect now has both MonsterEffect
(2D sprites) and AnimalEffect (3D model) for a mixed group.
Add LFS tracking for .tga and .fbx files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix animal facing direction to use full angle rotation
3D models need to rotate Y by the actual movement angle, not just
0/180 degrees. Use atan2 of the XY direction vector so animals face
their actual travel direction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix animal rendering and remove die animation
Use sortingOrder=103 on 3D mesh renderers instead of Z offset hack
to render animals above the map. Guard against die animation state.
Default scale to 15. Remove unused run state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Disable depth test on animal renderers to prevent map clipping
When 3D animals rotate to face away, their geometry extends into
positive Z and clips behind the map plane. Setting ZTest=Always
ensures they always render on top regardless of orientation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix animal depth clipping with renderQueue and ZWrite/ZTest overrides
Use renderQueue=4000 (Overlay), disable ZWrite, and set ZTest=Always
via SetFloat (compatible with more shader types) to prevent 3D animal
meshes from clipping behind the map when rotated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The first hero departure notification in the tutorial has no narrative
context. Old Marek now comments when a non-player faction loses a hero,
noting that Bregos has always inspired loyalty and something deeper must
be at work after the upheaval with Tarn.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add wandering monster animations for beast events on Eagle map
Replace generic vulture effects with animated DungeonMonsters2D monsters
for skeleton, zombie, rat, spider, demon, orc, and human-type beasts.
Each MonsterEffect spawns a group of 3 monsters that wander randomly,
idle, and occasionally attack within a province.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add ContextMenu test actions for monster effects
MonsterEffect: Test All Idle/Move/Attack/SpecialAttack
ProvinceBeastsController: Test spawn for each beast type in Motcia
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Spread test beast spawns across different provinces
Also add "Test All Beast Types" to spawn all 8 at once.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix monster sprite facing direction
DungeonMonsters2D prefabs default to facing left, so invert the
X scale flip logic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove malformed AnimationEvents from SkeletonArcher anims
The asset pack shipped with empty functionName fields on these events,
causing "AnimationEvent has no function name specified" errors at runtime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wire humanEffectPrefab in Gameplay scene
The HumanEffect prefab was created but never assigned to the
ProvinceBeastsController Inspector field, so human-type beasts
fell through to generic vultures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add vampire beast type and remove unused DungeonMonsters2D assets
- Add VampireEffect prefab, code mapping, scene wiring, and test entry
- Remove Bat, Ghost, GhostKnight, and four Golem variants (animations,
characters, PSBs) that aren't mapped to any beast type
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove DragonEffect component from monster effect prefabs
These were created by duplicating the dragon prefab, which left a
DragonEffect component that spawned an extra dragon alongside the
intended monsters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Update Eagle appearance trigger for Hedrick and Sadar expansion
Replace Bridget (faction 4) with Hedrick (faction 5) as the tutorial
antagonist, and add a second trigger path: Sadar controlling 4+ provinces.
The Eagle now appears when either Hedrick is eliminated OR the player
faction grows strong enough to warrant attention.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Lower weak King province hero loyalty to 20
Makes the heroes in the King's weak provinces much more likely to
defect, as they barely have any loyalty to the King.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Adjust weak King province hero loyalty to 25
Loyalty 20 was causing heroes to depart too quickly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Horseshoe prints were facing forward (open end leading) instead of
trailing behind the horse. Added 180° rotation for mounted units.
Duel accepted animation was never playing because DuelAccepted wasn't
matched as a result of the pending DuelChallenge command. Added it to
IsResultVariant and cached source/target grid indices in
PendingCommandSound so the animation plays even when units have been
removed from the model by later duel combat results in the same batch.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tutorial-specific initial chronicle text
The tutorial was using the same initial chronicle text ID as regular
games, but the text content references "The Eagle" character which
doesn't exist in the tutorial scenario. When the LLM tries to generate
a chronicle update, it looks up the previous chronicle entry's text
as a dependency, causing "Unknown dependency for LLM request" errors.
This adds a tutorial-specific chronicle text that describes the
actual tutorial scenario: Sadar Rakon defending Onmaa against
King Bregos Fyar's army led by Ikhaan Tarn.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update tutorial chronicle text wording
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Register SupportField dynamically for tutorial dialogue highlight
The SupportField highlight in the tutorial_rebuild_support and
tutorial_support_deadline dialogues wasn't appearing because the
static Inspector reference on TutorialTargetRegistry was not wired up.
Dynamically register the field from EagleGameController, matching the
pattern used for GoToBattleButton and CommandPanel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Elena's reinforcement dialogue referencing Tarn incorrectly
Tarn and the King are still allies at this point, so "half of them
had already fled before Tarn's men even arrived" doesn't make sense.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Vary Ranil's siege dialogue to avoid repeating Marek's phrasing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix highlight not rendering inside layout groups
The dialogue highlight was invisible when targeting UI elements inside
a layout group (e.g. SupportField). The highlight container was being
treated as a layout child, which collapsed its rendering. Adding
LayoutElement with ignoreLayout=true makes it overlay correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make Reset Tutorials also reset dialogue and trigger state
ResetAllProgress previously only cleared the old tutorial system's
PlayerPrefs state. Now it also clears DialogueManager's completed
script IDs and TutorialTriggerRegistry's one-shot flags, so all
narrative dialogues can replay without restarting the game.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix NRE: add RectTransform before LayoutElement
LayoutElement is a UI component that auto-creates a RectTransform,
so the subsequent AddComponent<RectTransform>() returned null.
Reorder so RectTransform is added first.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The conditional `if po.support > 0` prevented explicitly setting
support to 0. Now support always uses the JSON value directly.
Provinces 37 and 40 already have their support values specified.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove the special case that allowed Ikhaan Tarn to flee.
Now all units in the tutorial battle must fight to the end.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When we added Hedrick as an independent faction (ID 5), we didn't
add it to the aiFids list in createTutorialGameForUser. This caused
the game to get stuck waiting for commands from a faction that had
no AI controller.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Override duel_base_accept_odds to 200 (max roll is 100) for tutorial
games so the player's first duel challenge is always accepted,
ensuring the tutorial can teach dueling mechanics reliably.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Split reinforcement dialogue into per-hero events for staggered arrivals
Now that reinforcements arrive at different times (Elena round 5, Ranil
round 6), inspect the arriving unit's profession to fire hero-specific
dialogue triggers instead of one generic event for all reinforcements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update tutorial dialogue and add capture/duel triggers
- Update Elena Fyar's reinforcement dialogue: escaped captivity in Pieska
- Update John Ranil's reinforcement dialogue: pushed through from Nikemi
- Add enemy hero captured dialogue trigger with dynamic hero name
- Add friendly hero captured dialogue with special cases for Marek/Sadar
- Prioritize duel tutorial over melee when target is not Ikhaan Tarn
- Add GetScriptForTrigger to DialogueManager for dynamic text injection
- Cache unit names for capture detection (units removed before processing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix capture detection to scan removed units instead of action type
CapturedWhileFleeing only fires for flee-based captures. Heroes also
leave the battle via duels, destruction, etc. Now scans every action
result's GameStateViewDiff.RemovedUnits for hero units in the name
cache, catching all forms of hero removal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Make Ranil reinforce from Tarn's direction
Elena continues to reinforce from position 6, but John Ranil
now enters the battle from the same starting position as Tarn
and the other attackers.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delay Ranil's arrival to round 7
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Reduce Sadar's unit sizes by 10%
- Rakon's Loyalists: 511 → 460
- Onmaa Defenders: 300 → 270
- Hunters of the Steppe: 384 → 346
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor beast prefab selection to switch and add extensibility doc
Replace IsDragon boolean check with GetEffectPrefab switch on beast
name string, providing a clear extension point for adding new
beast-type-specific effects. Add docs/BEAST_EFFECTS.md explaining
the architecture and step-by-step process for adding new beast types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use exact beast name matching from beasts.tsv
Switch from case-insensitive substring matching to exact string
matching on the canonical singularName from beasts.tsv. Update doc
with beasts.tsv reference and candidates for custom effects.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add dragon beast animation on eagle map
When a province has a dragon beast event, show an animated 2D dragon
(DungeonMonsters2D DragonRed) circling the province instead of generic
vultures. The dragon plays its Move animation while orbiting and
periodically triggers Attack with fire rain particles falling below.
- New DragonEffect.cs: instantiates animated dragon prefab, drives
circular orbit with vertical bobbing, flips sprite to face travel
direction, emits fire rain particle bursts on attack
- ProvinceBeastsController: extracts beast name from BeastsEvent,
selects dragonEffectPrefab when name contains "dragon"
- Add DragonRed from DungeonMonsters2D asset pack (dragon-only files)
- Add .psb and .psb.meta to Git LFS tracking in .gitattributes
- Add 2D PSD Importer package dependency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix dragon animation and improve fire breath effect
Add com.unity.2d.animation package required for SpriteSkin bone
deformation at runtime. Rewrite fire breath as directional cone aimed
at province center using stretch render mode. Continuously enforce Move
animation state to prevent controller defaulting to Idle. Add context
menu test helpers for spawning dragon effects in editor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add animation variety and disable fire breath for now
Dragon now cycles through Move, Jump, Attack, and SpecialAttack
animations with weighted random selection. Fix IsInTransition check
so action animations play fully before returning to Move. Update
defaults to match tuned Inspector values. Disable fire breath
particle system pending direction/sizing fixes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add Hedrick faction in Kojaria, move Bridget to Alah
- Move Bridget's faction from Kojaria (39) to Alah (18)
- Create new faction for Hedrick the Hedge-Merchant in Kojaria (39)
with 300 light infantry, 23 support, same development as Onmaa
- Remove Hedrick from battle reinforcements (now only Elena and
John Ranil arrive as reinforcements at rounds 6 and 7)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix reinforcement hero IDs and adjust timing
- Update TutorialReinforcementHeroIds to Set(100, 101) to match
the two remaining reinforcement heroes (John Ranil and Elena)
- Change reinforcement timing: Elena arrives after round 5,
John Ranil arrives after round 6
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ExecuteReinforcementsAction was placing ALL PENDING_REINFORCEMENT
units when any reinforcement event fired, instead of only the units
belonging to that specific event. Match pending units by eagle_hero_id
against the event's CommonUnit list so each event only activates its
own reinforcement.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The updateWith call was only setting gold and food, ignoring the
support, economy, agriculture, and infrastructure values from the
tutorial_parameters.json configuration. This caused Onmaa to have
default values (0 support, 20/20/20 development) instead of the
configured values (23 support, 45/41/42 development).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Set up 12 King's provinces with single noProfession heroes, no troops,
no resources, and low loyalty to portray the realm falling apart:
- East Faluria, Regigia, Tegrot, Berkorszag, Oscasland, Wichel,
Oriyslia, Mesh, Musland, Motcia: loyalty 50-55
- Nikemi, Pieska: loyalty ~40 (even weaker)
Also refactored the attacking army (Ikhaan Tarn's force) to be
created directly in TutorialGameCreation.scala rather than read
from JSON, since the army is already in transit when the game
starts - it's not actually stationed in Nikemi.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add fear success and fear resisted animations
Fear success: dark cloud engulfs the target, pulses ominously, then
slowly dissipates. Fear resisted: a white flash at the target deflects
the wave back toward the source where it shrinks and fades.
Split Fear into three animation types (Fear attempt, FearSuccess,
FearResisted) so the server result triggers a distinct visual. The
pending command handler now also plays result animations, not just
sounds. Rename IsFailureVariant to IsResultVariant to handle both
success and failure result matching.
Add test buttons for Fear Success and Fear Resisted in the animation
test controller.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refactor test controller to use real sound+animation flow and fix fear attempt sound
Route most animation tests through TestPlayAnimationAndSound to exercise
the real two-stage sound pipeline. Consolidate duplicate PlayAnimation
methods and remove ?? fear fallback that caused double-play.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix fear sound routing and remove test debug logging
Add FearSuccess/FearResisted to ActionTypeForSound so they play the
correct result sounds. Pass isOwnCommand=true in test path so Fear
exercises the two-stage attempt sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Improve fear success and fear resisted animations
Fear success: tendrils start 2x farther out with ease-in spiral, add
ghost skull at 40% opacity filling the hex. Fear resisted: shield sprite
pops up and bashes the skull back toward the source with spin and flash.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Cut defending unit sizes by 25% (681→511, 400→300, 512→384)
- Cut reinforcement unit sizes by 25% with battle-worn variation
(450→443, 375→382, 450→446)
- Double food in Onmaa (2000→4000) to support larger army
- Set Onmaa development stats: 45 economy, 41 agriculture, 42 infrastructure
Reinforcements already arrive one at a time (rounds 5, 6, 7).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Tutorial game creation was passing an empty pregeneratedHeroes pool to
HeroGenerator, causing a crash when PerformUnaffiliatedHeroesAction
tried to spawn new heroes in provinces (e.g., when building support in
Onmaa).
Changed to use GameParametersUtils.pregeneratedOthers as the pool,
excluding hero names already used in the tutorial to avoid duplicates.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add duel animations and programmatic test buttons
Add gauntlet throw animation for duel challenges (arcing projectile with
dust landing effect) and duel declined animation (dismissive sword sweep).
Wire duel challenge as a two-stage sound so the challenge clip plays on
command and accepted/declined plays on server response.
Refactor AnimationTestController to generate buttons programmatically
from a prefab instead of requiring manual wiring per animation. Remove
redundant private animator fields in favor of direct gameController
access.
Remove black background from Iron-Etched-Gloves sprite for use as the
gauntlet throw projectile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Make gauntlet throw more aggressive and play sound at impact
Change gauntlet animation from a gentle arc to a violent downward slam
from above the source hex to the lower half of the target hex with
cubic ease-in for an accelerating impact feel.
Move challenge sound into the animator so it plays at impact rather
than when the command is issued. The two-stage system still tracks the
pending command for the accepted/declined result sound.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Change deploy conditions from event_name == 'push' to
event_name != 'pull_request', matching the pattern already used by
the Upload Addressables step. This allows workflow_dispatch to
trigger a full deploy, which is needed for re-deploying without
a code change.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Migrate client update system to SHA-addressed blob storage
Upload game files and installer to blobs/<sha256> with immutable cache
headers, serve manifests with Cache-Control: no-cache so CDN always
revalidates. This eliminates the need for CDN cache purges after builds.
During the transition period, files are also uploaded to their legacy
paths (unity3d/win/<path> and installer/Eagle0.exe) so existing
installers continue to work until users auto-update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add blob cleanup workflow and fix installer manifest URL
- Add daily scheduled GitHub Action to garbage-collect unreferenced blobs
- Fix installer_build.yml manifest step to use blobs/<sha> URL instead
of the legacy installer/Eagle0.exe path
- Add installer_sha256 to manifest for blob cleanup tool
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert installer blob changes — installer staleness is benign
The installer self-updates on next launch, so CDN staleness for
installer/Eagle0.exe is harmless. Keep blob addressing only for
game files where manifest/file mismatch causes download failures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use manifest comparison instead of HEAD requests for blob dedup
Fetch the previous manifest to determine which SHAs already exist as
blobs, avoiding N HEAD requests per build. Also only upload to legacy
unity3d/win/ paths when the file at that path actually changed, matching
the old behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove legacy unity3d/win/ uploads — installer self-updates first
Old installers fetch the manifest, see a new installer version, and
self-update before downloading game files. The new installer fetches
from blobs/<sha>, so there's no need to keep uploading to the legacy
unity3d/win/<path> paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Set no-cache on installer and whats-new.json uploads
Both are mutable files at fixed CDN URLs that were relying on default
cache behavior. Setting no-cache ensures the CDN always revalidates,
so users get the latest installer on first download and see current
changelog entries immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add unit tests for the Eagle strategic tutorial event system:
- Game type tests: verify only Tutorial games trigger events
- Eagle appearance negative cases: events don't fire when:
- Bridget still has provinces
- Eagle faction already exists
- Idempotence tests: verify no re-trigger after Eagle appears
Also updates visibility in BUILD files to allow test access.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Tests verify the tutorial config structure including:
- 3 reinforcement events with correct IDs
- Round triggers at 5, 6, 7 for Hedrick, Elena, John Ranil
- Correct hero IDs in reinforcement units
- Tarn visibility configured for defender
- Reinforcement heroes and battalions in game state
Also fixes :engine BUILD target to export game_history and
battle_resolution which appear in the Engine interface.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Battalion backstory updates were never generated - only initial backstories
are created. This removes the unused code:
- BattalionBackstoryUpdateAction.scala
- BattalionBackstoryUpdatePromptGenerator.scala
- BattalionBackstoryUpdateRequest from LlmRequestT sealed trait
- BattalionBackstoryUpdateRequest proto message (field 41 reserved)
- Related cases in LlmResolver and proto converter
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a province is conquered, the new owner can now see the backstory
text of unaffiliated heroes in that province. This matches the behavior
of PerformReconResolutionAction which extends backstory visibility for
ruling heroes to the faction performing recon.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The admin server was fetching whats-new.json from the public CDN URL
(assets.eagle0.net) which caches aggressively. After adding an entry,
the page reload would fetch the stale cached version, making it look
like the entry wasn't saved. Read directly from S3 so the admin server
sees its own writes immediately.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Detect NewRoundAction with month=1 after the tutorial battle has ended.
Old Marek explains what taxes provide (gold and food) and advises on
how to spend them wisely.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The GraphQL layer was a workaround for a token permissions issue that
has since been fixed. The REST /git/refs endpoint works correctly with
fine-grained PATs when contents:write is properly configured. This
removes ~90 lines of GraphQL code.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Right-click (or long-press on mobile) is a well-known RTS convention that
eliminates confusion from having two melee input methods. Left-click remains
the default action (move/archery); right-click triggers melee on adjacent
enemies.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When the player has a stable province (40+ support) but only one province,
Old Marek suggests using March to expand to a neighboring province and
explains the sworn sibling/Warlord control mechanic.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When the TUTORIAL_FACTION_APPEARS action result fires, Old Marek warns
the player about the Fracture Covenant seizing Ingia and Soria, and
introduces The Eagle as a mysterious new threat.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The REST /git/refs endpoint returns 403 with fine-grained PATs even
when contents:write is granted. Switch to the GraphQL createRef
mutation which handles the same permission correctly.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Marty McPolitics (BRV 8) and Aldric the Overlooked (BRV 14) would
decline duel challenges in the tutorial, frustrating players trying
to learn dueling mechanics. Replace them with Luke the Prank-tricker
(BRV 100) and Lucia, Queen of the Marshes (BRV 100) from
generated_heroes.tsv. Both are noProfession and would still lose
to Sadar Rakon.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When the player wins the tutorial battle, multiple enemy heroes can be
captured (Tarn, Edgtheow, Julius, McPolitics). Only the first captured
hero gets a messageId via CapturedHeroPleaGenerator.
When TutorialTarnDisappearsAction removes Tarn (who was first and had
the only messageId), the remaining captured heroes have no messageId.
This causes BattleAftermath to be stuck:
- capturedHeroes.nonEmpty = true (others remain)
- hasAvailablePlayerCommands = false (no messageId means no commands)
Fix by generating a plea for the next captured hero when Tarn is
removed, ensuring BattleAftermath has valid commands to show.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Pace combat tutorials to show one at a time with action between each
Archery, melee, charge, and duel tutorials now fire one at a time during
tactical combat. After a tutorial fires, the next one waits until a battle
action occurs (by either player) before showing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Defer battle-running dialogue to player's first turn and pace start fire
- Move shardok_battle_running trigger from setup→running transition to
the player's first turn in running state, so the dialogue appears when
the player can act rather than during the enemy's opening move
- Update dialogue text: "The enemy is drawing near" instead of "Now the
real fight begins"
- Add start_fire_available to the paced combat tutorial set with lowest
priority
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Instead of all three reinforcement heroes arriving at round 5,
they now arrive one at a time:
- Round 5: Hedrick the Hedge-Merchant (longbowmen)
- Round 6: Elena Fyar (heavy infantry)
- Round 7: John Ranil (heavy cavalry)
This gives the player time to position each new unit strategically
as they arrive.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The TarnNameTextId was hardcoded as "hero_name_ikhaan_tarn" but hero
nameTextIds are generated using MurmurHash3 (e.g., "fhn_d331492f").
This caused the hero lookup to fail, making canFleeHeroIds empty.
When canFleeHeroIds is empty, the fallback logic allows ALL units
with fleeProvinceId set to flee.
Fix by using LoadedHero.nameTextId("Ikhaan Tarn") to generate the
correct hash-based ID, so only Tarn can flee as intended.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add persistHighlight field to DialogueStep. When the final step of a
dialogue has this set, the highlight stays visible after the panel is
dismissed. It clears when the next dialogue trigger fires (e.g. the
player enters the battle) or when a new dialogue starts.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Allows map editor changes to be submitted as GitHub PRs directly from
the admin UI. Uses the GitHub REST API with a fine-grained PAT (stored
as ADMIN_GITHUB_TOKEN secret) to create a branch, commit the map file,
and open a PR -- no git credentials needed on the admin server.
The button only appears when GITHUB_TOKEN is set. The flow saves the
map first, then creates the PR, showing a clickable link to the result.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add ability to specify that certain units should be visible to specific
players from the start of a battle. Used in the tutorial to make Tarn's
unit visible to the player so they can see where he is positioned.
Changes:
- Add visible_to_player_ids field to CommonUnit proto
- Add InitialUnitVisibility message to TutorialBattleConfig proto
- Apply initial visibility in ShardokGamesManager when setting up units
- Set Tarn's unit visible to defender in tutorial config
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Plan post-battle tutorial dialogue in docs
Document two narrative dialogues for after the first tutorial battle:
1. Battle Aftermath - Marek on the close call, Tarn's disappearance,
and recruiting captured lieutenants
2. Rebuild Support - Marek explains Improve and Give Alms to raise
province support before January taxes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Implement post-battle tutorial dialogues and triggers
Add two narrative dialogues after the tutorial battle:
- Battle aftermath: Marek on Tarn's disappearance and captured lieutenants
- Rebuild support: Marek guides player to use Improve and Give Alms
Trigger detection uses lifecycle flags to ensure correct ordering:
tutorial_battle_ended fires when battle is removed and commands arrive,
tutorial_rebuild_support fires after captured heroes phase completes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix rebuild dialogue: any hero can improve/alms, need 40 for taxes
Engineers and paladins are more effective, not the only ones who can
do it. Support threshold of 40 is required to collect taxes at all,
not just for higher revenue.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Highlight SupportField and refine tax instruction text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add loyalty warning and support deadline tutorial triggers
- November loyalty warning: Marek warns about heroes with <70 loyalty,
highlights the loyalty label, instructs to use Give Gold or Feast
- December support deadline: Marek urges the player to have Elena give
alms immediately if support is still below 40
- Register first vassal's loyalty label as LoyaltyLabel highlight target
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Drop 'closer than I'd like to admit' from post-battle dialogue
Marek wouldn't admit it was close — he thought it was hopeless.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use default panel position for strategic dialogues, top for combat
Hard-code panel position based on context: if a Shardok model is active,
use "top" (combat); otherwise use default position (strategic map).
Remove panelPosition overrides from strategic dialogue JSON.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add a separate tutorialStartDate constant so the tutorial starts in May
while regular games continue to start in August.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The updateWith() call in TutorialGameCreation was missing the gold and
food parameters from the tutorial config, causing all provinces to start
with default values (0) and immediately trigger starvation.
Also set reinforcement heroes to start with 80 loyalty.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The AI-generated headshot for The Talismangobbler was too revealing.
Regenerated with a more appropriate image and uploaded to DigitalOcean
Spaces.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Silvio the Smooth (strength 92) was too strong a lieutenant. Marty McPolitics
(strength 24, bravery 8) is a political opportunist who "is no good in a fight"
- Sadar Rakon would clearly defeat him in a duel.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The beforeunload handler wasn't reliably showing a dialog in all
browsers. Add a click listener that intercepts link navigation with
a confirm() dialog when there are unsaved changes.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Set Sadar Rakon's first Light Infantry battalion to size 681 (up from 381)
and armament 75 (up from 60) for a stronger starting force in the tutorial.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After the tutorial battle resolves, Tarn will either have fled (returning
to Nikemi) or been captured. This creates a TutorialTarnDisappearsAction
that:
- Removes Tarn from captured heroes list if captured
- Removes Tarn from incoming army to Nikemi if fled
- Clears Tarn's faction ID
- Removes Tarn from King's Loyalists leaders list
He remains in the main heroes list but is effectively disappeared.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add tutorial triggers for duel, engineer bombard, and hide
- Duel: fires when ChallengeDuelCommand is available; Marek warns
against dueling Tarn but encourages challenging his lieutenants
- Engineer bombard: fires when John Ranil is within 3 hexes of an
enemy; Ranil explains fortify + reduce workflow
- Hide: fires when Hedrick has HideCommand available; Hedrick
describes slipping into cover
Adds TMP sprite assets for Duel, Fortify, and Hide icons with
fallback chain in dragoons.asset.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix duel tutorial: only champions can duel, loser may be captured
- Marek addresses Sadar directly (you can challenge, not "one of our heroes")
- Instructions clarify that Champions issue duels, target can't be in a castle
- Loser may be captured (not slain), evenly matched duelists may be inconclusive
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Remove verbose descriptions from the unit type instruction panel.
The icons and names are self-explanatory — players don't need to be
told that cavalry are fast or that heavy infantry are slow.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The tile images are isometric with the hex face compressed to roughly
half the image width in height. The old formula (sqrt(3)*width/2 = 222px)
assumed a non-isometric flat-top hex, but the actual hex face is only
~128px tall. This caused the source crop to include transparent space
above and 3D depth below, making tiles appear shifted down with white
gaps at the top of each hex.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The self-hosted runner doesn't have Node.js in PATH, causing the
node --check step to fail with 'command not found'.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add canFleeHeroIds field to ShardokBattle that overrides the army-level
fleeProvinceId logic. When this set is non-empty, only heroes in the
set can flee.
For the tutorial:
- Set canFleeHeroIds to only contain Tarn's hero ID
- Set reinforcement units to canFlee = false
This ensures all units except Tarn are locked into the tutorial battle.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The charge tutorial previously fired for any unit with a charge
destination. Now it only fires when Old Marek the Learned himself
can charge, and the dialogue has him volunteering to lead the charge.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
A git conflict marker was accidentally left in map_editor.js after a
rebase merge, breaking the map editor in production. This adds two
safeguards:
- CI lint job: `node --check` to catch JS syntax errors
- Pre-commit: `check-merge-conflict` hook to catch conflict markers
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When defending reinforcement units (from tutorial) survive a battle,
add them to the defending province's rulingFactionHeroIds and
battalionIds. Previously, these units existed in the game state but
were never assigned to a province after the battle ended.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
A rebase conflict marker was accidentally left in the file, causing
a JavaScript syntax error that broke the map editor entirely.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix race conditions in reinforcement hero headshot resolution
Two race conditions prevented headshots from showing for mid-battle
reinforcement heroes:
1. The eagle NewHeroes update and Shardok ChangedReserveUnits update
arrive as separate messages with no guaranteed ordering. If the
Shardok update arrives first, units are created with null headshot
and name data.
2. Streaming hero name text may not have arrived in ClientTextProvider
when the dialogue fires, so name-based matching fails.
Fix: propagate NewHeroes from the eagle model to all running
ShardokGameModels (backfilling any units created with missing data),
and retry headshot resolution each frame in DialogueManager until it
succeeds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use case-insensitive name matching and model display names for dialogue
Replace ResolveHeadshotPath with ResolveSpeaker that:
- Uses case-insensitive name matching (fixes "Hedge-Merchant" vs
"Hedge-merchant" mismatch for Hedrick)
- Returns both the headshot path AND the display name from the model,
so the dialogue shows the canonical hero name from the game data
Remove the per-frame retry mechanism — reinforcement heroes are in the
eagle game state from the start, so there is no race condition.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove unnecessary RegisterNewHeroes propagation
Reinforcement heroes are in the eagle game state from tutorial
creation, so they're already in the ShardokGameModel's dictionaries
when the battle starts. No need to propagate them later.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The tile images contain flat-top hexes (height = sqrt(3)/2 * width),
but our grid uses pointy-top hexes (height = 2/sqrt(3) * width) which
are ~15% taller for the same width. Scale based on the flat-top hex
height within the source image so the hex content fills the full
canvas hex vertically. Excess width is clipped.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The tile images contain flat-top hexes (height = sqrt(3)/2 * width),
but our grid uses pointy-top hexes (height = 2/sqrt(3) * width) which
are ~15% taller for the same width. Scale based on the flat-top hex
height within the source image so the hex content fills the full
canvas hex vertically. Excess width is clipped.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add Silvio the Smooth as a 5th hero in province 31 (before Aldric who
stays behind), and add a light cavalry battalion (size 450) to the
attacking force.
Also give the battalions proper names:
- Doomriders (heavy cavalry)
- The Shardok's Guard (heavy infantry)
- Bowmen of Nikemi (longbowmen)
- Swift Sabres (light cavalry)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Center the tile image on the hex center (center.y - dh/2) instead of
aligning the image top with the hex top vertex (center.y - HEX_SIZE).
The scaled image is taller than the hex and the hex clip handles
cropping the excess.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Remove "cruelty" and "madman" from Tarn description; replace with
erratic/paranoid/inscrutable behavior (he's not evil, just alarming)
- Add dragoons to Marek's unit rundown and instruction panel
- Rewrite reinforcement hero intros to reference backstories and the
strategic situation rather than self-introductions of abilities
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Reinforcement heroes (e.g. John Ranil) arrived mid-battle with no
headshot or name because ShardokGameModel's hero dictionaries were
only populated at battle creation. Add a fallback lookup to the eagle
model's Heroes collection so mid-battle heroes resolve correctly.
Also extract the hardcoded bridge scale (15) to an Inspector field on
HexGrid, defaulting to 22.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Tutorial reinforcement heroes (IDs 100, 101, 102) were being created as
CommonUnit objects for Shardok but never added to Eagle's GameState. This
caused a NoSuchElementException when ResolveBattleAction tried to look up
these heroes after the battle resolved.
The fix adds the reinforcement heroes and battalions to the game state
during tutorial game creation:
- Create HeroC objects from LoadedHero data with specific IDs (100-102)
- Create BattalionC objects with matching IDs
- Add both to the actionResult's newHeroes and newBattalions
The heroes are assigned to the player faction but not to any province,
representing "off-map" reinforcements that will appear during battle.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Three fixes:
1. Hex overlap: The hexCenter/pointInHex formulas used flat-top spacing
(col * 1.5 * s, row * sqrt(3) * s) for a pointy-top hex layout.
Swap to correct pointy-top formulas (col * sqrt(3) * s, row * 1.5 * s).
2. Image vertical offset: Tile images (256x384) have hex art in the
upper portion with depth effect below. Align image top with hex top
instead of centering, so the hex art matches the clip region.
3. Castles: Render the castle tile image (hexPlainsCastle00.png) when a
tile has a castle modifier, matching how Shardok renders castles.
Remove the brown box overlay for castles.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add tutorial reinforcements dialogue with dynamic headshot resolution
When reinforcements arrive in the tutorial battle, Marek introduces three
allied heroes (Ranil/Engineer, Fyar/Paladin, Hedrick/Ranger) who each
explain their profession abilities. Hero headshots are resolved at display
time from the ShardokGameModel, the same way UnitInfoPanelController does
it, rather than hardcoding image paths in the dialogue JSON.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix dialogue queuing, charge trigger timing, and start fire condition (#6170)
- Queue dialogue triggers when another dialogue is active so they play
after the current one ends (fixes reinforcements being dropped when
thunderstorm dialogue was showing)
- Trigger charge tutorial when a move command has a charge follow-up,
not when the charge command itself is available (too late)
- Require EnemyHostility for start fire tutorial, not just non-self
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* 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>
* Implement The Eagle's appearance in tutorial
When Bridget loses all provinces in tutorial mode, The Eagle (Fracture
Covenant) appears and takes control of provinces 32 and 6, displacing
any existing occupants to adjacent friendly provinces.
- Add TutorialStrategicEvents to check for tutorial-specific events
- Add EagleAppearsAction to create The Eagle faction with battalions
- Add TutorialEagleAppears and TutorialDisplacement ActionResultTypes
- Hook tutorial events into EngineImpl.withUpdateChecks
- Update visibility in BUILD files for tutorial package access
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move tutorial code from library to service to fix layering violation
The tutorial code was in library/tutorial/ which violated the architecture
where library/ should be pure game logic without proto/service dependencies.
Changes:
- Move EagleAppearsAction and TutorialStrategicEvents to service/tutorial/
- Add applyActionResults method to Engine trait for service-layer events
- Hook tutorial events into GameController.withHandledEngineAndResults
- Update BUILD.bazel visibility rules for the new location
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Rename TUTORIAL_EAGLE_APPEARS to TUTORIAL_FACTION_APPEARS
More generic name for potential reuse with other factions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Hetzner server uses IPv6-only networking with NAT64 for IPv4. The first
docker pull often fails because the NAT64 gateway hasn't warmed up. Add a
curl pre-warm to the DO registry and retry docker pull up to 3 times with
a 10s backoff.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Instead of changing PENDING_REINFORCEMENT units to RESERVE_UNIT and triggering
a placement phase, directly place them as NORMAL_UNIT at available starting
positions. This gives the player immediate control of reinforcement units.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Assembly-CSharp.csproj is auto-generated by Unity and already matched by
the *.csproj gitignore pattern, but was still tracked from a prior commit.
GeneratedProtos.meta is a Unity meta file for a generated directory.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add narrative dialogue tutorial system with Old Marek opening
Replace mechanical modal tutorials with in-world character dialogue.
Old tutorial content is suppressed (early return in RegisterAll) but
preserved for reference. DialogueManager loads JSON scripts from
Resources/Dialogues/, plays them via DialoguePanelController with
speaker headshots, and integrates with the existing highlight system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add province highlighting, strobe animation, and dialogue fixes
- Add province bounding box highlight via MapController proxy RectTransform
- Fix province highlight Y coordinate (byte array y=0 = bottom, matching anchors)
- Add strobe/pulse animation to all highlight borders
- Replace cross-canvas highlighting with child-of-target border approach
- End dialogue on battle transition to prevent highlight persistence
- Fix instruction text to match actual button label ("Battle!")
- Add highlightProvince field to DialogueStep for map province highlighting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- King (Bregos Fyar) is now faction 2 (was incorrectly 1)
- The Eagle (faction 1) is reserved for later
- Player (Sadar Rakon) remains faction 3
- Bridget remains faction 4
Also fixed GamesManager to assign player to faction 3 and
AI to factions 2 and 4.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add factionId and heroLoyalty fields to SetFaction proto message.
Tutorial parameters now specify:
- factionId: explicit faction ID for each faction
- heroLoyalty: loyalty value for heroes in that faction
This moves configuration out of hardcoded Scala into the JSON file,
making it easier to adjust tutorial setup.
Faction config:
- Sadar Rakon (Player): factionId=3, heroLoyalty=90
- Bregos Fyar (King): factionId=1, heroLoyalty=85
- Bridget: factionId=4, heroLoyalty=85
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Faction IDs:
- King (Bregos Fyar) gets faction ID 1
- Eagle reserved at faction ID 2
- Player (Sadar Rakon) gets faction ID 3
- Other factions get ID 4+
Hero and province setup:
- Player heroes loyalty set to 90
- Provinces 14, 31, 39 start with 50 gold and 2000 food
- Tarn's army marches to Onmaa; Aldric the Overlooked stays to hold Nikemi
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Shardok now returns proper gRPC status codes (INVALID_ARGUMENT for client
errors, INTERNAL for server errors) instead of re-throwing exceptions that
resulted in opaque UNKNOWN status. Eagle's ShardokInterfaceGrpcClient now
captures these and other errors via Sentry.captureException at all error
points, skipping transient UNAVAILABLE errors handled by reconnection.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Instead of creating reinforcement units at event time (which crashes the
ActionResultApplier's "battalion got larger" validation), pre-place them at
battle creation with a new PENDING_REINFORCEMENT status. When the tutorial
reinforcement event fires, simply change their status to RESERVE_UNIT.
Key changes:
- Add PENDING_REINFORCEMENT = 10 to UnitStatus enum in unit.fbs
- ActionResultApplier now reads status from changed unit bytes instead of
hardcoding NORMAL_UNIT, so reinforcement units correctly get RESERVE_UNIT
- PlaceUnitCommand, PlaceHiddenUnitCommand, and ReinforceCommand now
explicitly set NORMAL_UNIT status (previously relied on applier default)
- TutorialBattleController finds PENDING_REINFORCEMENT units and changes
them to RESERVE_UNIT, removing the UnitConversions dependency
- All exhaustive UnitStatus switch statements updated with new enum value
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Adjust tutorial event timing
- Reinforcements now appear after round 6 (was 5)
- Tarn's flight now triggers after round 7 (was 5)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Replace Alakanda with Elena Fyar in reinforcements
Sadar Rakon is already a Champion, so swapping in Elena Fyar
(Paladin) for more profession variety.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove Tarn flee events from tutorial
Removing both flee triggers (units lost and after round 7) -
keeping the battle to run its natural course.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Adjust tutorial battle unit sizes for realism
- Double the defender's longbowmen (200 -> 394)
- Add slight variance to all unit sizes to make them look battle-worn:
- Defender Light Infantry: 300 -> 293, 308
- Attacker Heavy Cavalry: 600 -> 592
- Attacker Heavy Infantry: 500 -> 507
- Attacker Longbowmen: 300 -> 291
The non-round numbers suggest these units have already been through combat.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Replace Colin with Old Marek the Learned in tutorial
Old Marek will be used extensively in the tutorial, so adding
him as one of the player's starting heroes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Defer hexGrid.SetUp() by one frame (EnqueueForNextUpdate instead of
Enqueue) so Unity's layout system fully settles after the Shardok
container activation. Previously, mapArea reported a stale size on
the setup frame, causing HexGrid.Update() to detect a size change
and trigger RebuildGrid() on the next frame — visible as the map
snapping to a different size/position.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Wire GameStateView.GameType through IGameModel so the client can
read the game type (Normal vs Tutorial). Also apply NewGameType
from GameStateViewDiff when present.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
After round 5, the player's TutorialBattleConfig includes three reinforcement
units configured as CommonUnits:
- Hedrick the Hedge-Merchant with 600 longbowmen (75 training/armament)
- John Ranil with 500 knights (80 training/armament)
- Alakanda with 600 heavy infantry (75 training/armament)
These troops are better equipped than the player's starting forces.
Note: This only configures the reinforcements in the TutorialBattleConfig proto.
The C++ Shardok-side implementation to actually spawn the units in battle is
being done separately.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a reinforcement placement phase to tutorial battles so that when
reinforcement events fire, the receiving player can place new units
before combat resumes. This introduces a new REINFORCEMENT_PLACEMENT
game state that pauses the turn flow until all reinforcement units
are placed.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sort lobby running games by CreatedTimestampMillis with GameId as
tiebreaker for existing games that share timestamp 0.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add created_timestamp_millis field to RunningGame (internal storage)
and GameInfo (client API) protos. Thread the timestamp through
ControllerInfo, GamePlayerInfo, and all lobby query paths in
GamesManager. Set timestamp on game creation (both regular and
tutorial). Existing games default to 0 (proto3 default).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The Verify workspace step was added to debug the sparse-checkout issue,
which is now fixed by the Clean workspace step. Also removes clean:true
which runs git clean -ffdx and destroys bazel-* symlinks unnecessarily.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fix tutorial battle flee action player ID
The FleeAction was using playerId=1 (defender/player) instead of
playerId=0 (attacker/Tarn). This caused the player's units to flee
instead of Ikhaan Tarn's units.
In Shardok battles:
- playerId=0 is the attacker
- playerId=1 is the defender
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add eagle_hero_ids to FleeAction for hero-specific fleeing
Extends FleeAction proto to support targeting specific heroes by their
Eagle hero ID. When eagle_hero_ids is specified, only units with matching
attached heroes will flee.
In the tutorial, this makes only Ikhaan Tarn flee when triggered, rather
than all of his units.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Enable --incompatible_enable_proto_toolchain_resolution and
--prefer_prebuilt_protoc so Bazel downloads a pre-built protoc
binary from official protobuf releases rather than compiling it.
Protoc plugins and the runtime library still build from source.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The gh CLI isn't installed on all self-hosted runners. Replace with
curl + python3 which are available everywhere.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When multiple commits are merged to main in quick succession, intermediate
builds can be skipped if a newer docker_build run is already queued.
This avoids wasting runner time on builds that will be immediately
superseded, while never cancelling a build or deploy that's in progress.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When multiple commits are merged to main in quick succession, intermediate
builds can be skipped if a newer docker_build run is already queued.
This avoids wasting runner time on builds that will be immediately
superseded, while never cancelling a build or deploy that's in progress.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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>
* Add GameType enum to track tutorial vs normal games
- 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>
* Add newGameType field to GameStateViewDiff
Ensures game type changes are sent to clients via diffs, not just
the full GameStateView.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove default value from GameState.gameType parameter
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>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Self-hosted runners can get stuck in sparse-checkout mode from previous
deploy jobs. The actions/checkout@v4 sparse-checkout disable has a bug:
it writes core.sparseCheckout=false to .git/config.worktree, then
immediately unsets extensions.worktreeConfig, causing git to fall back
to .git/config where core.sparseCheckout is still true.
Fix: explicitly set core.sparseCheckout=false in .git/config and
remove the worktree config files entirely before checkout.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The deploy job in docker_build.yml used sparse-checkout on [self-hosted, bazel]
runners. This left the workspace in sparse-checkout mode, causing subsequent
jobs on the same runner to fail with "not invoked from within a workspace"
because MODULE.bazel was missing.
Co-authored-by: Claude Opus 4.6 <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>
2026-02-06 07:20:05 -08:00
6309 changed files with 663351 additions and 324334 deletions
| python3 -c "import sys,json; runs=json.load(sys.stdin).get('workflow_runs',[]); print(len([r for r in runs if r['run_number'] > ${{ github.run_number }}]))")
if [ "$QUEUED" -gt 0 ]; then
echo "::notice::Skipping build — $QUEUED newer run(s) queued for this workflow"
**NEVER chain bash commands.** Do not use `&&`, `||`, or `;` to combine commands. Each command must be a separate Bash tool call. Use parallel tool calls when commands are independent.
## CRITICAL GIT RULES (NEVER VIOLATE)
**NEVER use `git -C`.** Instead, cd to the repo root and run git commands from there.
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
**NEVER merge PRs.** You create PRs. The user merges them. No exceptions.
@@ -225,6 +231,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):**
@@ -333,6 +342,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 switches Unity to iOS target and builds addressables for CDN upload
set -euxo pipefail
# Read Unity version from project file
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
# Use runner-specific build directory if EAGLE0_BUILD_DIR is set, otherwise default
When a province has a `BeastsEvent`, `ProvinceBeastsController` spawns a visual effect at the province centroid. By default this is the generic `BeastsEffect` (circling vultures/crows). Beast-specific effects can override this based on `BeastInfo.SingularName`.
| `Assets/Eagle/DragonEffect.cs` | Dragon-specific effect (3D model with flying/ground modes) |
| `Assets/Eagle/MonsterEffect.cs` | Wandering 2D monster group (DungeonMonsters2D prefabs) |
| `Assets/Eagle/AnimalEffect.cs` | Wandering 3D animal group (Animal Pack Deluxe prefabs) |
| `Assets/Eagle/Effects/` | All effect prefabs |
## Adding a new beast type
### 1. Create the effect MonoBehaviour (or reuse an existing one)
Three effect scripts exist for different asset types:
**MonsterEffect** (2D sprites, DungeonMonsters2D): Spawns a group of wandering 2D monsters. Uses PascalCase animator states (`Idle`, `Move`) and triggers (`AttackTrigger`, `SpecialATrigger`). Flips sprite X scale for facing direction. Best for DungeonMonsters2D prefabs.
**AnimalEffect** (3D models, Animal Pack Deluxe): Spawns a group of wandering 3D animals. Uses lowercase animator states (`idle`, `walk`, `eat`, `attack`) with `Animator.Play()`. Rotates model on Y axis for facing. Sets `sortingOrder=103`, `renderQueue=4000`, and `ZTest=Always` to render above the map. Best for Animal Pack Deluxe prefabs.
**DragonEffect** (single animated 3D creature): Spawns a 3D dragon that alternates between flying (circling at altitude with fly actions) and grounded (wandering with ground actions) modes. Uses `Animator.Play()` with DragonMapAnims controller. Best for large creatures with flight capabilities.
**Particle-based** (like `BeastsEffect.cs`): Good for simple effects like swarms or atmospheric particles.
To create a new effect script, use `[RequireComponent(typeof(RectTransform))]` for UI canvas positioning and accept tuning parameters as public fields.
### 2. Create the prefab
1. Create an empty GameObject in `Assets/Eagle/Effects/`
2. Add a `RectTransform` component
3. Add your effect MonoBehaviour
4. Wire up any references (animated prefab, textures, materials)
5. Save as prefab
### 3. Register in ProvinceBeastsController
Add a prefab field and a case in `GetEffectPrefab()`:
```csharp
// In ProvinceBeastsController.cs
// Add inspector field alongside existing ones:
publicGameObjectwolfEffectPrefab;
// Add case in GetEffectPrefab(), keyed by singularName from beasts.tsv:
In `Gameplay.unity`, find the `ProvinceBeastsController` component and assign your new prefab to the new field.
### 5. Test
Use `[ContextMenu]` methods on `ProvinceBeastsController` to test:
- "Test Dragon in Motcia (ID 2)" spawns a dragon effect
- Add similar methods for your beast type
- "Clear All Effects" removes all active effects
## Beast name matching
Beast names come from `BeastInfo.SingularName` in the proto, which uses the canonical `singularName` from `src/main/resources/net/eagle0/eagle/beasts.tsv`. The switch in `GetEffectPrefab` uses exact string matching on this canonical name (e.g. `"dragon"`, `"wolf"`, `"skeleton"`).
Human-type beasts (pirates, bandits, etc.) are matched via a `HumanBeastNames` HashSet in `ProvinceBeastsController` rather than individual switch cases.
## Beast animation coverage
### Custom animations
| Beast | Effect Type | Asset Pack | Prefabs Used |
34 human-type beast names use 3D Polytope Studio characters grouped into 5 archetypes. Each archetype uses a shared `HumanMapAnims.controller` that retargets Clown animation clips (idle, walk, attack, damage) via Unity's Humanoid system. One additional human type (psychopath) uses the 2D Butcher sprite from DungeonMonsters2D.
| Archetype | Polytope Type | Beast Names (count) |
### No custom animation (falls back to generic vultures)
Ordered by spawn likelihood (most common first):
| Beast | Likelihood | Notes |
|---|---|---|
| hippogryph | 0.04 | Fantasy creature |
| chimpanzee | 0.04 | Primate |
| unknown | 0.00 | Intentional fallback |
## Using low-poly animal packs (Africa Animals, Dino Pack)
Assets from these packs (by the same author) require extra setup because they ship with **Legacy** animation import and include physics components that conflict with AnimalEffect. The workflow below applies to all of them:
- Africa Animals Pack Low Poly V1 (Lion, Lioness, Black Panther, Crocodile, Elephant, Giraffe, Rhinoceros, Tiger, Zebra)
1.**Switch FBX import to Generic animation type.**
In the `.fbx.meta` file (location varies by pack):
- Change `animationType: 1` to `animationType: 2`
- Change `avatarSetup: 0` to `avatarSetup: 1` (newer format) — older metas without `avatarSetup` will auto-create an avatar at runtime
- Set `loopTime: 1` on looping clips (idle, walk, eat, run) but NOT one-shot clips (attack, death, hurt)
Unity will re-import the FBX with Mecanim-compatible clips when it next opens.
2.**Create an AnimatorController** in `Assets/Eagle/Effects/` named `<Animal>MapAnims.controller`.
Use the same 4-state pattern (idle, walk, eat, attack) as ElephantMapAnims. Reference the animation clips by their fileIDs from the FBX meta's `internalIDToNameTable` (type 74 entries) or `fileIDToRecycleName` (older format, type 7400000+). These IDs are stable across import type changes. Note: if the FBX names its idle clip `idle1`, create the controller state as `idle` but reference the `idle1` clip by fileID.
3.**Create an AnimalEffect prefab** in `Assets/Eagle/Effects/` named `<Animal>Effect.prefab`.
Reference the pack's prefab as the animal prefab, and wire up the new AnimatorController. Multiple color/sex variants from the same or similar FBX can share one controller (e.g., Lion and Lioness). AnimalEffect will automatically strip the pack's legacy Animation, Rigidbody, and Collider components at spawn time, and add an Animator if the prefab doesn't already have one.
4.**Register in ProvinceBeastsController** with a case in `GetEffectPrefab()` and a test ContextMenu method.
5.**Mark the effect prefab as Addressable** with the `beast-effects` label in the Unity editor (Window > Asset Management > Addressables > Groups).
### Notes
- The packs include both SingleTexture and TextureAtlas variants; use SingleTexture for best visual quality.
- Hippos have water-specific animations (idlewater, deathwater) that could be used for future water-province effects.
- The packs' prefabs include Rigidbody/BoxCollider components intended for gameplay use. AnimalEffect strips these automatically since it controls position directly.
### Available animals not yet set up as beasts
These animals exist in the imported packs and could be set up as in-game beasts using the workflow above:
**Africa Animals Pack V1** (already imported):
| Animal | Good fit for | Notes |
|---|---|---|
| Black Panther | panther/leopard beast | Separate FBX; same animation set as Lion |
| Giraffe (x2 variants) | giraffe beast | Tall model, may need larger scale |
| Rhinoceros | rhinoceros beast | Large, aggressive; good for a tough beast |
| Zebra | zebra beast | Herd animal; good with higher animalCount |
(Crocodile, Elephant, and Tiger already have effects from other packs.)
**Africa Animals Pack V2** (already imported):
| Animal | Good fit for | Notes |
|---|---|---|
| Antelope | antelope/gazelle beast | Fast-moving; good with higher moveSpeed |
| Gorilla | chimpanzee beast (closest match) | Large primate; could serve as chimpanzee proxy |
| Hyena | hyena beast | Pack hunter; good with animalCount 3-4 |
| Leopard | leopard beast | Similar to Black Panther but spotted |
| Phacochoerus (Warthog) | warthog beast | Small, aggressive; similar to boar |
**Dino Pack Low Poly V1** (already imported):
| Animal | Good fit for | Notes |
|---|---|---|
| Brontosaurus | brontosaurus/sauropod beast | Very large; needs big scale, low count |
| Pteranodon | pteranodon/flying beast | Has fly animation; could use DragonEffect-style flight |
| Stegosaurus | stegosaurus beast | Large herbivore |
| T-Rex | tyrannosaurus beast | Iconic predator; good as a solo beast |
| Triceratops | triceratops beast | Large herbivore; good for tough beast |
**Australia Animals Pack V1** (already imported):
| Animal | Good fit for | Notes |
|---|---|---|
| Kangaroo | kangaroo beast | Distinctive hop animation |
| Emu | emu/ostrich beast | Large flightless bird |
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 file contains a hardcoded geographic description of the map used in LLM prompts for generating narrative text. Update any references to the old province name.
This JSON file contains province metadata used for map rendering (centroid positions, areas, orientations). Each entry has a `name` field that should be updated to match.
Note: The actual province name displayed to players comes from the server via the `ProvinceView` proto, not from this file. However, this file should be kept in sync for consistency and because it may be used for map label positioning.
Update any test strings that reference the old province name.
## How Province Names Flow to the Client
Province names are sent from the Eagle server to the Unity client via the `ProvinceView` protobuf message:
```protobuf
messageProvinceView{
int32id=1;
stringname=6;// <-- Province name sent from server
...
}
```
The server reads province names from `province_map.tsv` at startup. The client receives names through the proto and displays them via `province.Name` in C# code. This means:
- Changing the TSV automatically updates what clients see
- No C# code changes are needed (the code uses `province.Name` generically)
- The `centroids.json` name field is for map rendering/positioning, not display
## Files That Do NOT Need Changes
- **Hero backstory TSV files** (`heroes.tsv`, `generated_heroes.tsv`) - These don't contain province name references
- **C# client code** - Uses `province.Name` from the server proto, not hardcoded names
- **Proto definitions** - No province names are hardcoded in protos
## Checklist
- [ ] Update `province_map.tsv` - province's own row
- [ ] Update `province_map.tsv` - all neighbor references
- [ ] Update `MapDescription.scala`
- [ ] Update `centroids.json`
- [ ] Update any test files with province name references
- [ ] Build and run tests: `bazel test //src/test/scala/...`
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"
@@ -149,6 +149,52 @@ Triggered during battles when players encounter spells, terrain, or abilities.
---
## Post-Battle Dialogue (Narrative Dialogues)
These fire after the first tutorial battle ends and the player returns to the strategic map. They use the narrative dialogue system (`DialogueManager` / `tutorial_strategic.json`) rather than the modal tutorial system.
### Battle Aftermath
| Field | Value |
|-------|-------|
| ID | `post_battle_aftermath` |
| Trigger | `tutorial_battle_ended` (battle removed from RunningShardokGameModels) |
| Speaker | Old Marek the Learned |
| Panel Position | top |
**Dialogue (Marek):**
> That was closer than I'd like to admit. We held — barely. But Tarn... Tarn is gone. Not retreated — *gone*. The men say he vanished from the field. Some claim sorcery. Others say he slipped away in the chaos. Whatever the truth, no one can find him.
>
> But we captured some of his lieutenants. These are soldiers, Sadar — they followed orders, same as we once did. They're not to blame for Tarn's madness. See if you can bring them to our cause, or at least hold them until they come around.
**Instructions:***(none — Handle Captured Heroes UI is self-explanatory)*
### Rebuild Support
| Field | Value |
|-------|-------|
| ID | `post_battle_rebuild` |
| Trigger | `tutorial_rebuild_support` (fires when available commands no longer include HandleCapturedHeroCommand) |
| Speaker | Old Marek the Learned |
| Panel Position | top |
**Dialogue (Marek):**
> Good. Now we need to rebuild our support here in Onmaa. The people are shaken — a battle on their doorstep will do that. We need them behind us if we're going to hold this province.
>
> Any of our heroes can develop the land or give alms to the people — nothing wins hearts faster than a full belly. Engineers like John Ranil are especially effective at improving a province, and paladins like Elena Fyar are the best at winning hearts. Either way, we need the people's support before the tax collectors come round in January.
**Instructions:** Use **Improve** and **Give Alms** to raise Support. You must have at least **40** support in the province by January in order to collect gold and food in taxes.
**Highlight targets:**`SupportField`
### Implementation Notes
- Both dialogues go in `tutorial_strategic.json`
- Need new trigger events: `tutorial_battle_ended` and `tutorial_rebuild_support`
-`tutorial_battle_ended` should fire when the tutorial battle is removed from `RunningShardokGameModels` in `EagleGameModel.ApplyGameStateViewDiff`
-`tutorial_rebuild_support` should fire when available commands no longer include `HandleCapturedHeroCommand` (i.e. the captured heroes phase is done and the player has regular commands available)
- The "rebuild" dialogue should highlight the Improve and Give Alms buttons in the command panel (requires registering them as highlight targets)
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.