Expedition autopilot Phase 3: ambient between-day ticker

Active expeditions now tick on a 3-hour cadence between the 06:00
briefing and 21:00 recap. Each fire DMs the player a short TwinBee
moment — mostly pure flavor (dripping sounds, polite possums,
diplomatic moths) with a 50/50 chance of a small mechanical nudge
(±SU, +threat, 1d4 coins from a found purse, +1 HP if camped).

The dungeon is now louder while you're offline without being
balance-relevant. Multi-day expeditions feel populated by something
other than the player's calendar.

Mechanics:
- new last_ambient_at column with CAS idempotency (mirrors
  briefing/recap pattern in dnd_expedition_cycle.go)
- ambientCooldown=3h, skipped if in turn-based combat session
- 60min politeness window around scheduled briefing/recap so we
  don't pile DMs on the daily messages
- weighted event pool with per-event eligibility (camp_visitor
  requires camped, pack_rat requires SU>=1, faction_whisper
  requires threat>=30 and not siege)
- effects: applyThreatDelta, updateSupplies, euro.Credit +
  coins_earned bump, SaveDnDCharacter HP nudge (clipped at max)

Files:
- internal/db/db.go: ALTER TABLE + CREATE TABLE update
- internal/flavor/twinbee_ambient_flavor.go: 7 humor-heavy pools
- internal/plugin/expedition_ambient.go: ticker + resolver
- internal/plugin/expedition_ambient_test.go: 8 pure-logic tests
- internal/plugin/adventure.go: wire goroutine in Start()
This commit is contained in:
prosolis
2026-05-14 23:28:54 -07:00
parent 632e5ee315
commit 368b980650
5 changed files with 669 additions and 0 deletions

View File

@@ -319,6 +319,10 @@ func runMigrations(d *sql.DB) error {
// jump back into combat — they're actually resting for the duration.
`ALTER TABLE dnd_character ADD COLUMN short_rest_charges INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE dnd_character ADD COLUMN resting_until DATETIME`,
// Phase 12 E7 (expedition autopilot Phase 3) — ambient ticker timestamp.
// Real-time between-day events fire at most once per ambientCooldown
// while an expedition is active; idempotent CAS on this column.
`ALTER TABLE dnd_expedition ADD COLUMN last_ambient_at DATETIME`,
}
for _, stmt := range columnMigrations {
if _, err := d.Exec(stmt); err != nil {
@@ -1803,6 +1807,7 @@ CREATE TABLE IF NOT EXISTS dnd_expedition (
gm_mood INTEGER NOT NULL DEFAULT 50,
last_briefing_at DATETIME,
last_recap_at DATETIME,
last_ambient_at DATETIME,
last_activity DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME
);