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>
This commit is contained in:
prosolis
2026-05-08 10:15:57 -07:00
parent 437460c9b2
commit c8fac7ffd9
6 changed files with 464 additions and 0 deletions

View File

@@ -152,6 +152,35 @@ func initResources(userID id.UserID, class DnDClass) error {
return err
}
// subclassResourceMax — resources granted by a subclass on top of the class
// pool. Phase 10 SUB2a-ii: Battle Master gets 4 superiority dice (5e
// long-rest refresh in our model; spec says short-rest, but we keep parity
// with the existing class pools' refresh cadence). Returns ("", 0) if the
// subclass has no extra pool.
func subclassResourceMax(sub DnDSubclass) (string, int) {
switch sub {
case SubclassBattleMaster:
return "superiority", 4
}
return "", 0
}
// initSubclassResources adds the subclass-specific resource pool. Called
// from applySubclassChoice; idempotent. If a player switches subclasses,
// the prior pool's row is left in place — refreshAllResources still touches
// it but no ability references it once the subclass changes, so it's inert.
func initSubclassResources(userID id.UserID, sub DnDSubclass) error {
resType, max := subclassResourceMax(sub)
if resType == "" {
return nil
}
_, err := db.Get().Exec(`
INSERT OR IGNORE INTO dnd_resources (user_id, resource_type, current_value, max_value)
VALUES (?, ?, ?, ?)`,
string(userID), resType, max, max)
return err
}
// getResource returns (current, max). Returns (0, 0, true) if no row exists.
func getResource(userID id.UserID, resType string) (int, int, error) {
var cur, max int