diff --git a/internal/plugin/dnd_subclass_combat.go b/internal/plugin/dnd_subclass_combat.go index 8efcfac..bdd7bf5 100644 --- a/internal/plugin/dnd_subclass_combat.go +++ b/internal/plugin/dnd_subclass_combat.go @@ -62,6 +62,28 @@ func applySubclassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDChar if c.Level >= 7 { stats.AttackBonus++ } + case SubclassThief: + // Phase 10 SUB3a-iii — Thief L10/L15. + // + // L10 Use Magic Device (5e L13: ignore class/race/level requirements + // on magic items, including spell scrolls and wands). We don't have a + // scroll/wand item system, so we proxy the fantasy as a small free + // "magical implement" effect every fight: a pre-combat wand-zap and a + // flat damage bump representing better tactical use of magic items + // the Thief is now allowed to wield. + // L15 Thief's Reflexes (5e L17: take two turns during the first round + // of combat). Engine has no "extra turn" primitive, so we ride the + // AssassinateBonusDmg + AssassinateAdvantage channel — the same one + // Gloom Stalker's Dread Ambusher uses for bonus-opener damage. The + // flat-damage proxy approximates the value of one extra turn's hit. + if c.Level >= 10 { + mods.FlatDmgStart += 6 + mods.DamageBonus += 0.05 + } + if c.Level >= 15 { + mods.AssassinateAdvantage = true + mods.AssassinateBonusDmg += 8 + } case SubclassAbjuration: // L5 Arcane Ward: HP buffer = 2× Mage level. 5e fluffs this as // recharged when casting an abjuration spell of L1+; we approximate diff --git a/internal/plugin/dnd_subclass_combat_test.go b/internal/plugin/dnd_subclass_combat_test.go index 9bc48b9..07e7af7 100644 --- a/internal/plugin/dnd_subclass_combat_test.go +++ b/internal/plugin/dnd_subclass_combat_test.go @@ -1020,6 +1020,55 @@ func TestInitSubclassResources_BattleMasterL15GrowsPool(t *testing.T) { } } +// ── SUB3a-iii — Thief L10/L15 ─────────────────────────────────────────── + +func TestApplySubclassPassives_ThiefL10UseMagicDevice(t *testing.T) { + c := &DnDCharacter{Class: ClassRogue, Subclass: SubclassThief, Level: 10} + stats := &CombatStats{} + mods := &CombatModifiers{DamageReduct: 1.0} + applySubclassPassives(stats, mods, c) + if mods.FlatDmgStart != 6 { + t.Errorf("L10 Thief FlatDmgStart = %d, want 6 (UMD wand zap)", mods.FlatDmgStart) + } + if mods.DamageBonus < 0.049 || mods.DamageBonus > 0.051 { + t.Errorf("L10 Thief DamageBonus = %v, want ~0.05 (UMD)", mods.DamageBonus) + } + if mods.AssassinateAdvantage { + t.Error("L10 Thief should not have AssassinateAdvantage (L15 only)") + } + if mods.AssassinateBonusDmg != 0 { + t.Errorf("L10 Thief AssassinateBonusDmg = %d, want 0 (L15 only)", mods.AssassinateBonusDmg) + } +} + +func TestApplySubclassPassives_ThiefL9NoUseMagicDevice(t *testing.T) { + c := &DnDCharacter{Class: ClassRogue, Subclass: SubclassThief, Level: 9} + mods := &CombatModifiers{DamageReduct: 1.0} + applySubclassPassives(&CombatStats{}, mods, c) + if mods.FlatDmgStart != 0 { + t.Errorf("L9 Thief FlatDmgStart = %d, want 0 (UMD gates at L10)", mods.FlatDmgStart) + } + if mods.DamageBonus != 0 { + t.Errorf("L9 Thief DamageBonus = %v, want 0", mods.DamageBonus) + } +} + +func TestApplySubclassPassives_ThiefL15ThiefsReflexes(t *testing.T) { + c := &DnDCharacter{Class: ClassRogue, Subclass: SubclassThief, Level: 15} + mods := &CombatModifiers{DamageReduct: 1.0} + applySubclassPassives(&CombatStats{}, mods, c) + if !mods.AssassinateAdvantage { + t.Error("L15 Thief should have AssassinateAdvantage (Thief's Reflexes)") + } + if mods.AssassinateBonusDmg != 8 { + t.Errorf("L15 Thief AssassinateBonusDmg = %d, want 8 (extra turn proxy)", mods.AssassinateBonusDmg) + } + // L10 layer must still apply. + if mods.FlatDmgStart != 6 { + t.Errorf("L15 Thief FlatDmgStart = %d, want 6 (L10 UMD still active)", mods.FlatDmgStart) + } +} + func TestInitSubclassResources_BattleMasterL15PreservesSpentDice(t *testing.T) { // Player burned through 3 dice (current=1, max=4), then levels up to 15. // The growth delta (+1) should be added to current → cur=2, max=5. We