# Aeshnidae.Invasions

Town invasions. On a timer, a town with players in it is warned, then **besieged**: for a
fixed time, bands of retail monsters keep appearing at the edge of town and walking in on
its centre, more of them the more defenders there are; when the siege ends their leader
comes. Kill the boss inside its time and the town holds: every player who damaged an
invader earns a bonus scaled to their own level and a token. Otherwise (or if the boss gets
away) the invaders withdraw; nobody is paid.

Design agreed with Tom, 2026-09-27: towns, not the Hub of Awakening; **town tiers** (a
starter town gets drudges, not Virindi - the invaders live in the shared world, one
strength for everyone there, so the tier is the only protection for new players);
rewards are **kill XP as retail + a completion bonus + a token**. The same evening, after
the first live invasion: it "worked really well", but the monsters ran in too fast and the
fight was over in two minutes (three waves, cleared in 46 s, 24 s and 12 s). So an invasion
became a **siege of fixed length** ("hold the town") instead of three waves cleared as fast
as possible, and the invaders walked in. After a siege in the client (17:48, 69 killed in 22
bands): bands faster (every 15 s, was 30), the walk too slow - "between what we had before
and now" - so a **jog** (run at 0.6x: ~5 m/s, against ~9 running and ~1.2 walking), and
defenders counted within **400 m** of the centre (was 150).

**Status: live since 2026-09-27 15:58 EDT** (graceful restart; Tom: "restart now"), the siege
version since 16:31, the jog, 15 s bands and 400 m radius since 18:05, the effects and
fireworks since 18:15 (hot reloads), and on staging. One town, Holtburg, starter tier. On live
`AutoSchedule` is **off** - invasions are started by hand (`/invasion-start Holtburg`, or
`ace-cmd.sh 'invasion-start Holtburg'`) until the siege has been watched in the client,
then switched on. Live's `Discord.Channel` is `invasions`, the #invasions channel (webhook
wired with set-webhook.sh the same day); staging's is empty.

## Commands

```
/invasion                              which town is besieged and how it is going, or when the next is expected
/invasion-start <town> [warn minutes]  admin, console-safe: start one now (ignores cooldown and player count); 0 = at once
/invasion-stop                         admin: call it off - invaders removed, nobody paid
/invasion-status                       admin: timer, every town and why it can/cannot be chosen, defenders, every invader's place and state
/invasion-next <minutes>               admin: when the timer next looks for a town
/invasion-reload                       admin: re-read Settings.json (a running invasion keeps its town and set)
/invasion-skip                         admin, testing: end the warning (siege starts) or the siege (boss arrives) now
/invasion-smite [n]                    admin, testing: kill standing invaders (boss last), nobody credited
/invasion-here                         admin: your map coordinates and distance to each town's centre - for writing town entries
/invasion-fx <effect> | list           admin: play a client effect (PlayScript name) on yourself - to choose the effects
/invasion-fireworks [town]             admin: set off the victory fireworks round you, or a town's centre (console-safe with a town)
```

## How it runs

Everything happens on the world thread from a `LandblockManager.Tick` postfix, once a
second; commands (which may come from the console's thread) only queue work for it.

1. **Choose.** With `AutoSchedule` on, when `NextScheduled` comes (first `FirstDelayMinutes`
   after load, then `IntervalMinutes` ± `JitterMinutes` after each invasion ends) a town
   qualifies if it is `Enabled`, its wave set exists (bands and a boss), its
   `TownCooldownMinutes` has passed, and at least `MinPlayersInTown` players stand within its
   `TownRadius`. One is picked at random; none: look again in `RetryMinutes`. `AutoSchedule`
   is **off by default** so a fresh deploy never surprises anyone.
2. **Warn.** World broadcast (and Discord, if a channel is set) `WarningMinutes` ahead, with
   reminders at `WarningReminders` minutes left.
3. **Siege** (`SiegeMinutes`, 12). A band arrives at once and then every `BandEverySeconds`
   (15), picked from the set's `Bands` by `Weight`, each spawn `Count` + `PerExtraPlayer` x
   (players in town - 1), rounded down. A band comes from one random side of town, spread
   `BandSpreadDegrees` either way, on open ground `SpawnMin`-`SpawnMax` metres out
   (`Placement.RingSpot`: map coordinates -> global metres -> landblock, cell and terrain
   height, so the ring crosses landblock seams; building cells and unwalkable slopes are
   refused). While `MaxStanding` (25) invaders stand, bands are held back, so an undefended
   town does not fill up; the clock runs on regardless. Killing fast kills more - it does not
   end the siege early. Players in town hear the time to the boss, the slain and the standing
   every `UpdateEverySeconds`.
4. **March.** `MarchDelaySeconds` after spawning, each invader's **home** is set to a point
   within `MarchRadius` of the centre and it is sent there at a **jog**: ACE's own
   return-home move in the gait `MarchRun` picks (run; false = walk) at `MarchSpeed` (0.6)
   times its speed - the client told the gait (run rate 0 clears `CanRun` for a walk) and the
   multiplier, the server physics given the same gait and `Speed`. ACE's monster movement
   alone would always run at full speed (`GetMovementParameters` clears `CanWalk`). Measured
   on staging for drudges: full run ~9 m/s, run x0.6 ~5 m/s, walk ~1.2 m/s - a band takes
   10-15 s to reach the centre. An awake monster on its way home still looks for targets on every
   think, so it charges a player it sees - that chase, and the return after a fight, are
   ACE's own, at full speed. After a fight it goes back to the centre, not to where it spawned.
   Every invader carries a `Lifespan` of `InvaderLifespanMinutes` - if the mod dies
   mid-invasion ACE deletes them anyway.
5. **Awake.** ACE sleeps a landblock a minute after its last player leaves (no physics, no
   monster thinking) and later unloads it, deleting what stands there. While an invasion
   runs, every landblock holding a live invader is kept active (`Landblock.SetActive`), so
   the attack neither freezes nor evaporates when the town empties.
6. **Boss.** When the siege ends, the set's `Boss` spawns (the boss, flagged `Boss`, and its
   escort, scaled like a band) come from one side; whatever is still standing stays. The boss
   must die within `BossMinutes` (5).
7. **Win.** The boss dies. Any survivors withdraw. Every player whose damage to invaders
   (their own or their pet's, read from ACE's `DamageHistory` every second and at each death,
   highest value per invader kept) totals at least `MinContributionDamage` and who is online
   is paid: `GrantLevelProportionalXp(BonusPercentOfLevel, BonusMinXp, BonusMaxXp)` - ACE's
   AwardLevelProportionalXP, quest XP, so `xp_modifier` applies (0.10 pays 5% of a level at
   the half rate) and Radiance follows through Bank - and `TokensPerDefence` of `TokenWcid`.
   Offline defenders are logged, not paid.
8. **Fail.** The boss's time runs out, it leaves the world without dying, it cannot be
   placed, an admin stops the invasion, or `Enabled` goes false: the standing invaders are
   deleted, nobody is paid.

## Effects

Tom, 2026-09-27: "fireworks or any kind of visual effect". All retail, so no dat change
(`Effects.cs`); an empty name or 0 turns one off, and the preview commands above let Tom look
before choosing.

- `ArrivalEffect` (PortalExit) - played on each invader a second after it spawns (so every
  client near already has the creature), a portal flash at the edge of town.
- `BossEffect` (PortalStorm) - played on the boss instead.
- `DefenderEffect` (WeddingBliss) - on each paid defender at the win.
- **Fireworks**: `CelebrationCount` (6) copies of `CelebrationWcid` (8359, "Anniversary
  Effect" - the object retail's Fireworks Generator 19457 spawned in towns on its "Fireworks"
  event; the object is the display) set down evenly round the centre within
  `CelebrationRadius` (30 m) when the town holds, taken away after `CelebrationSeconds` (60),
  each with a Lifespan backstop. `/invasion-status` shows how many are up.

The PlayScripts are broadcast with `GameMessageScript` on the object. Chosen from their names;
**none has been looked at in the client yet** - Tom previews with `/invasion-fx` and
`/invasion-fireworks` and the defaults may change. The 56 retail `firework*` weenies
(22259-22314: big boom, blossom, boom, ring, shockwave, sparkle, spiral x 8 colours) are
Missile objects meant to be launched; not used, since how retail launched them is unknown.

## Not yet (the later steps)

- **More towns and tiers**: mid (Tumerok/Olthoi) and high (Virindi/Guruk) sets and their
  towns - which town sits in which tier is Tom's call.
- **The token** (`TokenWcid` 0): the weenie (3000xxx, ledger) and what an NPC trades for it
  are not decided. A full pack loses the token (logged).
- **Healers** earn nothing unless they also do damage - ACE records no heal attribution.
- **Records**: no table, no leaderboard board yet; results are in the log and `/invasion-status`.
- **The timer on live**: switch `AutoSchedule` on in live's Settings.json and `invasion-reload`
  once a siege has been watched with players in it.

## Tests

Staging, 2026-09-27, console only, nobody in town:

- Placement: 7 drudges round Holtburg 45-77 m out, on the ground in A9B4, A9B3 and A8B4.
- Unloading the mod mid-invasion removed its invaders.
- Three-wave version (retired): wave 1 (7), wave 2 (8), boss wave (5), boss dead -> held.
- Siege version (2-minute siege, a band every 20 s): six bands of 3 from six sides, the boss
  wave (Drudge Vagabond + 4 Robbers) on time, 23 standing; walking measured at ~1.2 m/s
  (50 -> 21 m in 25 s); `/invasion-smite` -> "Holtburg holds!". A second run with
  `BossMinutes` 0.5 and `/invasion-skip`: "Drudge Vagabond was not killed in time", 8 withdrew.

Live, 2026-09-27 16:03 (three-wave version, Tom in the client): warned, 10 + 12 + 6 drudges,
boss killed, Holtburg held, one defender paid; posted to #invasions. Over in two minutes -
hence the siege.

Live, 2026-09-27 17:48 (siege version, Tom): 22 bands, 2 held back at MaxStanding, the
boss wave at 12:00, 69 killed, held - "paid nobody", because the kills were ACE's `/smite all`,
which records no damage (expected; a smite is not a defence).

Staging, 18:12: `/invasion-fireworks Holtburg` put up 6 displays and took all 6 away after
60 s; a quick invasion (skip, smite) played the arrival and boss effects and set off 6 displays
at the win, no errors.

**Still owed (needs a client):** the jog as players see it, a defender paid at the end of a
fought siege, and what each effect and the fireworks look like.

## Deploy

```bash
# from C:\ACEPublic\Mods\src - source to the VPS, then build for staging (default) or live
tar --exclude=obj --exclude=bin -czf - Aeshnidae.Invasions | ssh aelrynth-vps 'cd ~/ace/Mods/src && tar -xzf - && cd Aeshnidae.Invasions && dotnet build -c Release'
ssh aelrynth-vps 'cd ~/ace/Mods/src/Aeshnidae.Invasions && dotnet build -c Release -p:InvasionsTarget=live'
bash /c/Aeshnidae/ops/publish-source.sh    # after any live build
```

The csproj sends every Linux build to `~/ace-staging/Mods/Aeshnidae.Invasions`, never live's
`~/ace/Mods`; live is `-p:InvasionsTarget=live`, on purpose. With `HotReload` true a rebuild
reloads the mod by itself (no restart) - but a reload mid-invasion deletes that invasion's
invaders, so check `invasion-status` first. The reload reads `Settings.json` at that moment,
so edit settings after the build and `invasion-reload`. A settings shape change (the siege
replaced `Waves` with `Bands` + `Boss`): move the old file aside before the build so the
reload writes the new defaults, then put back the deployed-only values (`Discord.Channel`).
