UX S6: race truth-up — wire Tiefling fire resist + best-fit hints

R22: replace race copy that promised mechanics the engine doesn't deliver.
- Tiefling: wire FireResist as a CombatModifier. Enemy main attack is
  halved when monster is FireAttacker-tagged; aoe_fire abilities are
  halved unconditionally; fire-tagged traps deal half damage to Tieflings.
  DnDMonsterTemplate carries FireAttacker; toCombatStats propagates it.
  Hand-authored fire entries tagged in dnd_bestiary.go (flameskull,
  magmin, azer, salamander, fire_elemental, emberlord_thyrak,
  young_red_dragon, infernax, belaxath).
- Open5e tuned generator derives FireAttacker from the highest-AvgDamage
  attack's DamageType (threshold AvgDamage>=5). 19 tuned monsters tag.
  Regenerated bestiary_tuned_data.go.
- Elf: drop "immune to sleep" (no sleep mechanic); reframe as keen
  senses + trance flavor.
- Half-Elf: drop "two bonus skill proficiencies" (no skill system);
  reframe as adaptable cross-cultural know-how.
- Tiefling copy: drop "bonus on CHA checks" (no checks); keep fire
  resistance with flavor framing.

R23: DnDRaceInfo grows a BestFit field; renderRaceMenu emits an
"_best with: …_" hint per race so spiky stat spreads (Orc -1/-1/-1)
read as specialist picks rather than a brick of penalties.

R24: dnd.go header comment for the caster classes now reflects the
shipped state (Playable=true, spell lists populated) instead of the
pre-Open5e scaffold language.

Tests: TestApplyRacePassives gains a FireResist column; new
TestTieflingFireResistance asserts ~0.5x ratio over a 300-trial sweep
against a FireAttacker enemy. Full suite green.
This commit is contained in:
prosolis
2026-05-14 22:06:22 -07:00
parent 1512f6cc50
commit 6386161402
9 changed files with 198 additions and 67 deletions

View File

@@ -67,16 +67,17 @@ func genTuned() error {
// genTunedMonster mirrors plugin.DnDMonsterTemplate. Ability is nil when none
// of the creature's SRD traits map onto an engine effect.
type genTunedMonster struct {
ID, Name string
CR float64
HP, AC int
Attack int
AttackBonus int
Speed int
BlockRate float64
XPValue int
Ability *genMonsterAbility
Notes string
ID, Name string
CR float64
HP, AC int
Attack int
AttackBonus int
Speed int
BlockRate float64
XPValue int
Ability *genMonsterAbility
Notes string
FireAttacker bool
}
// genMonsterAbility mirrors plugin.MonsterAbility.
@@ -95,21 +96,38 @@ func tuneMonster(b genStatBlock) genTunedMonster {
}
ability := abilityFromTraits(b.Traits)
return genTunedMonster{
ID: b.Slug,
Name: stripNameParenthetical(b.Name),
CR: b.CR,
HP: b.HP,
AC: ac,
Attack: attackByCR(b.CR),
AttackBonus: primaryAttackBonus(b.Attacks),
Speed: tunedSpeed(b.SpeedWalk),
BlockRate: tunedBlockRate(ac),
XPValue: b.XP,
Ability: ability,
Notes: tunedNotes(b, ability),
ID: b.Slug,
Name: stripNameParenthetical(b.Name),
CR: b.CR,
HP: b.HP,
AC: ac,
Attack: attackByCR(b.CR),
AttackBonus: primaryAttackBonus(b.Attacks),
Speed: tunedSpeed(b.SpeedWalk),
BlockRate: tunedBlockRate(ac),
XPValue: b.XP,
Ability: ability,
Notes: tunedNotes(b, ability),
FireAttacker: isFireAttacker(b.Attacks),
}
}
// isFireAttacker returns true when a monster's signature damage is fire —
// i.e., its highest-average-damage attack deals fire. Threshold at AvgDamage
// >= 5 so a trivial fire fleck on an otherwise physical creature doesn't tag
// the whole bestiary entry. Tieflings (FireResist) take half damage from the
// primary attack of any creature flagged here.
func isFireAttacker(attacks []genStatAttack) bool {
bestAvg, bestType := 0, ""
for _, a := range attacks {
if a.AvgDamage > bestAvg {
bestAvg = a.AvgDamage
bestType = a.DamageType
}
}
return bestType == "fire" && bestAvg >= 5
}
// stripNameParenthetical drops a trailing "(…)" qualifier from a monster
// name so the player-facing display reads "Giant Rat" rather than "Giant Rat
// (Diseased)". The slug still carries the variant, so the engine keeps both
@@ -345,7 +363,11 @@ func buildTunedBestiarySRD() map[string]DnDMonsterTemplate {
strconv.FormatFloat(m.Ability.ProcChance, 'g', -1, 64),
m.Ability.Effect)
}
fmt.Fprintf(&b, "Notes: %q},\n", m.Notes)
fmt.Fprintf(&b, "Notes: %q", m.Notes)
if m.FireAttacker {
b.WriteString(", FireAttacker: true")
}
b.WriteString("},\n")
}
b.WriteString("\t}\n}\n")
return b.Bytes()