A boss is not entered like a wild encounter. Pressing the challenge in the den plays a themed entrance first, and the battle only becomes playable when it finishes. It is deliberately longer than the wild intro — roughly 4.2s against 2.65s — and it is skippable with a click at any point.
| Stage | At | What plays |
|---|---|---|
| 0 | 0.0s | The screen drops to near-black behind a radial veil and the whole overlay shakes three times. The boss art sits at 15% opacity, oversized, waiting. |
| 1 | 1.1s | The sigil ignites: an N-pointed star in the boss's own type colour, scaling up from 55% and turning slowly, with a contracting ring around it. 110 particles burst outward in the same palette. The boss art rises to full opacity. |
| 2 | 2.4s | The art settles from ×1.14 to ×1.00 and starts breathing. The name plate drops in: floor number, boss name, and its type line. |
| — | 4.2s | The entrance ends and the phase becomes select. The action menu arms 0.4s later. |
The entrance phase is bintro. It is never persisted — persistBattle only writes from
select onward — so a reload during the entrance cannot strand a player in it. A click anywhere calls
skipBossIntro(), which clears the timers and jumps straight to select.
The palette and the sigil's point count come from the boss's primary type
(Boss_Type_Primary in bosses.csv), through the BOSS_THEME table in the client.
Each entry is two colours — a body colour and a brighter highlight — plus the number of points on the star.
Every type in creatures.csv has a row; an unmapped type falls back to the codex gold. Examples:
| Type | Colours | Sigil | Boss |
|---|---|---|---|
| Aquatic | #2f7fd0 / #8fd8ff | 5-point | Floor 20 — Pelagos, the Trench Warden |
| Serpent | #4f9a5c / #9fe0a8 | 7-point | Floor 30 — Sethek, the Ossuary King |
| Amphibian | #3f9a7a / #9fe8cf | 5-point | Floor 70 — Morast, the Bogfather |
| Construct | #8a8f9a / #dfe6f0 | 4-point | Floor 100 — The Godforge |
Retuning a type is a one-line edit to BOSS_THEME; no art is involved, the sigil is a CSS clip-path
polygon generated by sigilPoly(points).
Preview any boss's entrance without spending a key:
/dev/?scene=bossintro on the floor below that boss.
Every boss carries two authored names and they are not interchangeable:
Boss_Character — the full name with its title, e.g. Mother Vesk, the Broodmother.
Used on the boss den screen, the entrance (bintro) card and the codex.Boss_Short_Name — the bare name, e.g. Vesk. Used on the battle name plate
(the life container above the HP bar) and nowhere else.Boss_Name — the den's own name, e.g. The Broodmother’s Hollow. Unrelated to the character.Boss_Name and Boss_Short_Name are “Über Vargr”, Boss_Character “Über Vargr, the Alpha”; never “Mutant”. The Atlas row reads Boss_Name from this sheet, the same cell the den plate reads.The short name is AUTHORED, never derived. Splitting Boss_Character on its comma is wrong:
floor 40 yields Mother Vesk, not Vesk. Any future boss must fill the column by hand.
bossData() falls back to Boss_Character when the column is blank, so a missing value degrades to the
old long-name behaviour rather than to an empty plate.
Why it matters mechanically: the battle plate is a fixed 300px box and plateNameFs() shrinks
the name to fit around the Lv chip and the type medals, down to a floor of 8px. A 28-character name drove the floor and
rendered unreadably small — reported live on floor 40 (feedback #11). Short names sit at the 19px maximum.
Source: codex/bosses.csv — columns Boss_Name, Boss_Character, Boss_Short_Name
Re-entering a boss room does not respawn the boss. After it dies a gravestone stands in its place and counts down from 10 seconds; at zero the boss returns in place. Keys are the rate limit, not the timer — the gravestone is a beat, not a gate.
BOSS_RESPAWN = 10000 ms, stamped per den in bossDown[floor] on the winning blow. Per-den, so killing one boss never hides another.bossRespawning() is Date.now() - bossDown[floor] < BOSS_RESPAWN. Walking out and back changes nothing; only elapsed time does.bossPresent is false) and the den shows the countdown. At zero the boss remounts and is challengeable again, provided a key is held.startGraveTick() repaints every 400 ms, paints once more after expiry so the boss returns in place, then cancels itself. It is started by the boss-win commit and re-armed by ensureTicker() if the floor comes back with a gravestone still running.Why the repaint is the whole fix (report #18). The respawn was always time-based, so re-entry never respawned anything. What was missing is that nothing repainted while it counted: challengeBoss() calls clearTimers(), so after the fight there was no timer left to redraw the room. It sat frozen on whichever second the fight ended on, and the boss appeared to return only after walking a floor and coming back — because the window had quietly elapsed on the way. Reported as "the CD to fight it again doesnt show and the game just stay there doing nothing".
The room has to say what it is waiting for. Until 2026-08-03 it did not. app.js computed challengeLabel — THE ALPHA STIRS… while the gravestone counts, ⚯ CHALLENGE THE BOSS when it is up, REQUIRES A BOSS KEY when it is up and you hold none — along with chOn, chColor, chBg, chBorder and chCursor, and the markup bound none of them. The only affordance in the room was the boss artwork itself. That is the rest of report #18: the gravestone fix restored the countdown, but nothing ever announced that the boss had returned, so the room still read as idle. The label is a sibling of both the bossPresent and bossRespawning gates, because it is the one element that must speak in every state.
Client: BOSS_RESPAWN, startGraveTick(), bossDownAtFor(), bossRespawning(), ensureTicker() in play/app.js. Probe: tools/probe_grave.py (6/6).
A boss floor is a den only. It has no wild map and no spawn pool of its own, and travel() enforces that outright: any trip that computes an x0 target steps past it (“boss floors have no wild map — never land on them”). A boss win therefore leaves the player standing in the den, in phase:'bossroom', with the gravestone counting in front of them. floorNum stays on the approach floor it was on before the fight.
maxFloor's job, not the room's. The win still raises maxFloor to the boss floor and still writes cleared[bf]. The battle event still reports floor: bf. What it no longer does is move the player's position onto a room that cannot be rendered.dispFloor resolves to bossFloor() whenever the player is in a boss fight, in the den, or on the victory screen of a boss win (vicWasBoss) — so “FLOOR 30” is what the player reads even though floorNum says 29.feedbackCtx() resolves its floor field the same way. It previously stamped floorNum raw, so a report written in the floor-30 den arrived labelled floor 29.bossExit() returns to floorNum (the approach floor) and rehydrates that floor's pool through refillPool(); travel('up') from the den lands on bf + 1.Why this was three separate bug reports (#38, #42, #45, measured 2026-08-07). The victory commit set floorNum to the boss floor and blanked floorPool, and toFloor() then opened that floor as an ordinary room. Three symptoms, one cause. The countdown never appeared — startGraveTick() was running the whole time (_graveT measured true) but the countdown lives in the den view and the den was not mounted. It appeared only after leaving and coming back — because coming back is what mounts the den. SPAWNS EXHAUSTED — 4/10 · NEXT IN 50s — the drought pill firing on a floor that has no spawns by design, against a pool that was empty and, measured over six seconds, was never going to fill. Reading the source would have found none of it; the room had to be opened and looked at.
Client: vicWasBoss, toFloor(), dispFloor, feedbackCtx() in play/app.js. Probe: tools/probe_bossreturn.py (9/9).
Every route into a boss den ends in enterBossRoom(), which derives its target from bossFloor() — the current floor's decade. It therefore cannot be called from another decade, and that constraint is what shapes all three doors.
floor % 10 === 9 with the portal open, the player walks in. bossFloor() already points at the right den, so enterBossRoom() is called directly.travelToBossRoom(bf, dir) parks floorNum at bf-1 and calls enterBossRoom() from the setState callback — one hop, nothing chained across a transit veil. Gate: cleared[bf] or maxFloor >= bf.maxFloor >= bf-1 — reaching the approach floor is the whole requirement to fight that boss — and absent otherwise rather than present and refusing. It routes through travelToBossRoom(bf, null, {approach:true}), where opts.approach widens the gate from bf to bf-1 so a key for a boss not yet beaten is not silently refused. The label is ENTER THE FLOOR N BOSS from anywhere in the tower.Status: LIVE on release since 2026-08-06 (0.34). The key button first shipped landing on the approach floor whenever the player stood outside that boss's decade, and it said GO TO FLOOR N when it did. Reported the same day (D, 2026-08-05): “using the boss key button is taking me to the room below it instead”. Reproduced by driving the real button from floor 45 with a floor-20 key — it landed on floor 19, phase 'floor'. Verified by tools/probe_keyboss.py (8/8; the unfixed build fails three, including the approach-only case a strict gate would have refused outright).
None of the three spend the key. A key is consumed by challengeBoss(), at the challenge, not at the door — walking into a den and leaving costs nothing.
Client: itemDetBoss(), keyBossOk(), travelToBossRoom(), enterBossRoom() in play/app.js
A boss on floor B takes the key that drops across the nine floors below it — B−1 down to B−9. The floor-10 boss takes the key found on floors 1–9, the floor-100 boss the key found on 91–99. Keys do not drop on boss floors themselves, which is why the drop table has no row for them.
Keys are strictly per zone. A key opens the boss it belongs to and no other. Until v0.34 a fallback counted any item tagged as a key whenever no zone row resolved — which was exactly on a boss floor — so a Grasslands key could open the floor-100 boss.
The count is asked of the boss, not of where you stand. The number shown, the challenge gate and the key actually spent all resolve against the boss being challenged. The older lookup used the floor the player occupied, which gives the right answer while climbing and the wrong one the moment a player stands past a boss and looks back at it — ten keys reading as zero from one floor above the boss they belonged to.
codex/bosses.csv — the separate uber_bosses.csv was folded into it and deleted, so one lookup resolves any boss by floor. LIVE on release since 0.48 (2026-08-13).Every boss kill drops one piece of gear and one egg (boss_loot.csv Gear_Guaranteed_Per_Kill, Egg_Guaranteed_Per_Kill), resolved by the server. The gear's grade is rolled on drop_grade_weights.csv one floor band ahead of the boss's own band (Gear_Grade_Band_Rule = one_band_ahead; Apex rows are one floor wide, floor 120 reads itself; Divine and above never drop); the egg's line is weighted by rarity (Egg_Line_Weight_C…SSR, 30/30/20/10/10) and its grade is band-rolled and capped at Legendary. The full tables are on droptables.html.
Since 2026-10-22 the game shows the same numbers itself: the CODEX window opens with a BOSS DROPS strip for the next boss on the player's climb — the guaranteed counts, each grade with its percentage for the band the boss rolls, and the egg-line split — read off the same two CSVs at load, the way the hatch screen shows its odds. It is a display, never a draw; the roll stays the server's. No real money or Wraithgold buys a chance outcome anywhere in the game (Wraithgold buys slots, pens and timed boons at fixed prices), so no paid-draw disclosure is owed; this strip exists because D asked for boss drop odds to be visible.
codex/boss_themes.csv has one row per type plus the _ fallback, with Accent, Glow, Sigil_Points, Plate_Veil and Plate_Fog. These drive the plate veil, the sigil over the den name, the card edges and, as before, the entry theatrics. A release pinned before the file keeps the old built-in colours. The den also shows a threat card: name, type chips, level, the Boss_HP the battle uses, how many actives fight beside the boss, and the signature mechanic on Apex dens. A spoils card shows the guaranteed gear + egg and the top gear-grade odds for this den, read the same way as the codex BOSS DROPS strip. CHALLENGE is now the big primary button, with the key cost and the keys held printed on it (“SPENDS 1 KEY · 3 HELD”). AUTOCHALLENGE is enlarged. All of this is display only: the fight still goes through the server's battle door. Research: docs/boss-ui-2026-10.md (D4 altar, Genshin Trounce, HSR Echo of War, MHW quest board). Gate: tools/probe_boss_ui.py, 1/14 → 14/14, phone landscape included.play/app.js codexDropOdds, data-driven from drop_grade_weights.csv / boss_loot.csv; probe tools/probe_t21_age.py 5a–5c). EN only: the shell has no Japanese string layer to put a JP label on.<Name> Prime; one identical to its creature stage keeps the name (The Sea Remembers, Silken Guillotine, Soul Elsewhere). 37 renames, the same on the tower boss and its über return: Reap the Fleeing, Run-Down, Wild Command, Ironquill Bulwark, Tide of Ruin, Drowned Court, Impenetrable Withdrawal, End Is Beginning, Buried Fang, Eternal Bind, Keening, Coronation Swarm, Pandemic Bite, Paralytic Silk, Titan’s Downstroke, Tyrant’s Verdict, Decree of Stone, Court of Rock, I Am the Storm, Sky Lacerate, Lightning Spear, Unbroken Gale, What Lies Beneath, Take You Under, Stitchflesh, Bog Lurk, Wake the Deep, Memory Flood, Lethean Ink, Neural Threading, Red Formality, Noble Feast, Arterial Map, Before There Was Light, Deprecation, Design Flaw, Anoint in Fire — each now “… Prime”, and the Kit_Synergy prose follows. bossData() strips the suffix when it resolves the moves.csv row, so a Prime still casts its creature move’s VFX. Same batch: the über bosses’ names (see system-void-apex). Gate: tools/probe_names.py, 1/6 → 6/6.bNow and the gravestone tick’s forceUpdate. The seconds are a ref’d span the gravestone tick writes on each whole second; the template commits once, at the expiry, so the boss still returns in place. The 10 s window and the key rule are unchanged. THE ABYSS DEEPENS (floor 119) now leaves at its own 6 s on one timer instead of riding on whatever re-rendered next. Gate: tools/probe_perf_2.py, commits over 5 s 24 → 0 and one at the expiry./api/backup/keymig, see admin-backend) mints the rows for legacy keys a save already holds, once, at the deploy — no player loses a key they hold.gpt-image-2.5-flare-2026-09-08. Label only; no prompt text changed.probe_vfxbank resolves 80/80 kit moves.codex/images/BOSS-080.png and its mutant derivative and advancing the pin retires the client entry. Gate tools/probe_bossface.py 18/18, red 8/18.codex/bosses.csv rows 102–120, and the reference to the deleted uber_bosses.csv is corrected with it. The Apex has been on release since 0.48 (2026-08-13); the sentence had been telling readers otherwise for four weeks. DESCRIPTIVE ONLY: no game behaviour changes.Boss_Gear_Grade is now authored on all twenty rows: Worn at floor 10 climbing to Legendary by floor 60 (the wild quality cap), then Legendary to Void across the Apex. wildSockets() applies the same slot table and rune formula gear uses. Measured boss Attack: floor 10 65→94, floor 50 160→575, floor 100 420→2,209, floor 120 420→3,723. This is a live balance change to floors 10–100 and their HP budgets were tuned against a gearless boss — it is on dev only and wants a pass before release.Core × AxisMult × QM. Measured: the floor-100 boss attacks at 420 while a grade-10 wild on floor 99 attacks at 1,150 — the boss hits at 37% of the trash one floor below it. bosses.csv gains Boss_Gear_Grade; bossData builds the tag through the same wildTags path a wild uses, and the boss unit carries it. BLANK on all ten tower bosses, so floors 10–100 are behaviourally unchanged — the tower fix is a live balance decision and is NOT taken here. Authored across the Apex only, Legendary at floor 102 climbing to Void at 120: Vargr 384→1,213 Attack, Eskaton 420→1,600.bosses.csv gains Active_Quality_Grade, blank on every tower boss so those are untouched, ramping Abyssal to Void across the Apex. Quality now multiplies their Attack, Defense and HP and drives their virtual tag through the grade factor, which is the only lever wild gear has. Measured: floor 102 Herne 397→502 Attack, floor 120 Ferrodrake 737→1,189 (×1.61) with HP 554→664.bossData built companions at level: bossFloor, which is correct to floor 100 and puts a Void Apex companion at level 102–120, above the cap every creature level obeys; their HP was sized off the same number. Both clamped at 100 in the client.codex/bosses.csv gains rows 102–120 and three additive columns (Signature_Name, Signature_Rule, AI_Tier, blank on the tower bosses). The separate uber_bosses.csv is deleted: a parallel file would have needed a parallel lookup, and the client already resolves a boss by floor out of this one. Verified end to end — all twenty boss floors resolve a boss row, a key row and a key item that exists.codex/uber_bosses.csv pairs each boss with an Apex floor, an HP budget derived from this page's anchor (13,750 at floor 102 rising to 39,000 at 120), a consumable key item, and a signature rule. Move powers are NOT inflated — the authored band ceiling is 160 and the kits carry over verbatim, so the uplift lives entirely in grades, HP, AI and the signature rule. Full rules on Void Apex.floorNum on the boss floor and toFloor() opened it as an ordinary room — a room that by travel()'s own rule has no wild map. One cause, three reports: no gravestone and no countdown anywhere (#38), the countdown appearing only after leaving and returning (#42), and SPAWNS EXHAUSTED — 4/10 on a floor with no spawns (#45). A fourth, #43, was the same field read raw: a report filed in the floor-30 den arrived stamped floor 29. floorNum now stays on the approach floor, vicWasBoss routes the win back into the den, and dispFloor and feedbackCtx() both resolve the den's own floor. New section above. Verified 9/9 in two real browsers.bossKeys integer, left from before keys were bag items, was removed — zero across 240 historical saves and never incremented.GO TO FLOOR N. It now routes through travelToBossRoom() with opts.approach, which widens that door's conquered-only gate to bf-1 so an unbeaten boss is reachable with its key. One label everywhere: ENTER THE FLOOR N BOSS. The “Reaching a den” section above is new — the three doors and the bossFloor() constraint that shapes them had never been written down. D report.challengeBoss() clears the timers, so the den froze until the player walked a floor and back. The countdown now drives its own repaint and cancels itself at zero, and the window went 30s → 10s. Keys remain the rate limit. D spec 2026-08-02; approved then, lost before it was committed, re-applied 2026-08-03. Verified 6/6.Boss_Short_Name column added; battle plate uses it. The battle name plate carried the full Boss_Character string, which shrank to the 8px font floor on the longer names (floor 40 reported by a player as unreadable). All ten rows now author a bare name and challengeBoss() builds the battle unit from it; den, entrance and codex are unchanged. Data change — DATA_PIN must advance in the release that carries it or the release client reads a pinned copy with no such column and silently keeps the long names.challengeBoss() previously set
phase:'select' directly: a boss appeared with no transition of any kind, while a wild encounter got a
shake and a zoom. It now routes through the bintro phase described above, themed on the boss's primary
type, longer than a wild intro and skippable. No balance or data change — presentation only.