mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Adv 2.0 D&D Phase 10 SUB3a-ii: Battle Master L10/L15
L10 Improved Combat Superiority — superiority dice grow d8 → d10. The two scalar maneuvers (Precision Attack +4→+5; Rally heal +4→+5) bump in their Apply hooks; Trip Attack has no scalar to scale. L15 Relentless — 5e regenerates one die when initiative is rolled with an empty pool. Proxied as +1 max pool size (4→5), which gives the same "always at least one die per encounter" guarantee across the long-rest cycle without introducing per-fight regen state. subclassResourceMax now takes level; initSubclassResources upserts the max (and bumps current by the delta) so existing characters grow on level-up. Hooked into the dnd_xp level-up loop so Battle Masters crossing L15 see the new die without waiting for a long rest. 4 new tests; no regression in subclass/resource/level-up paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -472,7 +472,7 @@ func TestInitSubclassResources_BattleMasterGetsSuperiority(t *testing.T) {
|
||||
if err := SaveDnDCharacter(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := initSubclassResources(uid, SubclassBattleMaster); err != nil {
|
||||
if err := initSubclassResources(uid, SubclassBattleMaster, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cur, max, err := getResource(uid, "superiority")
|
||||
@@ -490,7 +490,7 @@ func TestInitSubclassResources_NonBattleMasterIsNoop(t *testing.T) {
|
||||
if err := createAdvCharacter(uid, "bm_noop"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := initSubclassResources(uid, SubclassChampion); err != nil {
|
||||
if err := initSubclassResources(uid, SubclassChampion, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cur, max, err := getResource(uid, "superiority")
|
||||
@@ -546,7 +546,7 @@ func TestArmPrecisionAttack_AllowedForBattleMaster(t *testing.T) {
|
||||
if err := SaveDnDCharacter(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := initSubclassResources(uid, SubclassBattleMaster); err != nil {
|
||||
if err := initSubclassResources(uid, SubclassBattleMaster, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -941,3 +941,105 @@ func TestApplySubclassPassives_BerserkerL15Retaliation(t *testing.T) {
|
||||
t.Errorf("L15 Berserker SporeCloud = %d, want 2 (L10 still active)", mods.SporeCloud)
|
||||
}
|
||||
}
|
||||
|
||||
// ── SUB3a-ii — Battle Master L10/L15 ────────────────────────────────────
|
||||
|
||||
func TestPrecisionAttack_L10ImprovedSuperiority(t *testing.T) {
|
||||
// L9: d8 → +4. L10: d10 → +5.
|
||||
ab := dndActiveAbilities["precision_attack"]
|
||||
|
||||
c9 := &DnDCharacter{Class: ClassFighter, Subclass: SubclassBattleMaster, Level: 9}
|
||||
mods9 := &CombatModifiers{}
|
||||
ab.Apply(c9, mods9)
|
||||
if mods9.FirstAttackBonus != 4 {
|
||||
t.Errorf("L9 Precision Attack FirstAttackBonus = %d, want 4 (d8)", mods9.FirstAttackBonus)
|
||||
}
|
||||
|
||||
c10 := &DnDCharacter{Class: ClassFighter, Subclass: SubclassBattleMaster, Level: 10}
|
||||
mods10 := &CombatModifiers{}
|
||||
ab.Apply(c10, mods10)
|
||||
if mods10.FirstAttackBonus != 5 {
|
||||
t.Errorf("L10 Precision Attack FirstAttackBonus = %d, want 5 (d10)", mods10.FirstAttackBonus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRally_L10ImprovedSuperiority(t *testing.T) {
|
||||
// CHA 14 → mod +2. L9: 4 + 2 = 6; L10: 5 + 2 = 7.
|
||||
ab := dndActiveAbilities["rally"]
|
||||
|
||||
c9 := &DnDCharacter{Class: ClassFighter, Subclass: SubclassBattleMaster, Level: 9, CHA: 14}
|
||||
mods9 := &CombatModifiers{}
|
||||
ab.Apply(c9, mods9)
|
||||
if mods9.HealItem != 6 {
|
||||
t.Errorf("L9 Rally HealItem = %d, want 6 (d8 + CHA)", mods9.HealItem)
|
||||
}
|
||||
|
||||
c10 := &DnDCharacter{Class: ClassFighter, Subclass: SubclassBattleMaster, Level: 10, CHA: 14}
|
||||
mods10 := &CombatModifiers{}
|
||||
ab.Apply(c10, mods10)
|
||||
if mods10.HealItem != 7 {
|
||||
t.Errorf("L10 Rally HealItem = %d, want 7 (d10 + CHA)", mods10.HealItem)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubclassResourceMax_BattleMasterL15Relentless(t *testing.T) {
|
||||
// Pre-L15: 4 dice. L15+: 5 dice (Relentless approximation).
|
||||
if _, max := subclassResourceMax(SubclassBattleMaster, 14); max != 4 {
|
||||
t.Errorf("L14 Battle Master max = %d, want 4", max)
|
||||
}
|
||||
if _, max := subclassResourceMax(SubclassBattleMaster, 15); max != 5 {
|
||||
t.Errorf("L15 Battle Master max = %d, want 5", max)
|
||||
}
|
||||
if _, max := subclassResourceMax(SubclassBattleMaster, 20); max != 5 {
|
||||
t.Errorf("L20 Battle Master max = %d, want 5", max)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitSubclassResources_BattleMasterL15GrowsPool(t *testing.T) {
|
||||
// Seed at L5 (max=4), then re-init at L15 — pool max should grow to 5,
|
||||
// and current should bump by the same delta so a freshly-rested player
|
||||
// gets the new die immediately rather than after a long rest.
|
||||
setupAuditTestDB(t)
|
||||
uid := id.UserID("@bm_relentless:example")
|
||||
if err := createAdvCharacter(uid, "bm_relentless"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := initSubclassResources(uid, SubclassBattleMaster, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cur, max, _ := getResource(uid, "superiority")
|
||||
if cur != 4 || max != 4 {
|
||||
t.Fatalf("L5 pool = %d/%d, want 4/4", cur, max)
|
||||
}
|
||||
if err := initSubclassResources(uid, SubclassBattleMaster, 15); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cur, max, _ = getResource(uid, "superiority")
|
||||
if cur != 5 || max != 5 {
|
||||
t.Errorf("L15 pool after reconcile = %d/%d, want 5/5", cur, max)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// don't refill to max; that's the long-rest's job.
|
||||
setupAuditTestDB(t)
|
||||
uid := id.UserID("@bm_partial:example")
|
||||
if err := createAdvCharacter(uid, "bm_partial"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := initSubclassResources(uid, SubclassBattleMaster, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ok, err := spendResource(uid, "superiority", 3); err != nil || !ok {
|
||||
t.Fatalf("spendResource: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if err := initSubclassResources(uid, SubclassBattleMaster, 15); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cur, max, _ := getResource(uid, "superiority")
|
||||
if cur != 2 || max != 5 {
|
||||
t.Errorf("partially spent + L15 grow = %d/%d, want 2/5", cur, max)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user