class_action
Registers under Class Actions · source: modules/class_action/v1
`action_cost` — see engine.action_economy.ActionCost; which of the two per-round budgets this draws from is Content-authored per ability, never hardcoded to a category (per the doc's explicit rejection of category-specific engine logic). `essence_type_id`/`essence_cost_category` — combat-engine-readme.md → Essence: "Governs how often abilities/effects can be used." Basic Attacks are explicitly exempt ("always free, regardless of Essence state") — Class Actions are exactly the "special abilities/spells" engine.essence.spend()'s docstring says it's for. Both are required, not defaulted: which pool an ability draws from, and how much, are real per-ability choices (typically your class's weapon category's baseEssenceTypeId — see content/weapon_categories/ — though nothing enforces that link; a class could flavor an ability to cost a different pool). See bot/engine_bridge.py's live_duel_class_action for where this actually gets checked/spent. `timedStatModifier` payload: {"category": one of VALID_BONUS_CATEGORIES, "modifierPercent": float, "durationRounds": int, "appliesTo": "self" | "enemy"}. Resolved via gamemaster/engine/ primitives/effects.py's Timed + tick_timed, summed against the category's base value through primitives.bonuses.total_bonus — see bot/engine_bridge.py's live_duel_class_action. `appliesTo` is required, not defaulted — which side an ability affects is a real design choice per ability, not something safe to assume silently. Self/enemy only in this first slice; a real ally-targeted (Help category) version needs the still-unwired Targeting primitive (see primitives/targeting.py) to pick which ally, not just "the enemy" (a 1v1 duel's only other combatant, no picking required). `shield` payload: {"absorbAmount": float, "durationRounds": int, "damageTypeScope": Optional[str]}. Now explicitly a Timed effect (see gamemaster/engine/primitives/effects.py's Shield dataclass + tick_shield) rather than a bare accumulating float: absorbs the caster's own next `absorbAmount` of incoming raw damage before Mitigation (see pipeline.apply_shield), self-only (an ally-targeted version needs Targeting, same gap as timedStatModifier above), and expires on EITHER duration-end OR being fully absorbed, whichever happens first (see tick_shield's own docstring). `damageTypeScope` is optional — omit it for a shield that absorbs any incoming damage type; set it to one DamageType id for a type-scoped ward (a "Fire Ward" absorbing only Fire damage — a real, confirmed feature, not hypothetical). Casting a second shield while one's still up always appends a new, independently-tracked entry (see session["playerShields"]) rather than merging into an existing one — simpler than reconciling two different scopes or durations into one pool, and not something the doc specifies either way. `crowdControl` payload: {"blockedActionTypes": a non-empty subset of VALID_BLOCKED_ACTION_TYPES, or the literal string "all", "durationRounds": int, "blocksEssenceSpend": bool}. Enemy-only in this first slice — there's no consumer for a self-inflicted lockout (nothing currently forces that on the player), so `appliesTo` isn't even accepted here, unlike timedStatModifier where the choice is real either way. Resolved via effects.py's Timed + tick_timed the same way as a stat-modifier effect, checked as a "does the enemy skip their turn entirely" gate in live_duel_end_round (specifically: is "attack" in the effective blockedActionTypes, since a basic Attack is the only thing the enemy AI ever queues) rather than feeding total_bonus. `blocksEssenceSpend` is validated and stored but currently a no-op — the enemy has no Essence pool of its own to be blocked from spending in this first slice (same "wireable now, no-op until its consumer exists" honesty as weaponTierUnlock). `dispel` payload: {"mode": "specific" | "all", "appliesTo": "self" | "enemy", "effectId": required only when mode == "specific"}. The one primitive that removes rather than applies (see effects.py's Dispel dataclass). `appliesTo` picks which side's active-effect stacks to search — reuses the exact same self/enemy choice timedStatModifier already makes, rather than inventing a second targeting scheme for the same question. "specific" strips exactly one active effect whose originating `classActionId` matches `effectId` (every active-effect entry this bot tracks is already keyed by the id of the Class Action that created it — see session["playerActiveEffects"]/ ["enemyActiveEffects"]/["enemyCrowdControl"]/["playerShields"] — so this reuses that existing identifier rather than inventing a separate "effect id" scheme the engine doesn't otherwise produce anywhere); "all" is a full cleanse of every active Timed/CrowdControl/ Shield effect on that side, no partial category filtering. See bot/engine_bridge.py's live_duel_class_action for exactly which session lists each side maps to. `hybridDamageNextHit` payload: {"damageTypeId": str} — combat-engine- readme.md → Essence: "Hybrid damage (a tipped arrow...) costs the 'mana' EssenceType in addition to the weapon's normal Essence cost." Content-authored as a Ranged-flavored ability costing `ammo` (Ranged's specialEssenceTypeId — see weapon_category/v1), representing nocking one special/elemental tipped arrow: the caster's NEXT Attack this fight also deals `damageTypeId` alongside the weapon's normal type(s), resolved via the same N-way hybrid split (pl.mitigate_hybrid) an Artificer-added permanent hybrid type already uses — this is the temporary, per-shot version of that, not a permanent weapon change. See bot/engine_bridge.py's live_duel_class_action/live_duel_attack for how the pending type gets set and consumed. Doc's literal "costs Mana IN ADDITION TO the weapon's normal cost" (a second, always-Mana charge on top of whatever pool the ability's own essence_type_id already draws from) is NOT modeled here — this module only supports one essence_type_id per action, so this is a deliberate, flagged simplification (single cost, not the doc's literal dual-resource charge), not a silent reading of an unambiguous spec — the doc itself calls the exact cost shape "deferred." `instantDamage` payload: {"diceCount": int, "dieSize": int, "relevantStatId": str, "damageTypeId": str} — the sixth and last combat Effect Primitive (Instant, see gamemaster/engine/primitives/ effects.py) finally gets a real consumer (2026-10-19, DND-27 playtest finding: dive_bomb was crowdControl-only — all stun, no damage, despite its own "dealing damage of the landing impact" flavor text). Design call made with the user: always lands, no Hit/Crit roll at all (a Class Action ability isn't a weapon swing); RawDamage = Hitdie(diceCount, dieSize) x StatMod(relevantStatId) — no ProficiencyDamageMod%, CritMultiplier, or gearLevel bonus, since none of those are weapon-attack concepts — then Mitigation against the enemy's Resistance% for damageTypeId, same as any other damage reaching them. `damage_relevant_stat_id` is the core stat id (e.g. "strength") the flat/community-settable path builds effect_payload["relevantStatId"] from — it can be ANY core stat, not necessarily the caster's weapon's own relevant stat; see bot/engine_bridge.py's live_duel_class_action, which reads it off session["attackerStats"] rather than the resolved weapon profile.
Example Usage
# Migrated from content/class_actions/arcane_barrier.py (2026-10-14
# cleanup pass) -- effect_payload={"absorbAmount": 20.0, "durationRounds": 3}
# becomes the flat absorb_amount/duration_rounds fields modules/class_action/v1
# builds the same payload from (see that module's own _PAYLOAD_BUILDERS).
resource "arcane_barrier" {
source: modules/class_action/v1
id: "arcane_barrier"
class_id: "wizard"
action_cost: "bonusAction"
essence_type_id: "mana"
essence_cost_category: "medium"
effect_kind: "shield"
absorb_amount: 20.0
duration_rounds: 3
description: "A shimmering ward absorbs the next 20 damage before it touches you."
}
Arguments Reference
The following arguments are supported:
-
id- (Required) The unique id this record is registered and referenced under — what a `.resource` file's own filename-independent identity is, and what any other content's foreign-key-style field (a dungeon node's `next`, a skill node's `parent_id`, and so on) points at. community -
class_id- (Required) Currently registered:ranger,skylancer,warriorandwizard. community -
action_cost- (Required) `action_cost` — see engine.action_economy.ActionCost; which of the two per-round budgets this draws from is Content-authored per ability, never hardcoded to a category (per the doc's explicit rejection of category-specific engine logic). Valid options areactionandbonusAction. community -
essence_type_id- (Required) `essence_type_id`/`essence_cost_category` — combat-engine-readme.md → Essence: "Governs how often abilities/effects can be used." Basic Attacks are explicitly exempt ("always free, regardless of Essence state") — Class Actions are exactly the "special abilities/spells" engine.essence.spend()'s docstring says it's for. Currently registered:ammo,manaandstamina. community -
essence_cost_category- (Required) Valid options areheavy,lightandmedium. community -
effect_kind- (Required) Valid options arecrowdControl,dispel,hybridDamageNextHit,instantDamage,shieldandtimedStatModifier. community -
effect_payload- (Optional) Defaults toNone. developer -
description- (Optional) Flavor text shown to players — pure display data with no effect on game mechanics. community -
bonus_category- (Optional) Valid options areevasivenessPercentandtoHitPercent. community -
modifier_percent- (Optional) Defaults to0.0. community -
duration_rounds- (Optional) Defaults to0. community -
applies_to- (Optional) Valid options areenemyandself. community -
absorb_amount- (Optional) Defaults to0.0. community -
damage_type_scope- (Optional) Currently registered:air,blunt,earth,fire,necrotic,piercing,poison,psychic,radiant,slashingandwater. community -
blocked_action_types- (Optional) Defaults toNone. community -
blocks_essence_spend- (Optional) Defaults toFalse. community -
dispel_mode- (Optional) Valid options areallandspecific. community -
dispel_effect_id- (Optional) Currently registered:arcane_barrier,battle_cry,dive_bombandhobbling_shot. community -
hybrid_damage_type_id- (Optional) Currently registered:air,blunt,earth,fire,necrotic,piercing,poison,psychic,radiant,slashingandwater. community -
damage_dice_count- (Optional) Defaults to0. community -
damage_die_size- (Optional) Defaults to0. community -
damage_relevant_stat_id- (Optional) `damage_relevant_stat_id` is the core stat id (e.g. "strength") the flat/community-settable path builds effect_payload["relevantStatId"] from — it can be ANY core stat, not necessarily the caster's weapon's own relevant stat; see bot/engine_bridge.py's live_duel_class_action, which reads it off session["attackerStats"] rather than the resolved weapon profile. community -
damage_type_id- (Optional) Currently registered:air,blunt,earth,fire,necrotic,piercing,poison,psychic,radiant,slashingandwater. community