Expedition autopilot Phase 4 + rogue sneak attack + TwinBee voice sweep

Tedium-removal pass driven by live play feedback. Three big threads:

Expedition autopilot Phase 4 — background auto-run + harvest-until-dry:
- New expeditionAutoRunTicker walks active expeditions every 15min
  (5min tick, per-expedition CAS on new last_autorun_at column). Skips
  combat sessions, briefing/recap quiet windows, expeditions <30min old.
- Walks up to 3 rooms/tick with compact narration; suppresses DMs when
  0 rooms walked or just hitting the per-tick cap (no "stretch complete"
  filler). Player only hears from the bot when a real decision is needed.
- Compact mode: one-line combat narration for trash/elite, auto-resolves
  elite doorways via the forward-sim engine (boss still pauses). Threaded
  via new advanceOnceWithOpts → resolveRoom → resolveCombatRoom.
- Auto-harvest now grinds each Common/Uncommon node until dry (cap at 8
  attempts/visit), mirroring manual !scavenge retries. Rare+ still pauses.
- Ambient ticker: anti-repeat by Kind via new last_ambient_kind column
  (avoids two pack_rat DMs in a row when the pool only has 6 lines).
- !expedition go <n> now routes to the fork-choice handler when active +
  numeric; fork footer rewritten to suggest !expedition go instead of
  !zone go. Boss/Elite doorway: formatNextRoomMessage routes the action
  hint by next room type and autopilot loop breaks via new nextRoomType
  field so "Room X/Y — Boss" doesn't double-print with contradictory
  hints (!zone advance vs !fight).
- runAutopilotWalk extracted from expeditionCmdRun so foreground and
  background share the loop body.

Rogue Sneak Attack actually exists now:
- Audit found the rogue's "Sneak Attack" passive was AutoCritFirst +
  5% damage rider. No Nd6, ever. Phase 2 Monte Carlo masked this
  with a small flat buff; the class's defining mechanic never matched
  its tooltip or 5e identity.
- Added SneakAttackDie int to CombatModifiers, per-hit Nd6 in
  combat_primitives.go (same lane as DivineStrikePerHit). Scales with
  level: 1d6 at L1-2 ... 4d6 at L7-8 ... capped at 10d6 at L19-20.
- AutoCritFirst + 5% rider retained as bonuses on top of working sneak
  attack — preserves the opener-burst feel.
- L7 rogue expected per-hit ~8 → ~22 damage. Valdris fight math goes
  from "16 rounds to kill, die in 8" to winnable.

TwinBee voice sweep (Phase B3):
- ~530 line changes across 24 files replacing third-person "TwinBee
  notes/files/tracks/respects/..." constructions with first-person
  inside flavor string literals. Code identifiers (TwinBeeLine,
  twinBeeLine, etc.), chat-prefix labels (🎭 **TwinBee:**), item names
  (TwinBee's Bell), achievements, and game-title references in
  fun.go (Konami TwinBee ship) left intact.
- Catches a real bug: Valdris fight rendered "TwinBee marks the
  Legendary Resistance" mid-combat in third person, contradicting
  the Phase B2 convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-16 12:40:17 -07:00
parent f424300d87
commit 41adfce9f1
39 changed files with 1133 additions and 638 deletions

View File

@@ -96,7 +96,7 @@ type ZoneDefinition struct {
LevelMax int
Faction string
Atmosphere string // one-line biome description
Hook string // entry-room narration seed (TwinBee voice, italic)
Hook string // entry-room narration seed (I voice, italic)
MinRooms int
MaxRooms int
Enemies []ZoneEnemy
@@ -213,7 +213,7 @@ func zoneGoblinWarrens() ZoneDefinition {
LevelMax: 3,
Faction: "Goblins, Hobgoblins",
Atmosphere: "Low ceilings, torchlight, crude traps, cackling in the dark.",
Hook: "A network of fetid tunnels burrowed beneath the Merchant's Road. The smell arrives before the sounds — smoke, rot, and something worse. TwinBee advises keeping one hand on your blade.",
Hook: "A network of fetid tunnels burrowed beneath the Merchant's Road. The smell arrives before the sounds — smoke, rot, and something worse. I advise keeping one hand on your blade.",
MinRooms: 6,
MaxRooms: 7,
Enemies: []ZoneEnemy{
@@ -257,7 +257,7 @@ func zoneCryptValdris() ZoneDefinition {
LevelMax: 3,
Faction: "Undead",
Atmosphere: "Stone corridors, dripping water, candles that shouldn't still be burning.",
Hook: "The iron gate hangs open — someone left in a hurry. Carved into the stone above: \"HERE LIES VALDRIS. DO NOT.\" The rest has been chiseled away. TwinBee declines to speculate.",
Hook: "The iron gate hangs open — someone left in a hurry. Carved into the stone above: \"HERE LIES VALDRIS. DO NOT.\" The rest has been chiseled away. I decline to speculate.",
MinRooms: 6,
MaxRooms: 7,
Enemies: []ZoneEnemy{
@@ -313,7 +313,7 @@ func zoneForestShadows() ZoneDefinition {
LevelMax: 5,
Faction: "Beasts, Fey-corrupted creatures, Bandits",
Atmosphere: "Ancient forest, twisted paths, eerie silence, bioluminescent fungi, things in the canopy.",
Hook: "The forest was beautiful once. Travelers still say so, usually right before they stop saying anything at all. The trees lean in when you're not looking. TwinBee has noted this is not a metaphor.",
Hook: "The forest was beautiful once. Travelers still say so, usually right before they stop saying anything at all. The trees lean in when you're not looking. I have noted this is not a metaphor.",
MinRooms: 6,
MaxRooms: 8,
Enemies: []ZoneEnemy{
@@ -376,7 +376,7 @@ func zoneSunkenTemple() ZoneDefinition {
LevelMax: 5,
Faction: "Kuo-toa, Water Elementals, Aboleth-touched",
Atmosphere: "Flooded stone chambers, barnacled pillars, salt smell, alien glyphs, things that swim in the dark water.",
Hook: "The tide went out thirty years ago and never fully came back. The temple stayed wet anyway. Something down there keeps it that way. TwinBee suggests waterproofing your spellbook.",
Hook: "The tide went out thirty years ago and never fully came back. The temple stayed wet anyway. Something down there keeps it that way. I suggest waterproofing your spellbook.",
MinRooms: 6,
MaxRooms: 8,
Enemies: []ZoneEnemy{
@@ -422,7 +422,7 @@ func zoneManorBlackspire() ZoneDefinition {
LevelMax: 8,
Faction: "Undead, Shadows, Vampiric",
Atmosphere: "Victorian decay, impossible architecture, portraits whose eyes follow movement, cold spots, locked rooms that weren't locked before.",
Hook: "The manor has been for sale for eleven years. Every buyer has either left immediately or not left at all. The real estate listing describes it as 'full of character.' TwinBee finds this accurate.",
Hook: "The manor has been for sale for eleven years. Every buyer has either left immediately or not left at all. The real estate listing describes it as 'full of character.' I find this accurate.",
MinRooms: 7,
MaxRooms: 9,
Enemies: []ZoneEnemy{
@@ -490,7 +490,7 @@ func zoneUnderforge() ZoneDefinition {
LevelMax: 8,
Faction: "Fire Elementals, Constructs, Salamanders, Azers",
Atmosphere: "Volcanic caverns, rivers of cooling lava, ancient dwarven stonework, the constant bass note of something very large moving below.",
Hook: "The dwarven forge-city of Kharak Dûn was not abandoned. It was sealed from the outside. TwinBee does not have information on what they were sealing in.",
Hook: "The dwarven forge-city of Kharak Dûn was not abandoned. It was sealed from the outside. I do not have information on what they were sealing in.",
MinRooms: 7,
MaxRooms: 9,
Enemies: []ZoneEnemy{
@@ -547,7 +547,7 @@ func zoneUnderdark() ZoneDefinition {
LevelMax: 12,
Faction: "Drow, Mind Flayers, Beholders (far), Ropers, Hook Horrors",
Atmosphere: "Absolute darkness, phosphorescent mushroom groves, vast underground seas, carved drow cities in the distance, things older than the surface world.",
Hook: "There is a world below the world. It has its own cities, its own wars, its own sky — which is stone, and has never once been kind. TwinBee speaks more quietly here. Something might be listening.",
Hook: "There is a world below the world. It has its own cities, its own wars, its own sky — which is stone, and has never once been kind. I speak more quietly here. Something might be listening.",
MinRooms: 8,
MaxRooms: 10,
Enemies: []ZoneEnemy{
@@ -604,7 +604,7 @@ func zoneFeywildCrossing() ZoneDefinition {
LevelMax: 12,
Faction: "Hags, Redcaps, Will-o-Wisps, Fomorians, Unseelie Fey",
Atmosphere: "Impossible beauty, treacherous whimsy, time distortion, rules that change without notice, bargains with terrible fine print.",
Hook: "The veil between worlds is thin here. Colors are too saturated. The mushrooms are too large. A small creature made of starlight just offered you a deal. TwinBee advises extreme caution regarding deals.",
Hook: "The veil between worlds is thin here. Colors are too saturated. The mushrooms are too large. A small creature made of starlight just offered you a deal. I advise extreme caution regarding deals.",
MinRooms: 8,
MaxRooms: 10,
Enemies: []ZoneEnemy{
@@ -662,7 +662,7 @@ func zoneDragonsLair() ZoneDefinition {
LevelMax: 20,
Faction: "Kobolds, Drakes, Young Dragons, Wyrm",
Atmosphere: "Scorched stone, rivers of gold coins half-melted into the floor, kobold warrens as outer defenses, growing heat, the unmistakable smell of something ancient and enormous.",
Hook: "The mountain has not erupted in forty years. The locals say it is dormant. The locals are wrong about what lives in mountains. TwinBee has prepared an unusually long entry description for this one.",
Hook: "The mountain has not erupted in forty years. The locals say it is dormant. The locals are wrong about what lives in mountains. I have prepared an unusually long entry description for this one.",
MinRooms: 9,
MaxRooms: 10,
Enemies: []ZoneEnemy{
@@ -710,7 +710,7 @@ func zoneAbyssPortal() ZoneDefinition {
LevelMax: 20,
Faction: "Demons, Fiends, Corrupted Celestials",
Atmosphere: "Reality fractures, impossible geometry, constant low psychic pressure, the feeling of being watched by something that has no eyes.",
Hook: "Someone opened a door they should not have opened. The door is still open. Things are still coming through. TwinBee is not making jokes about this one.",
Hook: "Someone opened a door they should not have opened. The door is still open. Things are still coming through. I am not making jokes about this one.",
MinRooms: 9,
MaxRooms: 10,
Enemies: []ZoneEnemy{