Commit Graph
100 Commits
Author SHA1 Message Date
93ad8c4ae1 Reintegrate SparklePlugin into main workspace (#6308) (#6311)
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>
2026-02-27 13:16:02 -08:00
8cb75d48e2 Add giant beast type using 3x-scaled peasant (#6308)
* 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>
2026-02-27 13:05:59 -08:00
97bf29d4a2 Fix Bazel Xcode version caching in CI (#6309)
* 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>
2026-02-27 12:43:14 -08:00
38c3fbd6bd Add psychopath to beast effects coverage doc (#6310)
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>
2026-02-27 09:49:46 -08:00
47f936df05 Upgrade grpc from 1.74.1 to 1.78.0 (#6307)
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>
2026-02-27 09:27:13 -08:00
85a3f4bc39 Upgrade dragon beast to 3D model with full animation suite (#6306)
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>
2026-02-27 09:25:37 -08:00
ce49304406 Add Polytope 3D character archetypes for human beast animations (#6304)
* 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>
2026-02-27 07:36:59 -08:00
c9afc4a6b8 Convert existing asset packs to Git LFS (#6303)
* 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>
2026-02-27 07:18:02 -08:00
90f20e25b2 Add psychopath beast type (#6301)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 06:58:27 -08:00
d6c3abd505 Reduce raccoon animal scale from 15 to 5 (#6300)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:05:46 -08:00
ee31265ff6 Clean up warmup-binary artifacts after deploy (#6299)
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>
2026-02-26 20:58:47 -08:00
e289913e18 Add raccoon beast animation (Sketchfab 3D model) (#6298)
* 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>
2026-02-26 20:58:19 -08:00
62cba309d8 Replace Unity attributions.json with symlink to canonical copy (#6297)
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>
2026-02-26 15:00:13 -08:00
25d1d2b268 Note controller override in honey badger doc entry (#6296)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:14:11 -08:00
cd7e21cfa4 Add honey badger beast animation (3D model) (#6293)
* 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>
2026-02-26 14:01:27 -08:00
5bb6b4ca9d Add attribution for new beast animation asset packs (#6294)
* 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>
2026-02-26 13:33:55 -08:00
9030c455e0 Add no-chained-commands rule to CLAUDE.md (#6295)
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>
2026-02-26 11:41:27 -08:00
49c14be159 Add clown beast animation with both Clown Pack models (#6292)
* 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>
2026-02-26 10:08:18 -08:00
e63735f428 Update beast effects doc for ogre/troll coverage (#6291)
* 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>
2026-02-26 08:48:57 -08:00
f4b9a9268c Refactor BeastUtils to use pattern matching instead of asInstanceOf (#6290)
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>
2026-02-26 08:39:22 -08:00
272badda47 Balance beast danger levels and add powerExponent (#6288)
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>
2026-02-26 08:34:46 -08:00
080329e369 Fix NullReferenceException when clicking map before model loads (#6289)
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>
2026-02-26 08:32:31 -08:00
f9afcea9f7 Add ogre/troll beast animation using Orc-Ogre 3D asset pack (#6287)
- 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>
2026-02-26 08:15:49 -08:00
cec058c369 Wire up animations for 8 new beast types (#6286)
* 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>
2026-02-26 07:35:57 -08:00
679b5ef1ce Add 8 new beast types with matching asset pack animations (#6285)
New beasts from Animal Pack Deluxe:
- crab (0.04): coastal swarm, low power, agriculture damage
- frog (0.10): plague swarm, very high count, minimal power
- rabbit (0.20): crop-destroying swarm, high agriculture damage
- salamander (0.02): rare, poisonous, moderate power
- scorpion (0.15): venomous swarm, like spiders
- stag (0.04): territorial wild deer, moderate power
- wild goat (0.10): mountain crop raiders, agriculture damage
- wild pig (0.10): aggressive crop destroyers

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 07:07:36 -08:00
f44b7351b4 Add spawn likelihood to uncovered beast types in docs (#6284)
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>
2026-02-26 07:01:52 -08:00
03e82ae635 Update beast effects docs with full animation coverage (#6283)
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>
2026-02-25 21:25:18 -08:00
3e4aa6d6d2 Update animal effect prefab scales from 20 to 15 (#6282)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 21:21:37 -08:00
6bfbf38fb4 Add animal wandering effects for bear, boar, crocodile, snake, wolf (#6281)
* 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>
2026-02-25 21:08:55 -08:00
28560940cb Add Old Marek commentary when King's heroes depart in tutorial (#6280)
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>
2026-02-25 20:40:20 -08:00
38d097461d Add wandering monster animations for beast events (#6279)
* 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>
2026-02-25 20:32:09 -08:00
ce6bc0b975 Update Eagle appearance trigger for Hedrick and Sadar (#6278)
* 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>
2026-02-25 20:00:05 -08:00
8ce0a18d09 Fix horseshoe print orientation and duel accepted animation (#6277)
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>
2026-02-25 19:31:18 -08:00
2befe8b67f Add tutorial-specific initial chronicle text (#6276)
* 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>
2026-02-25 19:27:20 -08:00
76cf6f0624 Fix SupportField highlight in tutorial dialogue (#6272)
* 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>
2026-02-25 19:08:47 -08:00
b0c409d62c Fix support=0 not being applied to weak King provinces (#6275)
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>
2026-02-25 19:01:34 -08:00
b23ee1485b Simplify tutorial battle: no attackers can flee (#6274)
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>
2026-02-25 17:37:59 -08:00
03874da321 Add Hedrick's faction to tutorial AI controllers (#6273)
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>
2026-02-25 17:30:05 -08:00
7ca5db5509 Guarantee duel acceptance in tutorial battles (#6271)
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>
2026-02-25 17:07:45 -08:00
2364db42a7 Split reinforcement dialogue into per-hero events (#6268)
* 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>
2026-02-25 16:59:38 -08:00
cb1391e417 Make Ranil reinforce from Tarn's direction (#6270)
* 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>
2026-02-25 16:04:08 -08:00
d940393f3f Refactor beast effect selection for extensibility (#6269)
* 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>
2026-02-25 15:42:08 -08:00
6e484f14ca Add dragon beast animation on eagle map (#6267)
* 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>
2026-02-25 15:30:43 -08:00
e421bac79c Add Hedrick faction in Kojaria, move Bridget to Alah (#6266)
* 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>
2026-02-25 13:40:03 -08:00
960344a9d6 Fix tutorial reinforcements all arriving at once (#6265)
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>
2026-02-25 12:34:11 -08:00
5eb195188e Fix tutorial province stats not being applied from JSON (#6264)
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>
2026-02-25 11:28:35 -08:00
454aa7edf6 Add weak King's provinces to show collapsing authority in tutorial (#6263)
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>
2026-02-25 11:08:01 -08:00
aa3bcbb25d Fix fear animations and sound routing (#6262)
* 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>
2026-02-25 07:33:33 -08:00
05c1a49645 Balance tutorial battle difficulty and Onmaa resources (#6261)
- 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>
2026-02-25 06:58:47 -08:00
665a4f1e04 Fix tutorial HeroGenerator crash when spawning unaffiliated heroes (#6260)
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>
2026-02-25 06:40:07 -08:00
8fe0779422 Add duel challenge/declined animations and programmatic test buttons (#6259)
* 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>
2026-02-24 21:30:34 -08:00
1456276710 Allow manual workflow_dispatch to deploy Unity builds (#6258)
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>
2026-02-24 21:12:54 -08:00
8ed3a168dc Replace gh CLI with curl in installer artifact cleanup (#6257)
The self-hosted runner doesn't have gh installed. Use curl + GitHub
REST API directly instead.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:12:20 -08:00
6f03ef86f0 Migrate client update system to SHA-addressed blob storage (#6253)
* 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>
2026-02-24 16:50:14 -08:00
d71ad680c5 Add Scala tests for TutorialStrategicEvents (#6256)
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>
2026-02-24 13:51:04 -08:00
a4c8c0670e Add comprehensive tests for TutorialBattleController (#6255)
Add C++ unit tests for the Shardok tutorial battle event system:

- RoundTrigger tests: fires at/after trigger round, multiple events
- UnitsLostTrigger tests: threshold, counts statuses, player filtering
- DamageTakenTrigger tests: threshold, sum across units, initial_sizes
- UnitKilledTrigger tests: by unit_id, by eagle_hero_id
- ReinforcementsAction tests: activates pending units, places directly
- FleeAction tests: generates results, specific units, by hero_id
- Fire-once semantics tests: events don't re-fire
- Controller state tests: disabled controller, default constructor

28 total tests covering all trigger types and action execution.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-24 13:50:42 -08:00
8f219f1f56 Add unit tests for TutorialBattleConfig generation (#6254)
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>
2026-02-24 13:14:02 -08:00
8ce2adfca9 Remove dead battalion backstory update code (#6252)
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>
2026-02-24 09:09:39 -08:00
e2bc6df62e Extend unaffiliated hero backstory visibility to new province owner (#6251)
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>
2026-02-24 07:06:12 -08:00
dc5d342b17 Read What's New data from S3 instead of CDN URL (#6250)
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>
2026-02-24 07:02:46 -08:00
b0ba44c4d5 Add tutorial dialogue when January taxes are collected (#6241)
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>
2026-02-24 06:49:51 -08:00
d3c3df7772 Replace GraphQL branch creation with plain REST API (#6249)
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>
2026-02-24 06:48:40 -08:00
fb2fcf4497 Replace shift-click and Melee button with right-click to melee attack (#6246)
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>
2026-02-24 06:46:49 -08:00
7804d6e160 Add tutorial dialogue for expansion guidance (#6242)
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>
2026-02-23 20:59:28 -08:00
8836c8f65a Add tutorial dialogue for The Eagle's appearance (#6240)
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>
2026-02-23 20:58:00 -08:00
21891c2627 Use GraphQL API for branch creation in map editor PR flow (#6244)
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>
2026-02-23 06:42:34 -08:00
18c4c08583 Replace low-bravery tutorial heroes with high-bravery ones (#6239)
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>
2026-02-23 06:29:25 -08:00
d1eb51f83d Fix stuck BattleAftermath when Tarn removed with other captured heroes (#6238)
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>
2026-02-22 22:20:45 -08:00
1cde547501 Pace combat tutorials to show one at a time (#6236)
* 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>
2026-02-22 18:46:39 -08:00
fc47f21f33 Space out tutorial reinforcement arrivals across rounds (#6237)
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>
2026-02-22 18:46:27 -08:00
113b6eb564 Fix tutorial attackers fleeing: only Tarn should be able to flee (#6235)
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>
2026-02-22 18:37:58 -08:00
ff650fb82e Persist Battle button highlight after dialogue is dismissed (#6234)
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>
2026-02-22 17:23:07 -08:00
3731a2886e Add "Create PR" button to map editor (#6233)
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>
2026-02-22 17:21:00 -08:00
2a2cd37fbe Add proper names to tutorial battalions and set Onmaa support (#6232)
- Rename Sadar's battalions from generic type names to:
  - "Rakon's Loyalists" (Light Infantry)
  - "Onmaa Defenders" (Light Infantry)
  - "Hunters of the Steppe" (Longbowmen)
- Add names to reinforcement battalions:
  - "Hedrick's Bowmen" (Longbowmen)
  - "Ranil's Riders" (Heavy Cavalry)
  - "Fyar's Vanguard" (Heavy Infantry)
- Set Onmaa's starting support to 23

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-22 17:11:26 -08:00
97b9a293ab Add initial visibility support for tutorial battles (#6231)
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>
2026-02-22 17:01:32 -08:00
47ab61d0ab Add post-battle tutorial dialogues (#6227)
* 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>
2026-02-22 17:00:11 -08:00
19ce34eae4 Change tutorial start date to May 371 (#6230)
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>
2026-02-22 16:44:03 -08:00
03114b8c98 Fix tutorial province food/gold and reinforcement loyalty (#6229)
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>
2026-02-22 16:40:35 -08:00
22d4cb755f Replace inappropriate Talismangobbler headshot (#6228)
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>
2026-02-22 16:24:22 -08:00
473d1c36fb Replace Silvio with Marty McPolitics in tutorial enemy army (#6226)
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>
2026-02-22 16:12:12 -08:00
9c06af6963 Warn before navigating away from unsaved map editor changes (#6225)
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>
2026-02-22 15:54:20 -08:00
9c7c6b5c03 Increase Sadar Rakon's starting battalion size and armament (#6224)
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>
2026-02-22 14:46:46 -08:00
545433342c Make Ikhaan Tarn disappear after tutorial battle (#6222)
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>
2026-02-22 14:44:23 -08:00
a534b363be Allow "tarn" user to access tutorial button in lobby (#6223)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 14:39:51 -08:00
312d7f0852 Add tutorial triggers for duel, engineer bombard, and hide (#6220)
* 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>
2026-02-22 14:35:44 -08:00
7f873732ca Simplify unit type list to icons and names only (#6221)
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>
2026-02-22 14:33:01 -08:00
9e797d73b8 Fix tile image vertical positioning in map editor (#6218)
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>
2026-02-22 14:32:43 -08:00
87e7def2fb Add setup-node step for JS syntax check in CI (#6219)
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>
2026-02-22 14:25:31 -08:00
7b71ff309e Only allow Ikhaan Tarn to flee in tutorial battle (#6217)
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>
2026-02-22 14:19:21 -08:00
3da0688310 Only trigger charge tutorial when Old Marek can charge (#6215)
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>
2026-02-22 14:10:22 -08:00
33be85fdc5 Add JS syntax checking to CI and merge conflict pre-commit hook (#6216)
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>
2026-02-22 14:07:09 -08:00
e7d76d99b7 Add surviving reinforcement units to province after battle (#6213)
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>
2026-02-22 14:01:14 -08:00
d4089ae399 Remove stray git conflict marker from map_editor.js (#6214)
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>
2026-02-22 13:59:22 -08:00
15955ff139 Fix race conditions in reinforcement hero headshot resolution (#6210)
* 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>
2026-02-22 13:58:49 -08:00
fed04aec7c Scale tile images to fill pointy-top hex height (#6212)
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>
2026-02-22 13:49:13 -08:00
625155a6c6 Scale tile images to fill pointy-top hex height (#6211)
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>
2026-02-22 13:40:12 -08:00
68a6037cc6 Add light cavalry to Ikhaan Tarn's tutorial army (#6206)
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>
2026-02-22 13:31:54 -08:00
b186c059f4 Fix tile image vertical centering in map editor (#6209)
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>
2026-02-22 13:17:47 -08:00
be275a5aaf Revise tutorial dialogue: Tarn characterization, dragoons, reinforcement intros (#6207)
- 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>
2026-02-22 13:15:04 -08:00
9977fc965a Fix reinforcement hero headshots and make bridge scale configurable (#6205)
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>
2026-02-22 13:02:58 -08:00