Skip to content

Talent-aware bot behavior: Zealot Martyrdom as proof-of-concept #38

Description

@hummat

Summary

Teach bots to adapt their playstyle based on equipped keystones, starting with Zealot Martyrdom as proof-of-concept. A Martyrdom Zealot should stay at low health (where it gets up to +50% melee damage, +50% attack speed, -37.5% toughness DR) and refuse healing that would remove those stacks.

Requires Tertium 5/6 — vanilla bots have talents = {} and never have keystones.

Background: How Martyrdom Works

  • Passive keystone — always active, no activation needed
  • Grants stacking bonuses per missing health segment (max 5 stacks):
    • Base: +10% melee damage per stack (max +50%)
    • Optional modifier: +10% melee attack speed per stack (max +50%)
    • Optional modifier: -7.5% toughness damage taken per stack (max -37.5%)
    • Optional modifier: +0.1 ability CDR per stack per second (max +0.5/s)
  • Internal buff: zealot_martyrdom_base
  • Talent ID: zealot_martyrdom

Detection API

-- Option A: Check talent table (requires Tertium 5/6 to populate talents)
local talent_ext = ScriptUnit.has_extension(unit, "talent_system")
local talents = talent_ext and talent_ext:talents() or {}
local has_martyrdom = talents["zealot_martyrdom"] ~= nil

-- Option B: Check active buff stacks (works if buff is running)
local buff_ext = ScriptUnit.has_extension(unit, "buff_system")
local has_martyrdom = buff_ext and buff_ext:current_stacks("zealot_martyrdom_base") > 0

-- Stack count (for threshold scaling)
local health_ext = ScriptUnit.extension(unit, "health_system")
local max_wounds = health_ext:max_wounds()
local damage_taken = health_ext:damage_taken()
local permanent_damage = health_ext:permanent_damage_taken()
local max_health = health_ext:max_health()
local current_wounds = Health.calculate_num_segments(
    math.max(damage_taken, permanent_damage), max_health, max_wounds
)
local martyrdom_stacks = math.min(max_wounds - current_wounds, 5)

API notes:

  • zealot_martyrdom_grants_ally_power_bonus is a modifier talent special rule, NOT the base keystone — don't use it for base detection
  • buff_extension:has_buff_name() does not exist — use current_stacks() instead
  • Health.damage_taken_percent() does not exist — use 1 - Health.current_health_percent(unit)

Gracefully degrades: vanilla bots and non-Zealots get false, no error.

Implementation Plan

1. Talent detection in build_context()

local talent_ext = ScriptUnit.has_extension(unit, "talent_system")
local talents = talent_ext and talent_ext:talents() or {}
ctx.has_martyrdom = talents["zealot_martyrdom"] ~= nil

2. Suppress healing

Bot healing uses three independent subsystems. Martyrdom bots should opt out of the first two:

System What it does Where tracked Hook target Martyrdom behavior
Health stations Fixed level objects (medicae stations) _update_health_stations() in bot_behavior_extension.lua sets needs_health = true Hook _update_health_stations Skip needs_health = true — Martyrdom bot never queues, freeing charges for others
Deployed med-crates Medical crates placed on ground by players BotGroup._update_pickups_and_deployables_near_player() sets pickup_component.health_deployable Hook the bot_group function or clear health_deployable after assignment Skip — don't interact with deployed med-crates
Pocketable pickups Med-kits / wound cures carried in inventory BotGroup._update_mule_pickups() sets pickup_component.mule_pickup Hook _update_mule_pickups or filter by slot Allow wound cures (slot_pocketable_small / syringe_corruption_pocketable), block med-kits (slot_pocketable / medical_crate_pocketable)

Wound cure exception: Wound cures heal corruption (heal_types.blessing_syringe), not green health. Martyrdom wants low green health but NOT high corruption — corruption reduces max health segments, which reduces available Martyrdom stacks. Wound cures should remain allowed.

Distinguishing med-kits from wound cures: Different pickup templates and inventory slots:

  • Med-kit: medical_crate_pocketable, slot slot_pocketable
  • Wound cure: syringe_corruption_pocketable, slot slot_pocketable_small

3. Adjust heuristic thresholds

Current Zealot heuristics treat low health as emergency → defensive ability use. Martyrdom inverts this:

Heuristic Current behavior Martyrdom adjustment
zealot_dash Activates at toughness < 30% (defensive) Keep — toughness recovery is still valuable at low health
zealot_invisibility Emergency at health < 25% Raise threshold or disable health-based panic — low health is intended
zealot_relic Team toughness support No change — team utility unrelated to Martyrdom

The key change: don't treat low health as a crisis. A Martyrdom Zealot at 20% health with 5 stacks is at peak performance, not in danger.

4. Follow-up: cross-bot Martyrdom awareness

Current heuristics don't inspect ally health — ogryn_taunt keys off target_ally_needs_aid (a perception/disable signal, not "ally is low HP"). So there's no immediate risk of other bots panic-rescuing a low-health Martyrdom Zealot.

If future heuristics add ally-health checks (e.g. #10 rescue dash), they should exclude Martyrdom Zealots from the "allies in trouble" calculation. This is a follow-up concern, not part of the initial implementation.

5. Optional: prefer melee

All Martyrdom bonuses are melee-only. Could bias weapon selection toward melee when stacks are high, but this is a stretch goal — the existing BT melee/ranged switching works acceptably.

Future: Other Keystones

This establishes a talent-aware framework that can extend to:

Keystone Class Behavioral change Detection
Martyrdom Zealot Don't heal, fight at low HP talents["zealot_martyrdom"] or current_stacks("zealot_martyrdom_base") > 0
Alt Peril Explosion Psyker Higher peril tolerance (no knockdown) has_special_rule("psyker_no_knock_down_overload")
Discharge Debuff Psyker Gate Shout behind safety window (+10% dmg taken for 8s) has_special_rule("psyker_discharge_damage_debuff")
Chemical Dependency Broker Spam abilities to maintain stacking buff has_special_rule("broker_keystone_chemical_dependency_sub_1_crit_chance")
Adrenaline Junkie Broker Melee-weighted, chain abilities during Frenzy has_special_rule("broker_keystone_adrenaline_junkie_extra_killing_blow_stacks")
Carapace Armor Ogryn Stack management via Charge/Taunt timing has_special_rule("ogryn_carapace_armor_explosion_on_zero_stacks")

Start with Martyrdom as the simplest (binary "don't heal" rule), then extend the framework.

Scope & Constraints

  • Tertium-only: Vanilla bots have talents = {}. Graceful degradation — feature simply doesn't activate.
  • No new modules needed: Talent check in build_context(), healing hooks in BetterBots.lua, heuristic adjustments in heuristics.lua.
  • No mod settings needed initially: Auto-detect from talent. Could add a manual override toggle later ("Force Martyrdom playstyle" for vanilla bot users).
  • Testing: Unit tests can mock talent_extension in build_context(). In-game validation requires Tertium 5/6 + Zealot with Martyrdom equipped.

Acceptance Criteria

  • Martyrdom Zealot bot does not use health stations
  • Martyrdom Zealot bot does not interact with deployed med-crates
  • Martyrdom Zealot bot does not pick up pocketable med-kits
  • Martyrdom Zealot bot DOES pick up wound cures (corruption healing)
  • zealot_invisibility does not panic-activate at low health when Martyrdom is active
  • Non-Martyrdom Zealots and other classes are unaffected
  • Vanilla bots (no Tertium) are unaffected — no errors, no behavior change
  • build_context() includes has_martyrdom field
  • Unit tests cover Martyrdom detection and healing suppression logic

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3: backlogNice to have, no timelinearea: bot-behaviorGeneral bot behavior beyond ability activationenhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions