Commit Graph

92 Commits

Author SHA1 Message Date
prosolis
4a5159ee14 Adv 2.0 D&D Phase 10 SUB2-AT: Arcane Trickster spellcasting
Slots into the Phase 9 spell pipeline so an L5+ Rogue who picks Arcane
Trickster becomes a third-caster on the Mage list, INT-based.

  - isSpellcaster(c) gate: full casters by class plus AT Rogue at L5+.
    !cast / !spells now consult this; the cast class-gate also accepts
    Mage-tagged spells when the player is an AT Rogue.
  - subclassSpellSlots() / slotsForCharacter() / setSpellSlotsForCharacter()
    layer subclass slots on top of the class baseline. AT pool: 3 L1 at
    L5–6, 4 L1 + 2 L2 at L7+, +2 L3 at L13+ (third-caster table).
  - spellcastingMod returns INT for AT Rogue.
  - L7 Magical Ambush proxy: spellSaveDC bumps +2 once Rogue+AT hits L7,
    standing in for "targets have disadvantage when casting from hidden"
    since the one-shot combat layer doesn't model hidden state.
  - ensureSpellsForCharacter bootstraps a Mage-list starter spellbook
    (minor_illusion, shocking_grasp, magic_missile, shield, sleep at L5;
    mirror_image + misty_step at L7+; hypnotic_pattern at L13+) and
    refreshes the slot pool on level/subclass change.
  - applySubclassChoice runs ensureSpellsForCharacter for AT so the
    spellbook is provisioned at selection time (idempotent).
  - !cast clamps upcasts to highestAvailableSlotForChar when AT, since
    third-caster's slot ceiling lags behind the spell list.

Tests added: AT spellcaster gate (L5 cutoff, non-AT Rogue rejected), INT
mod, subclass slot pool by tier, L7 ambush DC bump, ensureSpells default
list (all on Mage list, slots correct), end-to-end !cast queues
magic_missile for AT Rogue, !cast rejects plain Thief Rogue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:21 -07:00
prosolis
c8fac7ffd9 Adv 2.0 D&D Phase 10 SUB2a-ii: Battle Master + Assassin L5/L7
Battle Master:
  - New "superiority" resource pool (4/long-rest at L5), provisioned via
    initSubclassResources at !subclass selection.
  - Three armed maneuvers fueled by superiority dice:
    * Precision Attack — +d8 (≈+4) to first attack roll
    * Tripping Attack  — enemy skips first attack (reuses Phase 9
      SpellEnemySkipFirst)
    * Rally            — +(d8 + CHA) HealItem at <50% HP
  - L7 Know Your Enemy proxied as +1 AttackBonus passive.

Assassin:
  - L5 Assassinate: AssassinateAdvantage (re-roll first miss, take better
    of two d20s) + AssassinateBonusDmg = level (5 at L5) stacked on top
    of the Rogue's existing Sneak Attack auto-crit.
  - L7 Impostor bumps the bonus damage by +3.
  - L5 Infiltration Expertise → +5 Deception; L7 Impostor → +10.

Engine:
  - CombatModifiers gains FirstAttackBonus, AssassinateAdvantage,
    AssassinateBonusDmg.
  - resolvePlayerAttack consumes each on the first attack only via new
    combatState one-shot flags.

Tests added: 14 covering passive gating (BM L7+, Assassin L5/L7),
maneuver Apply flags, superiority pool init for BM only, !arm gating
across class/subclass for precision_attack, Deception bonus tiers,
statistical lift from FirstAttackBonus and AssassinateBonusDmg in
SimulateCombat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:21 -07:00
prosolis
437460c9b2 Adv 2.0 D&D Phase 10 SUB2a-i: Champion + Berserker + Thief L5/L7
First mechanical slice of the subclass system. Each subclass gets its L5
and L7 abilities wired through the existing combat engine + skill check
substrate; no new resource pools introduced.

Champion (L5/L7):
- Improved Critical: new Mods.CritThreshold lowers crit floor to nat 19+
  in resolvePlayerAttack. Default 20 preserved for non-Champions.
- Remarkable Athlete: +½ proficiency bonus to STR/DEX/CON skill checks
  via subclassSkillBonus, layered on raceSkillBonus.

Berserker (L5/L7):
- Rage: new !arm-able active ability gated to Berserker subclass via
  DnDAbility.Subclass field. Reuses the Fighter stamina pool (3/long-rest
  matches 5e's rage uses). On fire: +2 flat damage per hit, halve incoming
  weapon damage, +50% damage approximation for Frenzy's bonus-attack-per-
  turn (one-shot combat can't model that literally).
- Frenzy exhaustion: new exhaustion column on dnd_character; incremented
  by persistDnDPostCombatSubclass after any rage'd combat. Decremented by
  one per long rest.
- Mindless Rage: documented; charm/frighten conditions don't exist in the
  combat engine yet, so no functional effect until P11 zone bosses land.

Thief (L5/L7):
- Sleight of Hand added to dndSkillTable.
- Fast Hands: +5 to Sleight of Hand checks via subclassSkillBonus.
- Supreme Sneak: advantage on Stealth (best of two d20s) via new
  subclassSkillAdvantage hook in performSkillCheck. 5e's half-speed gate
  dropped — we don't track movement speed.

Plumbing:
- applySubclassPassives layered after applyClassPassives in both arena
  and dungeon combat bridges.
- DnDAbility.Subclass enables per-subclass gating; characterActiveAbilities
  filters !arm and !abilities lists by both class and subclass.
- Existing !respec full-wipe also clears Exhaustion.

Tests: Champion CritThreshold gating across L4/L5/no-subclass; rage Apply
flag-set; rage probabilistic win-rate lift vs tougher enemy; exhaustion
increment only on raged combats; long-rest decrement; Champion bonus on
STR/DEX/CON only and only at L7+; Thief Fast Hands SoH-only; Thief
Supreme Sneak L7+ Stealth-only; statistical advantage roll lifts Thief
stealth average by ≥2 vs plain Rogue; !arm rage subclass gating; schema
roundtrip; characterActiveAbilities visibility.
2026-05-09 14:25:21 -07:00
prosolis
1ee4276bcd Adv 2.0 D&D Phase 10 SUB1: subclass selection system
Implements the identity layer of gogobee_subclass_system.md: 15 subclasses
(3 per class) selectable at L5 via !subclass.

- Schema: new subclass + last_subclass_respec_at columns on dnd_character.
- Registry (dnd_subclass.go): 15 entries with class, display, tagline, and
  TwinBee flavor line; helpers for prompt rendering and input resolution
  (number, id, or display-name; case/space/hyphen-insensitive).
- !subclass command: bare form prints prompt or current; !subclass <name>
  selects (free first time) or changes (500 euros + 30-day cooldown via
  separate timestamp from the full !respec wipe). Save-then-debit ordering
  mirrors !respec audit fix B.
- Level-up DM at L5+ now embeds the real selection prompt instead of the
  Phase 9 placeholder; suppressed once a subclass is set.
- Sheet shows the subclass below the class line, or nudges !subclass when
  unchosen at L5+.
- Existing !respec full-wipe also clears subclass + cooldown timestamp.

Mechanical effects of L5/7/10/15 subclass abilities deferred to SUB2/SUB3.
2026-05-09 14:25:21 -07:00
prosolis
b53e516bf0 Adv 2.0 D&D Phase 9 SP4: mage spellbook cap + level-up nudge
- Enforce per-level Mage known-spell cap in handleSpellsLearn; surface
  spellbook budget in renderSpellsList.
- Mage level-up DM now nudges with "Spells available to learn: N" via
  extracted buildLevelUpMessage.
- Extract halveSavedDamage and enemySpellSaveMod for clarity; document
  single-target AoE limitation in applySpellDamageSave.
- Add tests: mage learn cap, prepare flow, AoE behavior, spell save
  rounding, spells migration.
2026-05-09 14:25:21 -07:00
prosolis
01c70f5297 Adv 2.0 D&D Phase 9 SP3: wire pending spells into combat
applyPendingCast resolves c.PendingCast against the upcoming fight before
SimulateCombat. Damage spells (Fire Bolt, Burning Hands, Magic Missile,
Fireball, etc.) emit a pre-combat spell_cast event via new
CombatModifiers.SpellPreDamage{,Desc}. Control spells (Hold Person, Sleep,
Command) set SpellEnemySkipFirst so the engine skips the enemy's round-1
attack with a spell_held event; Hold-family also primes AutoCritFirst.
Buffs (Mage Armor, Bless, Hunter's Mark, Shield of Faith, Aid, Spiritual
Weapon, Mirror Image, Greater Invisibility) fold into stats/mods directly.

Concentration-on-damage break is left for Phase 11 (turn-based bosses);
ConcentrationSpell persists across fights until manually dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:21 -07:00
prosolis
9e1a1f606c Adv 2.0 D&D layer: Phases 1-8 + Phase 9 SP1+SP2
Combines all uncommitted D&D work into one checkpoint:

Phases 1-8 (per gogobee_dnd_session_summary.md):
- Race/class/stats system, !setup flow, !sheet, !respec
- d20-vs-AC combat with race/class passives, auto-migration
- XP/level-up, skill checks, NPC refund hooks
- Equipment slot mapping, rarity, !rest short/long
- Pre-arm active abilities, resource pools
- Bestiary, !roll/!stats/!level, onboarding DM
- Audit fixes A-I, flavor wiring under internal/flavor/
- Phase 8 weapon dice + armor AC tables (gogobee_equipment_appendix)

Phase 9 SP1 — spell system foundations:
- 79 SpellDefinitions (76 in-scope + 3 reaction-deferred)
- dnd_known_spells, dnd_spell_slots tables
- pending_cast / concentration_* columns on dnd_character
- Slot tables for Mage/Cleric/Ranger, DC + attack-bonus math
- Long-rest refreshes slots; respec wipes spell state
- Auto-grant default known list on !setup confirm and auto-migration

Phase 9 SP2 — !cast / !spells / !prepare commands:
- Out-of-combat HEAL and UTILITY resolve immediately
- Damage/control/buff queue as pending_cast for next combat
- Audit-style save-then-debit, slot refund on save failure
- !cast --drop, concentration supersession, prep-cap enforcement
- Reaction spells refused with Phase 11 note

SP3 (combat-time resolution of pending_cast) and SP4 (full prep/learn
tests) are next. Build clean, full test suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:21 -07:00
prosolis
8e0fe0230c Death source tracking + combat threat/jitter fixes
- Adventure characters now record DeathSource ("adventure"|"arena") and
  DeathLocation when killed. Threaded through Kill() and transitionDeath.
  Daily report uses the recorded death location instead of misattributing
  arena deaths to the day's adventure log: when DeathSource != "adventure",
  the adventure block shows the alive icon and a separate "Later fell in
  {location}." line. Standout-loss flavor uses DeathLocation. Empty
  DeathSource (legacy rows) falls back to current behavior.
- calcDamage applies a ±15% per-hit jitter, so successive hits in a phase
  no longer produce identical numbers. Previously every hit in a phase was
  flat (e.g. 17, 17, 17, 17) and mirror-symmetric between player/enemy,
  reading as scripted.
- assessThreat rewritten to use the engine's actual penetration formula:
  damage-per-round each direction, then rounds-to-kill ratio. The old
  additive HP+Attack*3 model ignored Defense, so even fights could be
  rated trivial — players died after consumables were skipped with the
  "threat assessed as manageable" message.
- SelectConsumables never skips on trivial when arenaRound > 0. Arena
  losses cost real money and equipment, so the cost of a wrong assessment
  is too high to gamble on; adventure-side fights still skip trivial
  threats to avoid wasting items on chump enemies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:21 -07:00
prosolis
a00987e75c Coop lockout fix, holidays full list, !fx convert, lottery cleanup
- Co-op: when a run wipes or completes, clear the 99-sentinel combat
  lock for participants and pin combat_actions_used to maxCombatActions.
  Previously the lock only released on the next midnight reset, so
  members were stuck unable to combat for the rest of the day after
  their run ended.
- Holidays: !holidays now prints the full list. Previously it called
  the same featured-only formatter as TwinBee's morning auto-post,
  so users saw the same truncated message twice with a misleading
  "Use !holidays for the full list" hint that pointed at itself.
- Forex: add currency conversion. Auto-detects when the first arg is
  numeric (`!fx 1500 USD EUR`) and also accepts an explicit `convert`
  subcommand. Tolerant parsing: "to"/"into"/"in"/"=" filters, comma
  and k/m suffix support, and any of `1500 USD EUR`, `1500 USD/EUR`,
  `USD/EUR 1500` orderings. Includes parser + comma-formatter tests.
- Lottery: drop 2-match prize tier. At 1/16 odds × €25 it was +EV per
  ticket on its own and bled the community pot (~€775/wk recently),
  preventing jackpot rollover from accumulating. Removed from scoring,
  payout loops, draw announcement, !lottery odds, and !lottery history.
  Match2Winners DB column kept (history rows preserved); always written
  as 0 going forward.
- Lottery: shift week boundary so Sat/Sun return next Monday. Friday's
  23:59 draw used to leave players locked at their 100-ticket cap until
  next Monday rolled in; now the cap resets immediately after the draw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:21 -07:00
prosolis
d9ac998458 Adventure: surface crafts in !adventure status + document crafting
- Status sheet now shows lifetime craft count + a hint pointing at
  !adventure recipes (only displayed once the player has crafted at
  least once — keeps the line absent for non-foragers).
- README gains a Crafting subsection documenting the auto-craft flow,
  XP rewards, level gates, the recipes command, and the consumable
  protection (Robbie + sell-all both skip them).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:26:06 -07:00
prosolis
0bebcb56cd Adventure: !adventure recipes — list known crafting recipes
Players had no way to see what they could craft without grinding
ingredients and watching combat narrative. New command lists every recipe
unlocked at the player's current Foraging level, grouped by tier, with:

- Result name + ingredient list (so players know what to gather)
- Per-recipe success rate at the player's current level
- Locked-recipe count + next-unlock threshold
- Max auto-crafts per combat (1 + (level-10)/10)

Sub-Foraging-10 players see only the unlock hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:23:53 -07:00
prosolis
8e3b8377c0 Adventure crafting: grant foraging XP per attempt, track lifetime crafts
Crafting was a downstream consumer of Foraging skill but never gave any
XP back, so the feedback loop was one-directional — only gathering
contributed to the level that gates better recipes.

Now: each auto-craft attempt grants Foraging XP (success > failure).
Per-tier values:
  tier 1:  +12 / +3
  tier 2:  +25 / +5
  tier 3:  +40 / +8
  tier 4:  +60 / +12
  tier 5:  +90 / +18

Successful T1 ≈ 30% of a Foraging Success haul; T5 ≈ 40%. Failures get a
small consolation grant — the attempt still produced practical knowledge
(and lost ingredients).

Schema: adventure_characters.crafts_succeeded INT for lifetime tracking.
Migration entry included.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:19:06 -07:00
prosolis
671773abd5 Adventure: protect consumables from Robbie and 'sell all'
Consumables (auto-crafted from foraged ingredients or dropped from T2+
activities) are a curated stockpile — selling them should be an explicit
choice, not a side effect.

Two surfaces tightened:
- Robbie's qualifying-items filter now skips Type == "consumable" the
  same way it skips Arena gear and cards.
- !adventure sell all leaves consumables in inventory and reports them
  in the kept-items tally. The merchant message points players at
  !adventure sell <name> for explicit consumable sales.

!adventure sell <name> still works fine on consumables — that path is
already explicit by definition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:13:09 -07:00
prosolis
e22d4f8362 Hold'em: route sender-private replies to DM origin + jitter broadcasts
Two changes that were running in prod but hadn't been committed:

1. Sender-private DM routing. When a player invokes a holdem command from
   their DM, the dispatcher rewrites ctx.RoomID to the game room so
   actions affect the right table. But sender-private replies (errors,
   help, status, "unknown command") would then post to the public games
   room instead of back to the DM that asked. Now: the dispatcher saves
   ctx.OriginRoomID = the DM, and a new reply() helper routes sender-
   private messages to OriginRoomID when set, falling back to in-thread
   reply otherwise. (OriginRoomID was added to MessageContext earlier.)

2. broadcastDM jitter. 150-400ms sleep between sends to avoid Matrix
   rate-limit bursts when a game has 4+ players each in their own DM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:37:19 -07:00
prosolis
4826b5e466 Adventure: scale random-event gold rewards 100× to match current economy
Random event gold ranges (€20-250) were authored against an earlier
economy and have been near-trivial against current per-action haul values
(thousands at higher tiers). Scaled the GoldMin/GoldMax across all event
entries by 100× so the rewards feel like real moments instead of
rounding errors.

Pure flavor-table numeric rebalance — no logic changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:37:04 -07:00
prosolis
7878ce0999 Adventure: defer random-event roll to 1-3h after a player acts
Previous scheduler built one fixed roll-minute per player per UTC day in
the 10:00-16:00 window, regardless of when (or whether) the player
actually played. Players who acted late in the day might miss their roll
entirely; players who acted early still had to wait for an arbitrary
mid-day window.

New scheduler:
- Each ticker minute, scan for players who have acted today but don't
  yet have a scheduled roll. Assign each a one-shot roll-minute 60-180
  minutes in the future (capped to 23:50 UTC).
- At the assigned minute, fire the 0.5% trigger roll. Mark advEventRolled
  so it won't fire again that day.
- New day rebuilds both maps fresh.

Result: events trigger relative to the player's actual activity, not
clock time. Late-day actors get their chance; players who skip a day
don't get a roll.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:36:52 -07:00
prosolis
6eadb09c1e Adventure: extend NPC encounter expiry to end of UTC day
NPC encounter pending interactions were using advDMResponseWindow
(the short DM-reply window), so a player who didn't get to their DMs
quickly would lose the encounter prompt. Bumped to "until midnight UTC"
so the encounter is available throughout the day the NPC arrived.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:36:36 -07:00
prosolis
d93dc4faa4 Adventure: skip overlevel penalty when no higher tier is accessible
Previously, advOverlevelMultiplier penalized every action where the
player's effective level exceeded the location's MinLevel by 4+, with
no escape valve. That punished players grinding at their max-available
tier (e.g., a level 50 character at the highest mining location they
qualify for, with nothing higher unlocked).

Now: the multiplier short-circuits to 1.0 when no accessible higher-tier
location of the same activity exists for that player. Signature changed
from (effectiveLevel, minLevel) to (effectiveLevel, *AdvLocation) so the
helper can scan the activity's location list. combat_bridge.go updated
to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:36:17 -07:00
prosolis
07ca5288c3 Coop: stack same-type gifts on the same day into a single vote post
Multiple baskets (or multiple mimics) sent during the same day now share
one game-room post, one vote, one resolution. First-in becomes the stack
"lead"; subsequent same-type sends become followers that inherit the
lead's deadline and votes.

Behavior:
- Send a basket → new lead, new post, 6h timer starts
- Send another basket within the window → silently joins the stack,
  edits lead's post to bump count
- At stack size 2, TwinBee adds a "this gift looks REALLY special" line
  (one of 5 variants picked deterministically by lead id). No further
  escalation as the stack grows
- One !coop giftvote on the lead's id covers the whole stack
- Resolution applies the same outcome to every gift in the stack;
  modifier is N × ±6
- End-of-run gift log groups by stack with all senders attributed

Schema: coop_dungeon_gifts.stack_lead_id INTEGER (NULL for lead/standalone,
otherwise points at lead's id). Migration entry included.

Why first-in deadline (vs extending on each follower): exploitability.
Saboteurs could spam-extend a stack to delay resolution. Locked deadline
keeps senders honest about timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:28:23 -07:00
prosolis
48e5000745 Coop: lock combat actions for the full duration of an active run
Players in a co-op were getting their combat action back every midnight
reset and offered the dungeon option in the morning DM. Per spec they
should be "off in the Co-op" and unable to also solo-grind dungeons.

Fix at the DB layer (so all CanDoCombat() paths agree):
- New helper lockCoopCombatActions() sets combat_actions_used=99 for any
  user in an active coop run. Called immediately after every
  resetAllAdvDailyActions() — at startup and at midnight.
- Render layer: morning DM now shows "combat locked (in Co-op #N, day
  X/Y)" and replaces the dungeon section with an explanatory line
  pointing them at !coop status.

Combat returns to normal automatically once the run completes/wipes —
the next midnight reset finds status != 'active' and skips the lock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 20:38:00 -07:00
prosolis
c060e13b41 Coop: surface per-gift countdowns in !coop status
Each gift now has its own 6h voting window, but the only places the
remaining time was visible were the original arrival post and any
edit-triggered re-render after a vote. Mid-window the displayed
countdown was stale.

!coop status now lists pending gifts (anything with vote_result still
NULL on this run) with current open/leave tally and a fresh per-gift
countdown derived from expires_at. The party can poll this any time to
see what's about to close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 20:20:00 -07:00
prosolis
b7077eeea1 Coop: substitute {count} and {leader} placeholders in gift flavor
The TwinBeeGiftArrival pool entries each end with a templated footer
authored to the spec format ("Open: {count} · Leave it: {count}\nMajority
rules. Ties go to {leader}."), but the placeholders were never being
substituted — players saw the literal "{count}" and "{leader}" strings.

Substitute in renderCoopGiftPost: ordered Replace for the two {count}
positions (opens, then leaves), ReplaceAll for {leader}. Also dropped
the redundant "Current votes:" line my code was appending — the flavor's
own tally line covers it after substitution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:14:49 -07:00
prosolis
9f094549b7 Coop: per-gift voting timers (6h) — gifts resolve throughout the day
Replace the single daily-tick gift resolution with independent per-gift
expiries. Each gift now has its own 6h voting window; once that elapses,
the votes are tallied, the sender gets DM feedback, and the live game-room
post is edited to reveal the type and resolved modifier. The modifier
sits on the run waiting for the next floor resolution to merge it in.

Effect:
- Gifts fire continuously throughout the day rather than all at once at
  08:00 UTC, surfacing sender activity in real time.
- Senders get fast feedback (~6h instead of waiting for the next daily
  tick).
- Floor resolution becomes purely "sum pending modifiers from already-
  resolved gifts" — cleaner separation of concerns.
- Atomic per-gift apply via markCoopGiftApplied (UPDATE...WHERE applied_at
  IS NULL) prevents double-application on retry.

Schema additions:
- coop_dungeon_gifts.expires_at (when voting closes)
- coop_dungeon_gifts.applied_at (when modifier merged into a floor)
- Migration entries provided.

Defensive backstop: floor resolution still force-resolves any gifts whose
expiry was missed (e.g., bot down during the timer window).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:55:14 -07:00
prosolis
4bc541ca85 Coop: shorten first-day window — require 12h since lock, not date change
Previous guard skipped resolution if lock_date == today_date, pushing
first-day windows to 24-32h regardless of when in the UTC day the lock
fired. With per-minute lock checks, the original same-tick race no longer
exists, so the date guard was just adding latency.

New guard: at least 12h since lock. Late-night locks still get >12h
funding/voting windows; early-morning locks get 24h+ as before. No effect
on the existing run #1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:50:24 -07:00
prosolis
90edb27147 Coop: add !coop admgift admin debug path
Admin-only command that creates a gift without the party-member or
harvest-action checks. Lets a solo tester exercise the basket/mimic
mechanic without recruiting a non-party spectator.

Usage: !coop admgift <run_id> <basket|mimic> [sender_user_id]
       (sender defaults to the caller)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 18:15:26 -07:00
prosolis
ecca66da58 Coop: split lock checks (per-minute) from resolution (daily)
Locks were gated behind the same once-per-day 08:00 UTC tick as floor
resolution. A run created after morningHour would wait until next day's
tick to be eligible — meaning actual lock latency could be 24-48h, not the
24h advertised. Worse, a run created late on Day N missed the Day N+1 tick
(too early) AND the Day N+2 tick happens 48h after creation.

Fix: lock checks fire on every ticker minute (cheap timestamp scan over
typically 0-5 open runs). Resolution stays daily, gated by JobCompleted.

Real symptom that prompted this: Run #1 created 17:17 UTC, locked never
fired because the morning tick at 08:00 UTC the next day saw the run as
not-yet-eligible (only 15h elapsed), then marked the daily job complete
before the actual 24h elapsed at 17:17 UTC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:35:37 -07:00
prosolis
9d949dc649 Hold'em: deduplicate broadcast in solo-vs-bot games
Win/end announcements were sent twice in solo-vs-bot: once via
SendMessage(game.RoomID, ...) for the public room, once via broadcastDM
which iterated the human player's DMRoomID — but in solo-vs-bot the
game.RoomID IS the player's DM. Skip players whose DMRoomID matches
game.RoomID in broadcastDM so the room post isn't echoed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:38:16 -07:00
prosolis
a1ede53fa9 Equalize co-op gift magnitudes to kill 'always leave' dominant strategy
Old values (open ±7, leave ±4) had matching EV but asymmetric variance;
risk-averse parties had a strict preference to always leave gifts, which
collapsed the dilemma. New values (all ±6) keep EV=0 for both strategies
AND equalize variance. Choice now depends entirely on inferring sender
intent.

Adds TestCoopGiftVarianceSymmetric — fails loud if anyone reintroduces
asymmetric magnitudes. EV test was already in place but only checked the
mean; missed this on first pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:12:22 -07:00
prosolis
f3d1e65bf1 Pin co-op invite posts; unpin on lock/cancel
Adds Base.PinEvent / Base.UnpinEvent helpers (m.room.pinned_events state)
that read-modify-write the existing pin list — idempotent on both sides.
The bot needs power level for state events; failures are logged but not
fatal.

Schema: add coop_dungeon_runs.invite_post_id to remember which event to
unpin. Migration entry included.

Wired:
- !coop start: pin the invite post in the games room
- !coop cancel: unpin before posting cancellation
- coopProcessLocks (auto-lock or auto-cancel): unpin before lock/expiry post

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:24:21 -07:00
prosolis
91af4f1e83 Thanks, Ollama.
Replace flat 'LLM might be offline' error strings in vibe and howami with
the deadpan 'Thanks, Ollama.' callback. Skipped tarot — its 'union-mandated
vacation' line was already the right register and shouldn't be displaced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:09:32 -07:00
prosolis
73f3362400 Fix misleading 'combat action refunded' DM on coop invite expiry
Combat actions are only deducted at lock, never at start or join, so
there's nothing to refund when an unjoined invite expires. Just say so.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:25:08 -07:00
prosolis
9639a86268 Add !coop stats — public telemetry for runs, gold flow, betting, gifts
Aggregates that surface community-level patterns:
- Per-tier outcomes table (open/active/won/wiped/cancelled, win%, avg party size)
- Gold flow: rewards distributed vs funding forfeited to wipes
- Betting: bets placed, distinct bettors, total wagered, paid to winners,
  estimated house cut
- Gift activity: basket/mimic sent, open rates by type
- TwinBee helpfulness: last 30 events vs lifetime

Public command (no admin gate) so the community can debate strategy with
shared context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:14:45 -07:00
prosolis
e8a3b8b35d Co-op audit fixes: lock scheduler, idempotent payouts, deterministic ties
Audit pass addressed concurrency races, crash-recovery double-pay risks, and
non-determinism in tie tallying.

CRITICAL — concurrency
- coopResolveFloor now acquires advUserLock for every party member (sorted by
  UserID) before mutating state, so concurrent !coop fund / vote / giftvote
  commands serialize against the scheduler.

CRITICAL — crash idempotency
- Add coop_dungeon_runs.last_resolved_day and coop_dungeon_members.member_payout
  columns (with migration entries) for crash-resume tracking.
- Skip resolution only if last_resolved_day >= day AND status is terminal;
  otherwise re-enter and finish via idempotent operations.
- On resume detection (event already has outcome), reuse the saved roll
  instead of re-rolling — prevents different outcomes on retry.
- claimCoopMemberPayout / claimCoopBetPayout: atomic UPDATE...WHERE col IS NULL
  pattern. Each member/bet can only be paid once; retries are silent no-ops.
- createCoopEvent now INSERT OR IGNORE — idempotent on retry.
- last_resolved_day is set as the FINAL write per floor.

HIGH — bugs
- Lazy-create floor event in resolver if missing — covers crash window
  between lockCoopRun and the original createCoopEvent.
- coopTallyVote sorts tied winners alphabetically before fallback (was
  non-deterministic via Go map iteration).
- Removed dead day-1 wipe combat-action refund block — refund check was
  always false because midnight reset clears state before resolution.

LOW
- Log slog.Error on malformed JSON in parseCoopVotes / parseCoopFundingMap
  instead of silently dropping data.

Tests
- Tally test extended to assert determinism across 50 runs (catches map-iteration
  leaks).
- Added coverage for new tie-with-leader-vote-outside-winners case.
- Idempotency-guard sanity check on LastResolvedDay vs CurrentDay.

Race detector: clean. Full suite: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:01:29 -07:00
prosolis
8ad31a0009 Add co-op dungeon system (party runs, voting, betting, gifts, items)
Multi-day party runs with funding decisions, TwinBee-narrated floor events,
spectator parimutuel betting, basket/mimic gift system, and weighted-roll
item distribution including masterwork drops at T4-T5.

Schema (5 tables, 2 fewer than spec by computing helpfulness on-the-fly and
skipping the loot-pending state):
- coop_dungeon_runs / _members (daily funding as JSON column)
- coop_dungeon_events (votes as JSON, used to derive TwinBee helpfulness)
- coop_dungeon_bets / _gifts

Mechanics:
- 2-4 player parties, 24h invite window, locks consume one combat action
- Per-floor success roll: base + sum(funding) + level/pet bonuses + event
  vote modifier + active gift modifiers, clamped to 5..95
- Funding tiers (none/min/std/agg/all_in) with liability cap at +8% for
  under-leveled players
- TwinBee narrates events from existing flavor pool; 20 authored events with
  per-option modifiers and embedded recommendation
- Parimutuel betting with 10% rake; odds line shown on lock/daily/status posts
- Gift modifiers symmetric at 50/50 sender mix so no dominant strategy
- Item drops on success via weight-by-contribution roll; T4 25% / T5 100%
  masterwork chance (random pick from existing T4/T5 defs)

Balance via Monte Carlo (coop_dungeon_balance_test.go):
- All tiers exceed 1.5x solo daily income at average party profile
- Optimal funding strategy walks up tiers correctly (Min/Std/Std/Mixed/Agg)
- All-In stays -EV at every tier (boost lever, not optimal play)

Tests: parsing, liability cap, JSON roundtrips, vote tally with leader
tiebreak, event meta consistency, parimutuel payouts, gift EV symmetry,
weighted-roll distribution (Monte Carlo), masterwork tier gates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 07:58:13 -07:00
prosolis
16d64323d9 Fix babysit logging, achievement table name, and blacksmith repair costs
- runAutoBabysitDay now logs daily totals (was silently skipping logBabysitActivity)
- Stop wiping babysit_log on expiry/cancel; clear at next purchase instead so history persists between services
- Fix achievement query referencing wrong table name (babysit_log -> adventure_babysit_log)
- Tune blacksmith baseRates ([1,3,8,20,55,150] -> [1,2,5,12,30,80]) and soften the convexity (damage/100 -> damage/200) so mid-game repair drag isn't punitive

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 07:57:31 -07:00
prosolis
be76973fd2 Fix arena gear lockout, extend DM window, scrub mining flavor
Arena helmet auto-equip now preserves the existing helmet to inventory
(or stashes the new drop if the existing piece is strictly higher tier),
and arena gear can be re-equipped via !adventure equip. Shop only blocks
swaps that aren't an upgrade over current arena tier. DM response window
bumped from 15m to 3h so NPC/treasure/shop prompts don't expire while
players are AFK. MiningSuccess flavor pools no longer name specific ores
in prose — uses {ore}/{location}/{tool} placeholders so the narration
matches the actual loot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:23:58 -07:00
prosolis
d77c2ebbbb Add activity combo system with +200% per-step multiplier
Messages, links, and images each have independent combo streaks
that multiply passive euro earning. Combo grows 3x per step
(+200%), resets after 10 min idle, with daily caps (50 msg,
10 links, 15 images). !combo command shows current streaks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 16:06:28 -07:00
prosolis
148b8d20f2 Add tip audit logging, CFR alignment tests, and multiway scenarios
Three confidence features for the poker tip system:
- Tip audit table (holdem_tip_audit) logs every tip with full context
  for bulk review after real sessions
- CFR alignment test validates all 32 scenarios against the trained
  5M-iteration policy — rules engine never contradicts the bot's AI
- 8 new multiway test scenarios covering all streets and key decisions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 14:11:59 -07:00
prosolis
b15c13cde7 Add version system, tax tracking, lottery winner DMs, fmtEuro, audit fixes
- Version system: per-plugin versions via Versioned interface, crash_log and
  version_history DB tables, !version command, crash stats in !botinfo
- Tax tracking: tax_ledger table, trackTaxPaid across all communityTax and
  communityPotAdd sites, tax paid display in !stats and !superstatsexplusalpha
- Lottery: DM winners with winning ticket details and matched numbers bolded
- fmtEuro: generic currency formatter with thousand separators across all
  economy display paths
- Audit fixes: streak_decayed column for Streak Survivor achievement,
  combat_narrative empty-phase guard, crafting success rate integer division,
  RecordCrash nil-DB guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 01:07:25 -07:00
prosolis
42e6e23900 Overhaul hold'em tips: solver-backed scenarios, equity ranges, validation suite
Replaces hardcoded tip scenarios with solver-frequency-backed decisions, adds
equity range display, fixes bet-size matching tolerance (25% threshold), and
adds comprehensive test coverage for scenario validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 01:06:50 -07:00
prosolis
7c450aaefb Add forward-simulating combat engine with consumables, monster abilities, and narrative rendering
Replaces the single-roll probability system for dungeon and arena combat with
a multi-phase simulation engine where gear, buffs, pets, and NPCs are meaningful
mechanical inputs. Adds consumable items (auto-crafted from forage/mine/fish drops),
monster abilities for T2+ enemies, and Dragon Quest-style combat narrative with
phased message delivery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 01:05:22 -07:00
prosolis
08e9925d8f Wordle: prefer common words via DreamDict frequency batch lookup
Gather up to 5 candidate words from DreamDict RandomWord, then use
FrequencyBatch to pick the most commonly used one. Players get
recognizable everyday words instead of obscure ones that just clear
the min_freq floor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 17:15:16 -07:00
prosolis
76110f61ca Add housing/pets/Thom Krooke, wordle overhaul, boost system, backup strategy
Part 3 (Housing, Thom Krooke & Pets):
- Housing system with tiered upgrades, mortgage loans, FRED API rates
- Thom Krooke realtor NPC with !thom commands and !thom pay extra principal
- Pet system with arrival, naming, leveling, combat, morning defense
- Pet ditch recovery scales with level, name validation

Wordle overhaul:
- Remove daily expiration — puzzles persist until solved/failed
- Auto-start new puzzle after solve, fail, or skip
- Sequential puzzle IDs instead of date-based
- Auto-create puzzle on first guess if none active

Adventure fixes:
- Double XP/money boost toggle (!adv boost, admin only)
- Fix ensureCharacter wiping existing data on query errors
- Fix rival RPS format string bug
- Fix daily summary empty data (load today+yesterday logs)
- Fix holdem DM-to-room reply routing
- Fix Robbie payout to 25% of item value
- Add fishing to leaderboard and TwinBee calculations
- Add Thom to morning menu
- Persist mortgage rate across restarts via db.CacheGet/Set

Infrastructure:
- Daily database backup via VACUUM INTO with 7-day retention
- Admin DM notification on backup failure (async)
- Daily NPC house balance reset for holdem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 16:50:49 -07:00
prosolis
90865d1c51 Fix NPC audit findings: race conditions, debit checks, sniper mechanics
Concurrency:
- npcTrackMessage now acquires advUserLock to prevent lost updates
  on message count and double encounter triggers from rapid messages
- Lock released before spawning encounter goroutine (avoids deadlock)
- Only one NPC encounter fires per message

Security:
- Debit() return value checked for both Misty and Arina payments
- Buff only applied on successful debit; failed debit falls through
  to decline/debuff path
- NPC encounter won't overwrite existing pending interactions (shop,
  treasure discard, etc.)

Arena sniper rework:
- Sniper checked independently before combat roll, not after
- If sniper fires, combat roll is skipped entirely (not a death save)
- Sniper kill renders as standalone narrative with no combat log
- Sniper flavor text rewritten: reads as random arena chaos with no
  hint of Arina's involvement

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 23:01:39 -07:00
prosolis
2ef7a29f02 Add Misty & Arina NPCs, fix tier gating, babysit actions, Robbie filter, streak/scheduler bugs
Misty & Arina: random encounter NPCs triggered by chat message counting
(5-10 msg threshold, 7-day cooldown, 7.5% roll). Misty asks for €100 —
accept gives 7-day arena buff (20% gourmet food heals gear + 5 enemy dmg),
decline gives 7-day debuff (20% crowd revenge damage). Arina asks for €5000 —
accept gives 7-day sniper buff (8% instant kill in arena). Effects integrate
into arena round resolution after combat roll; sniper overrides death.

Other fixes:
- Tier gating now uses base skill only — buffs improve success, not access
- Babysitter uses all harvest actions (3/day, 4 on holidays) instead of 1
- Robbie collects all non-equipped items, not just slotted gear
- Robbie timing: fire on first tick >= target hour, not exact minute match
- Streak: dead players who acted still get streak credit
- Streak: dead players skip shame DM (use LastDeathDate, not Alive flag)
- Midnight ticker: in-memory date cache prevents 1439 wasted DB checks/day
- T1 loot values ~3x across all activities to close T1/T2 gap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:41:21 -07:00
prosolis
1b825498bd Fix harvest actions remaining to use consistent harvestMax pattern
The nudge message after an action was computing harvest remaining
inline with maxHarvestActions + 1 for holidays instead of using the
harvestMax++ pattern used everywhere else. Functionally equivalent
but inconsistent — would break if the holiday bonus ever changed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 17:22:04 -07:00
prosolis
e459b6e78d Full codebase audit: 21 security/robustness fixes, 328 tests across 4 packages
Security & economy:
- Credit()/Debit() reject non-positive amounts (closes infinite-money exploit)
- Lottery ticket purchase uses in-transaction count check (closes TOCTOU race)
- Arena bail channel ownership prevents double-close panic
- Entry.ID validated before exec.Command in fetch-esteemed
- Internal errors no longer leaked to users (forex, esteemed)

Robustness:
- safeGo() panic recovery added to 17 goroutine launch sites across 14 plugins
- db.Close() added and called on shutdown
- Miniflux mutex pattern fixed (snapshot-under-lock)
- Silently ignored DB/JSON errors now logged (lottery_db)

Performance:
- Added indexes: idx_arena_runs_user(user_id, status), idx_euro_bal_user(user_id)

UX:
- Empty !buy args shows usage instead of "No item matching ''"
- "Failed to load character" now suggests !adventure

Test coverage:
- internal/util/parser_test.go (19 tests: XP, levels, progress bar, commands, parsing)
- internal/crypto/crypto_test.go (12 tests: encrypt/decrypt, HMAC, ParseKey)
- internal/plugin/helpers_test.go (30 tests: formatNumber, calc, lottery, chat perks)
- internal/plugin/audit_fixes_test.go (25 tests: safeGo, bail channels, error leaks, overflow)
- internal/db/db_test.go (4 tests: Init, Close, schema indexes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 17:04:40 -07:00
prosolis
68b2f8b7a5 Add chat level perks: XP bonus, shop flavor, rare drop nudge, death pardon (Adventure 2.5 steps 3-6)
- XP bonus: +5% per 10 chat levels (cap +25% at 50+), applies to all
  adventure and arena XP
- Shop flavor: shopkeeper greeting changes at chat levels 10/30/50,
  replacing random flavor for recognized players
- Rare drop nudge: +0.5% per 10 chat levels (cap +2.5%) applied
  additively to treasure and masterwork drop rates
- Death pardon: chat level 20+ gets 33% chance to survive death once
  per 7 days (converts to bad-failure, quiet DM). Does not apply in
  arena. Fires before Sovereign Death's Reprieve check.
- New adventure_chat_perks.go with perk calculation helpers
- LastPardonUsed field + migration on adventure_characters

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:50:28 -07:00
prosolis
a44a9d9234 Fix character sheet holiday actions, remove dead holiday prompt, cap babysit counter
- Character sheet now checks isHolidayToday() for correct remaining
  action counts on holidays
- Remove renderAdvHolidaySecondPrompt (dead code after economy split)
  and its test
- Babysit harvest counter clamped to max to prevent exceeding budget

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:31:25 -07:00
prosolis
7e4fbe5ec8 Add action economy split and cross-plugin chat level lookup (Adventure 2.5 steps 1-2)
Action economy: replace single daily action with 1 combat + 3 harvest
actions per day. Holidays grant +1 to each bucket. Rest consumes all
remaining actions. Arena remains outside both buckets.

- Add CombatActionsUsed/HarvestActionsUsed counters to AdventureCharacter
- Add CanDoCombat/CanDoHarvest/AllActionsUsed/HasActedToday helpers
- Update all 14 ActionTakenToday check sites across adventure, scheduler,
  babysit, twinbee, events, and render
- Morning DM shows action budget and grays out exhausted categories
- Activity resolution checks per-bucket availability before proceeding
- Midnight reset zeros both counters
- Post-action nudge shows remaining actions instead of holiday prompt

Cross-plugin lookup: export XPPlugin.GetLevel(), wire into AdventurePlugin
via p.chatLevel(userID) for upcoming chat level perks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:24:49 -07:00