# Aeshnidae.AllegianceBounty

The allegiance bounty. When any member of an allegiance completes a listed quest, the
allegiance gains an entry for that quest for **24 hours**: **+2% for a quest, +0.5% for a
kill task**, added to the **kill experience** of every member. The same quest completed
again by anyone in the allegiance while its entry is active adds nothing (once per quest
per allegiance per day); different quests stack; no cap by default. The bigger the
allegiance, the less each entry counts (`SizeBands`). Unsworn characters get nothing.

```
/bounty                          the allegiance's active entries (quest, who, time left), the raw
                                 total, the member count, the size multiplier, the effective percent
/bounty quests                   the quest list and what each kind is worth
/bounty-reload                   admin: re-read Settings.json and Quests.json; entries kept
/bounty-grant <player> <quest>   admin: record a completion for that player's allegiance (testing)
```

## The list

`Quests.json` (the mod reads it) and `Quests.md` (the same table for people): the wiki's
**Quests: Level 180** (61) and **Level 200** (33) categories, **Kill Tasks: Level 180** (31)
and **Level 200** (9), and the quests on the wiki's *Legendary Key Quests* list that sit
outside those bands (11, kind `legendary`, worth a quest). 146 entries, 131 with a flag.
Each entry: `name`, `kind` (`quest` / `killtask` / `legendary`), `level`, `flags`,
`percent`, `npc`, `note`. Percent is stored per entry so one quest can be tuned by hand;
an entry with no percent takes `QuestPercent` / `KillTaskPercent` by kind.

**The flag is the completion.** For every quest the turn-in NPC's emote sets were read in
the world database and the flag taken is the one **the final turn-in stamps in the same
emote set as the reward** - a kill task's completion timer (`...Wait`, `...Completed`),
never its kill counter; a quest's `...Completed` / `...Wait` / `...Finished` timer, never
a chain flag (Hoshino Fortress Infiltration lists Mareeno Donn's tower stamp, not the two
300M stages before it; Seed of Power the final 50M turn-in, not the three splinters).
Where the final hand-in stamps nothing at all, the note says what is used instead: the
**pickup stamp on the hand-in item** (`PropertyString.Quest`, written through the same
`QuestManager.Update` when it is looted - the pincers, the tusk, the turkey feather, the
Oubliette's page, the Experimental Bloodstone Shard), or the stamp one step earlier that
the reward cannot be had without (Lugian Assault: Commander Presk's; Hive Queen Assault:
the queen's remains). Fifteen entries have `flags: []` because nothing is ever stamped:
the four society rank tests (an IntStat, not a flag), John's *Legendary Quests* (he
erases the counter), *Rare Island Trophies* (heads with no stamp), *Honeyed Meads* (a
recipe), *Forgotten Tunnels of Nyr'leha* (its NPCs have no emotes in the database), and
the seven **Shadow Invasion dailies**, which all stamp the one shared timer
`ShadowInvasionWait` and are counted as one entry, *Shadow Invasion Daily*. The two
Nyr'leha kill tasks list their flags but their NPCs (Kagami, Bayani) have no emotes
either, so they cannot fire until that content exists.

## How it works

**Detection** is a prefix/postfix on `QuestManager.Update(string)` - the one method every
completion write in ACE goes through: the `StampQuest` emote (`Stamp` is `Update`),
`IncrementQuest`, a kill task's counter (`HandleKillTask` -> `Stamp`), the `UpdateQuest`
emote and an item pickup with a quest stamp. The solve count is read before and after,
and a completion is the count going up (the same shape Aeshnidae.Bank uses on the same
method): a repeat of a once-only quest at its maximum does not increment, a repeat before
a timer is up never reaches `Update`. The flag is matched case-insensitively against the
list; the player's allegiance (`player.Allegiance.MonarchId`) gains the entry unless it
already has it - which is also the guard against an NPC stamping the same flag twice in
one turn-in. `SetQuestCompletions` is not hooked: it initialises counters and writes no
listed flag.

**Application** is a prefix on `Player.GrantXP(long, XpType, ShareType)` adding
`amount x percent / 100` to `XpType.Kill`. GrantXP is the one funnel every award passes
through after `xp_modifier` and the enchantments and before the level, the allegiance
passup and the items read the amount, so one number lands and one kill line is printed.
**Radiance follows automatically**: Aeshnidae.Bank mints it in a postfix on the same
method and sees the bonused amount. **The fellowship split** - GrantXP with
`ShareType.Fellowship` hands the whole amount to `Fellowship.SplitXp`, which re-enters
GrantXP per member with the flag removed - is applied on the inner grants only: the
outer call is skipped (it grants nothing itself), a thread-static remembers what the
outer call was, and a member's `XpType.Fellowship` slice is bonused by *their own*
allegiance only when the outer grant was a kill (`ApplyToFellowshipShares`; a quest
turn-in's shares stay unbonused). Not applied to Quest (`ApplyToQuestExperience`, off:
the bonus must not pay for the quests that create it), Allegiance passup (computed from
the already-bonused kill), Proficiency, Emote, Admin. Nothing is said per kill.

**Storage** is `aeshnidae_allegiance_bounty` on the shard database, created on first run
the way Aeshnidae.Bank creates its table, credentials from ACE's `Config.js`:

```
id, monarch_Id, quest_name, flag, percent, completed_by, completed_by_Id, completed_at, expires_at
UNIQUE (monarch_Id, quest_name)      one live row per quest per allegiance; a completion after expiry overwrites
```

Active rows are cached in memory at enable and read there on every kill; a gain writes
through; expired rows are dropped from the cache as they are met and pruned from the
table hourly. Entries survive restarts and `/bounty-reload`.

**Size bands.** The raw total (sum of active entries, capped by `MaxPercent` if set) is
multiplied by the band the allegiance's total member count falls in - `Allegiance.
TotalMembers`, everyone sworn under the monarch, the number `/allegiance info` shows.
Defaults: up to 10 x1, to 25 x0.75, to 50 x0.5, to 100 x0.33, above x0.25 (the last band
is the fallback). A quest done by one of ten is a bigger deal than one of a hundred, and a
huge allegiance would otherwise hold every quest on the list active for ever.

## The personal layer

On top of the allegiance's bounty, each character earns a bonus of its own for the day's
questing: **+0.1% of kill experience for every distinct quest flag stamped today, capped
at +20%**, resetting at **17:00 server time** (`PersonalResetHour`; the day is the
reset-to-reset window, so a stamp at 16:59 belongs to the day that began at 17:00 the
day before). **Any** quest flag counts (`PersonalAnyQuest`; false limits it to
`Quests.json`) - quest stamps are abundant by design and the per-stamp value is small -
but **each flag counts once per day**, so re-stamping the same cheap flag is worth
nothing; that is the guard against looping one stamp to the cap. Detected in the same
`QuestManager.Update` postfix (any Update that wrote a flag - a new registry row or a
count going up - the first time today), applied in the same `GrantXP` prefix, added to
the allegiance percent (additive; same Kill-only and fellowship-share rules; Radiance
follows). Nothing is said per stamp; one line when the cap is reached. Stored in
`aeshnidae_personal_bounty` (`character_Id, day, flag, stamped_at`,
`UNIQUE(character_Id, day, flag)`), the character's current day cached at login (and
lazily on first touch after a hot reload), days older than two pruned hourly. `/bounty`
shows it: "today: N distinct quest stamps, +X% (cap 20%), resets at 17:00".

## Settings

`Enabled`, `QuestPercent` (2), `KillTaskPercent` (0.5), `HoursActive` (24), `MaxPercent`
(0 = uncapped), `SizeBands`, `AnnounceToAllegiance` (true: "<Name> completed <Quest>: the
allegiance's bounty is now +X% for 24 hours" to every online member),
`ApplyToQuestExperience` (false), `ApplyToFellowshipShares` (true), `QuestsFile`
(`Quests.json`); the personal layer: `PersonalEnabled` (true), `PersonalPercentPerStamp`
(0.1), `PersonalMaxPercent` (20), `PersonalResetHour` (17), `PersonalAnyQuest` (true).

| File | |
| --- | --- |
| `Bounty.cs` | The allegiance layer: the cache, the gain, the summary, the per-kill percent. |
| `Personal.cs` | The personal layer: the day, the distinct stamps, its table. |
| `BountyDb.cs` | The table. |
| `Patches.cs` | `QuestManager.Update` (detection) and `Player.GrantXP` (application). |
| `QuestList.cs` | `Quests.json`, indexed by flag. |
| `Commands.cs` | `/bounty`, `/bounty-reload`, `/bounty-grant`. |
| `Settings.cs` | The terms. |
| `Quests.json`, `Quests.md` | The list, for the mod and for people. |
