Sets Tutorial Panel's Bottom offset to 650 in Shardok battles (vs 400
default) so the panel doesn't cover the hex grid. Both values are
Inspector fields — no runtime save/restore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete TutorialModalPanel and TutorialCanvasBuilder (old runtime-built
modal system). Remove SetPanelPosition from DialoguePanelController —
it was overriding the editor layout by setting anchoredPosition.y = 0,
causing the panel to shift and buttons to jump between steps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The battle tutorial's first step is short, so -height/2 placed the
bottom edge high on screen. Subsequent longer steps then grew off
the top. Use a fixed -200px bottom offset from center instead, so
the bottom position is consistent regardless of first step height.
Also restore Top anchor to pivot Y=1 (grows downward from top edge),
since growing upward from the top of the screen doesn't make sense.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tutorial sequences change anchors between steps (e.g., Center for
welcome, Right for province info, Top for commands). Skip
repositioning only when the same anchor and target are reused
between consecutive steps, so the panel properly moves for battle
tutorials while keeping the bottom edge fixed within same-position
sequences.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of recomputing position every step (which fights the pivot),
just position once on step 0. ContentSizeFitter + pivot Y=0 (set in
the inspector) grows the panel upward, keeping the bottom edge fixed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pivot change alone wasn't enough — PositionPanel recomputes
anchoredPosition.y from height on every step, so the bottom still
shifted. Now we save anchoredPosition.y on step 0 and restore it
on subsequent steps, keeping the bottom edge (and buttons) fixed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bottom-anchor the panel pivot (Y=0) so the bottom edge stays fixed
when ContentSizeFitter resizes the panel between tutorial steps.
Buttons no longer shift position on Continue/Skip.
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>