# Aeshnidae.Challenge

A challenge run is a personal copy of a retail dungeon whose cast is **drawn, not written
down**: five creature species whose creature-power-index score sits in a band, placed at the
dungeon's own spawn spots, nothing leaving a corpse, the strongest of the band at the far
end, and one chest behind it. Every run is a different cast. Every run is a fair one.

The first is **the Old Mine** (landblock `0190`), aimed at levels 1-10 and deliberately easy.

## The one idea

**Difficulty is the BAND, not the creatures.**

Resolve (`PropertyInt` 9053, owned by `Aeshnidae.Awakens`, read here and never written)
multiplies the score the roster is drawn around. It does not multiply any creature.

That is not a stylistic choice. A retail creature's skills, health and damage are written in
by hand, and only thirteen creatures in the world database carry our attribute-only marker
(`PropertyInt` 9041), so a tier multiplier applied to an arbitrary retail creature stretches
numbers nobody chose - which is the problem `C:\Aeshnidae\ops\build-creature-index.md` was
written to solve. Drawing a harder cast is the same difficulty curve without that problem,
and it has a property the multiplier never had: the appraisal window tells the truth about
every monster in the room, because nothing was touched.

A challenge copy is therefore marked **Unscaled** (below), so InstancesNoDat's own tier
scaler leaves it alone and `Aeshnidae.AwakenedRewards` sees a stock copy.

## How a roster is drawn

`Roster.cs`, in order:

1. **Target.** `BaseScore x ScorePerResolve ^ Resolve`. For the Old Mine, `0.40 x 1.6^R`.
   0.40 is the middle of what the index's own validation table calls a level-15 dungeon
   (Lugian Thrall 0.32, Drudge Slave 0.34, Lugian Overseer 0.48, Mudlurk Mosswart 0.51).
2. **Band.** Every scored creature within `[target / 1.35, target x 1.35]`, filtered to
   creatures the world database places somewhere (`PlacedOnly`) and that are not immobile.
3. **Shape.** The band's median damage-per-minute and median time-to-kill, then keep only
   the creatures within `ShapeFactor` (2.5) of **both**. A score is a product of those two,
   so it cannot tell a sponge from a glass cannon; this takes the extremes out of the pool
   before anything is drawn rather than mixing them in and hoping. 42 of 46 survive it at
   Resolve 0.
4. **Boss.** Drawn at random from the strongest `BossTopFraction` (0.15) of that pool - the
   top of the band, differently each run.
5. **Cast.** `RosterSpecies` (5) species drawn without replacement, boss excluded.

`Spawns` (18) of the shell's 69 spots are filled, spread evenly from the entry to the far
room, skipping the `EntryClearSpots` (2) nearest the portal so arriving is not being jumped.
The farthest spot of all is the boss's and never gets a minion.

Measured bands (index of 2026-10-13): 46 species in band at Resolve 0, then 80, 111, 112,
129, 106, 115, 93, 133, 155, 123 up to Resolve 10. `challenge-admin bands 0190 10` prints it.

## The reward

**Reward follows the draw, not the dial.** The payout multiplier is
`(mean score of the cast drawn / BaseScore) ^ RewardScorePower`, capped at
`MaxRewardMultiplier`. An unlucky soft draw inside a high band pays the soft draw's reward,
because what the player fought is the roster.

- **Experience:** `BossXp` (4,000 raw) x that multiplier, paid once to everyone in the copy
  when the boss dies, as `XpType.Quest` - so the server's `xp_modifier` 0.5 applies and a
  player receives half.
- **The chest** (wcid 3000141) is created in the copy when the boss dies, at the boss's
  **spot** rather than wherever the boss happened to die. It holds 1,000 pyreals and a
  retail `treasure_death` type 6 roll (tier 1, `lootQualityMod` 0 - the retail profile
  exactly). It is not locked: the boss is the lock, and a locked chest is a lockpick roll
  away from making the boss optional.

**Known limit:** the chest's treasure profile is tier 1 whatever the Resolve. The experience
scales with the band and the loot does not. Making it scale is a settings addition (a chest
per band), not a code change, and it is a design decision rather than a bug - say which way
before raising Resolve's price.

## What was changed in Aeshnidae.InstancesNoDat, and why it was unavoidable

Four small things, all of them general variant machinery rather than anything about
challenge runs.

| Change | Why it could not live here |
| --- | --- |
| `VariantSetting.Unscaled` + one clause in `Scaling.IsScaled` | The tier rule is stated once, there, and `AwakenedRewards` binds to `Scaling.TierFor`. An exemption asserted from outside would be a second answer to the same question. |
| `VariantSetting.NoCorpse` + `Scaling.LeaveNothing`, called from the existing `AddWorldObject` postfix | That postfix is the one call every spawn routes through - the initial pass, a generator mid-run, a relocation. The same reason the scaler lives there. A second mod patching it would be a second place for the hottest path in the most load-bearing mod to go wrong. |
| `Variants.Filling`, a provider hook consulted when a copy opens | A variant in `Settings.json` is a fixed cast. A drawn cast is a different list of `Adds` every run, which no settings file can hold. The hook takes **JSON** over host and runtime types only, so no InstancesNoDat type crosses a load context. |
| `Variants.Supply` made public | It is the only way to open a copy with a chosen filling and **nobody logged in**, which is the whole console test below. |

Everything after the hook is machinery a hand-written variant already used: `Removes`,
`Adds`, the guid allocation, the spawn thread, the ambient. This mod decides what goes in
and patches exactly two ACE methods of its own: `Creature.Die` and the world tick.

## Nothing to loot

`NoCorpse` in ACE does **not** mean what its name says. `Creature_Death.CreateCorpse` reads
it and, instead of making a corpse, rolls the creature's death treasure and drops every item
**loose on the floor**, plus its wielded treasure and its contain list. So three things go
together (`Scaling.LeaveNothing`):

- `NoCorpse` - no body.
- `DeathTreasureType` cleared - the loot roll produces nothing.
- `Bonded = Destroy` on what it holds - ACE's own "this goes with the body".
- The create list's `Contain` and `Treasure`-without-`Wield` entries dropped, because
  `GenerateTreasure` **creates** those fresh at death and bonding cannot reach them.

That last one is **replaced, never edited**: `ConvertToBiota` is called from `WorldObject`'s
weenie constructor with `referenceWeenieCollectionsForCommonProperties` true, and that hands
the biota the **cached weenie's own** collections for `PropertiesCreateList`,
`PropertiesEmote`, `PropertiesEventFilter` and `PropertiesGenerator`. Removing an entry in
place would change that creature for the whole server.

Experience and luminance for the kill are untouched. A fight pays; it is the stopping to
loot that is removed.

## The portal, and the retail Old Mine

The way in is portal **3000140**, on the Hub of Awakening's lowest row at the retail Mite
Maze portal's spot (`8A0201B6`, `40 -80 0.25273`). It is placed by InstancesNoDat's hub
variant `Adds`, as every hub door has been, so moving it is one edit to
`Mods/Content/tools/challenge_settings.py` and an `inst-reload-settings`.

`0190` is listed with **`OnlyVia: [3000140]`**, and that matters. The ladder dungeons were
deliberately left ungated so the retail landscape portals hand out copies too, because there
is no shared version of those twelve. The Old Mine is different: it is a real retail dungeon
people can walk into, and a challenge filling is not what someone stepping through the
retail Old Mine Portal (3632) asked for. Gated, only 3000140 hands out a copy; the retail
portal, a recall and a login all land on the real shared dungeon exactly as before.

Leaving is retail's own exit portal (3633), which is kept in the copy - one reason this shell
was preferred.

## The shell

`Shell.cs` reads the real landblock's placed rows once at load and keeps two things: the
creature spots (69, nearest the entry first, by straight-line distance from the portal's
destination) and the wcids a copy must leave out. Removals are worked out, not written down:

- every creature - the retail cast is what is being replaced;
- every generator - a link generator would keep putting retail monsters back all run (the
  Old Mine has one on a three-minute cycle);
- every chest - the payout is one chest at the end, and retail's five would pay before the
  boss and make the boss optional;
- every **locked** door - a level-1 character cannot pick an "excellent" lock and has no key.

Kept: the exit portals, unlocked doors, scenery. The real dungeon is untouched; a copy has
always been a separate set of objects in the same geometry.

For the Old Mine that comes to 14 removals and a boss spot 173 m in at `019001ED` - which is
where retail's own Zombie Dark Magus stands. Nothing told it to pick that spot; it is the
sanity check that "farthest from the entry" is not nonsense.

## Settings

`Settings.json` in the deployed mod folder. `Enabled` is **false** by default - off means the
filling hook is never installed and a copy of a challenge shell opens as the retail dungeon.
Every number is documented at the top of `Settings.cs`; the ones worth knowing are
`BaseScore`, `ScorePerResolve`, `BandWidth`, `ShapeFactor`, `RosterSpecies`, `Spawns`,
`BossTopFraction`, `BossScoreFactor` and `BossXp`.

`IndexFile` is relative to ACE's `content_folder` (`data/creature-index.json`), so a refreshed
index needs no mod deploy. Re-read it with `challenge-admin reload`.

## Commands

| | |
| --- | --- |
| `/challenge` | What this run drew and what to look for. Player. |
| `challenge-admin status` | The mod, the index, the shells, the binding. |
| `challenge-admin shell <lb>` | Spawn spots, removals, boss spot. |
| `challenge-admin draw <lb> <resolve> [n]` | Draw rosters without opening anything - the test for "runs differ but stay in band". |
| `challenge-admin bands <lb> [max]` | Band and pool size at each Resolve. |
| `challenge-admin variant <lb> <resolve>` | The JSON a copy would be filled with. |
| `challenge-admin open <lb> <copy> <resolve>` | Really open that copy with a drawn filling, no player needed. |
| `challenge-admin look <lb> <copy>` | What is standing in an open copy, with each creature's score, `NoCorpse` and death treasure. |
| `challenge-admin kill-boss <lb> <copy> [wcid]` | Kill the boss, or one creature of that wcid. |
| `challenge-admin chest <lb> <copy>` | What the reward chest holds, with the 9043/9042 stamps. |
| `challenge-admin tier <lb> <copy>` | InstancesNoDat's `IsScaled` / `Counted` / `TierFor` for any copy. |
| `challenge-admin runs` | Every run on record. |
| `challenge-admin reload` | Re-read settings, the index and the shells. |

All of `challenge-admin` is console-safe, so it works through `ace-cmd.sh` with nobody
logged in. That is deliberate: the thing that most needs proving - that two runs draw
different but equivalent casts - cannot be seen from inside the game without running the
dungeon twenty times.

## Known weaknesses

- **A drawn creature does not always fit a retail spot.** Staging logged
  `AddWorldObjectInternal: couldn't spawn ... Controlled Frost ... at 0x01900129` where a
  Zombie Revenant stood, and the copy opened with 18 of its 19 placements. For a minion that
  is one monster fewer. For the **boss** it would be fatal - no boss, no chest, an
  unfinishable run with nothing on screen to say why - so `Runs.EnsureBoss` looks for the
  boss ten seconds after the copy opens and puts one back, at its spot or beside the deepest
  creature that did spawn, logging an error if even that fails.
- **The boss is not always the hardest thing in the room.** It is drawn from the top 15% of
  the pool while minions are drawn from all of it, so a minion occasionally edges it. Raise
  `BossScoreFactor` above 1.0 to draw the boss from its own band a notch up.
- **The index's own weaknesses are inherited**, by design (nothing is recomputed here): the
  score saturates above ~550 skill, low-level casters are under-rated, and it is a ranking
  rather than a calibrated predictor. For a level 1-10 band none of that bites much.
- **Nothing filters for sense.** A Bronze Statue of a Reedshark and a town Guard are
  score-matched and will turn up. Retail levels are ignored on purpose (the index says why),
  which is how a level-126 Baby Olthoi with 20 health lands in a level 1-10 run quite
  correctly - but it does mean the cast is occasionally odd rather than wrong.
- **Two different wcids can share a name**, so a cast of five can read as four.
- **The run is repeatable with no cooldown.** Entry is free by design. Whether clearing it
  twenty times in an evening is a problem is a judgement to make after watching one.
