dnd5e-engine 0.4.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dnd5e_engine-0.6.0/CHANGELOG.md +1032 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/PKG-INFO +2 -2
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/pyproject.toml +9 -7
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/__init__.py +20 -24
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/actor_stats.py +83 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/apply.py +215 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/attack.py +1308 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/build_context.py +579 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/check.py +66 -22
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/conjuration.py +481 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/context.py +566 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/d20.py +97 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/damage.py +22 -2
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/dice.py +13 -7
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/effects.py +26 -5
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/forced_movement.py +40 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/formula.py +44 -8
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/heal.py +14 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/mastery.py +271 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/monster_actions.py +166 -2
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/resolver.py +164 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/save.py +61 -17
- dnd5e_engine-0.6.0/src/dnd5e_engine/activities/save_primitive.py +340 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/scale.py +56 -24
- dnd5e_engine-0.6.0/src/dnd5e_engine/build_party.py +78 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/build_spec.py +776 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/check.py +13 -1
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/death_saves.py +38 -10
- dnd5e_engine-0.6.0/src/dnd5e_engine/events.py +861 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/orchestrator.py +13464 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rest.py +69 -5
- dnd5e_engine-0.6.0/src/dnd5e_engine/rules/character.py +577 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/rules/choices.py +147 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/conditions.py +179 -26
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/skills.py +92 -12
- dnd5e_engine-0.6.0/src/dnd5e_engine/rules/uses.py +93 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/spatial.py +636 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/specs.py +121 -10
- dnd5e_engine-0.6.0/src/dnd5e_engine/spellcasting.py +312 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/turn_lifecycle.py +128 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/types/__init__.py +18 -0
- dnd5e_engine-0.6.0/src/dnd5e_engine/types/combat.py +388 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/types/effects.py +45 -5
- dnd5e_engine-0.6.0/src/dnd5e_engine/views.py +220 -0
- dnd5e_engine-0.6.0/tests/activities/test_actor_stats.py +51 -0
- dnd5e_engine-0.6.0/tests/activities/test_apply_damage_magical.py +105 -0
- dnd5e_engine-0.6.0/tests/activities/test_attack_advantage_live.py +276 -0
- dnd5e_engine-0.6.0/tests/activities/test_attack_condition_rows.py +169 -0
- dnd5e_engine-0.6.0/tests/activities/test_attack_unseen.py +90 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_build_context.py +70 -6
- dnd5e_engine-0.6.0/tests/activities/test_c18_trait_consumers.py +345 -0
- dnd5e_engine-0.6.0/tests/activities/test_conjuration_routing.py +259 -0
- dnd5e_engine-0.6.0/tests/activities/test_d20.py +57 -0
- dnd5e_engine-0.6.0/tests/activities/test_damage_count_marker_scaling.py +79 -0
- dnd5e_engine-0.6.0/tests/activities/test_monster_action_ranking.py +216 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_monster_actions.py +61 -16
- dnd5e_engine-0.6.0/tests/activities/test_multiattack_opaque_ids.py +48 -0
- dnd5e_engine-0.6.0/tests/activities/test_resolve_check_activity.py +783 -0
- dnd5e_engine-0.6.0/tests/activities/test_save_dc_and_damage_exprs.py +160 -0
- dnd5e_engine-0.6.0/tests/activities/test_save_ignore_cover.py +42 -0
- dnd5e_engine-0.6.0/tests/activities/test_save_magic_resistance.py +64 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_save_primitive.py +202 -26
- dnd5e_engine-0.6.0/tests/activities/test_summon_registry.py +241 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_weapon_mastery.py +63 -15
- dnd5e_engine-0.6.0/tests/c20_support.py +112 -0
- dnd5e_engine-0.6.0/tests/c21_support.py +206 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/harness.py +12 -1
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c06_reactions.py +10 -3
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c07_sneak_and_repertoire.py +59 -23
- dnd5e_engine-0.6.0/tests/e2e/test_c12_conditions.py +481 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c13_concentration.py +445 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c14_action_economy.py +628 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c15_attack_rules.py +542 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c16_spatial_targeting.py +758 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c17_slots_and_rests.py +299 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c18_monster_economy.py +629 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c19_character_derivation.py +387 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c20_class_features.py +805 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c21_summons.py +494 -0
- dnd5e_engine-0.6.0/tests/e2e/test_c22_dataset.py +506 -0
- dnd5e_engine-0.6.0/tests/test_action_economy_attacks.py +761 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_active_effect_resistance_fold.py +8 -2
- dnd5e_engine-0.6.0/tests/test_active_effect_statuses_order.py +64 -0
- dnd5e_engine-0.6.0/tests/test_attack_classification.py +80 -0
- dnd5e_engine-0.6.0/tests/test_attack_range_tiers.py +433 -0
- dnd5e_engine-0.6.0/tests/test_build_party_c19.py +169 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_build_party_passives.py +2 -1
- dnd5e_engine-0.6.0/tests/test_build_spec_multiclass.py +100 -0
- dnd5e_engine-0.6.0/tests/test_c16_orchestrator.py +955 -0
- dnd5e_engine-0.6.0/tests/test_c16_spatial.py +243 -0
- dnd5e_engine-0.6.0/tests/test_c16b_vision.py +714 -0
- dnd5e_engine-0.6.0/tests/test_c17_slots.py +550 -0
- dnd5e_engine-0.6.0/tests/test_c18_monster_economy_units.py +1428 -0
- dnd5e_engine-0.6.0/tests/test_c20_action_surge.py +232 -0
- dnd5e_engine-0.6.0/tests/test_c20_bardic_inspiration.py +245 -0
- dnd5e_engine-0.6.0/tests/test_c20_cunning_action.py +106 -0
- dnd5e_engine-0.6.0/tests/test_c20_feature_uses.py +303 -0
- dnd5e_engine-0.6.0/tests/test_c20_fighting_styles.py +223 -0
- dnd5e_engine-0.6.0/tests/test_c20_multiclass_live.py +122 -0
- dnd5e_engine-0.6.0/tests/test_c20_rage.py +216 -0
- dnd5e_engine-0.6.0/tests/test_c20_unarmed_and_monk.py +321 -0
- dnd5e_engine-0.6.0/tests/test_c21_concentration_anchor.py +262 -0
- dnd5e_engine-0.6.0/tests/test_c21_magic_weapon.py +298 -0
- dnd5e_engine-0.6.0/tests/test_c21_polymorph.py +382 -0
- dnd5e_engine-0.6.0/tests/test_c21_roster_events.py +95 -0
- dnd5e_engine-0.6.0/tests/test_c21_roster_primitives.py +358 -0
- dnd5e_engine-0.6.0/tests/test_c21_spiritual_weapon.py +495 -0
- dnd5e_engine-0.6.0/tests/test_c21_stat_block_commands.py +321 -0
- dnd5e_engine-0.6.0/tests/test_c21_summon_allegiance.py +566 -0
- dnd5e_engine-0.6.0/tests/test_c21_summon_dragon.py +485 -0
- dnd5e_engine-0.6.0/tests/test_c21_summon_turns.py +523 -0
- dnd5e_engine-0.6.0/tests/test_c21_surface.py +87 -0
- dnd5e_engine-0.6.0/tests/test_c21_transform_core.py +390 -0
- dnd5e_engine-0.6.0/tests/test_c21_wild_shape.py +393 -0
- dnd5e_engine-0.6.0/tests/test_capability_matrix.py +827 -0
- dnd5e_engine-0.6.0/tests/test_combat_bucket_keys.py +61 -0
- dnd5e_engine-0.6.0/tests/test_combat_flag_advantage.py +25 -0
- dnd5e_engine-0.6.0/tests/test_concentration_check_event.py +193 -0
- dnd5e_engine-0.6.0/tests/test_concentration_lifecycle_drops.py +261 -0
- dnd5e_engine-0.6.0/tests/test_concentration_save_modifier.py +102 -0
- dnd5e_engine-0.6.0/tests/test_concentration_timed_expiry.py +168 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_cover_bonus_sidecar.py +20 -10
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_coverage_backfill_death_saves.py +24 -0
- dnd5e_engine-0.6.0/tests/test_death_saves_exhaustion.py +54 -0
- dnd5e_engine-0.6.0/tests/test_derive_sheet.py +605 -0
- dnd5e_engine-0.6.0/tests/test_dodge_help_hide.py +834 -0
- dnd5e_engine-0.6.0/tests/test_effect_timed_expiry.py +474 -0
- dnd5e_engine-0.6.0/tests/test_end_combat_retention.py +137 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_gen2_has_no_gen1_imports.py +6 -0
- dnd5e_engine-0.6.0/tests/test_grapple_shove_stand_up.py +564 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_grid_topology.py +14 -9
- dnd5e_engine-0.6.0/tests/test_incapacitated_by_effect.py +179 -0
- dnd5e_engine-0.6.0/tests/test_initiative_rolling.py +305 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_live_combat_view.py +44 -1
- dnd5e_engine-0.6.0/tests/test_loading_property.py +217 -0
- dnd5e_engine-0.6.0/tests/test_monster_legacy_swing.py +121 -0
- dnd5e_engine-0.6.0/tests/test_opportunity_attack_primitive.py +279 -0
- dnd5e_engine-0.6.0/tests/test_orchestrator_condition_gates.py +824 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_grid_combat.py +48 -3
- dnd5e_engine-0.6.0/tests/test_orchestrator_hydration_proficiencies.py +118 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_pc_resolution_typed.py +4 -1
- dnd5e_engine-0.6.0/tests/test_orchestrator_trait_hydration.py +58 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_party_member_reach_ft.py +2 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_public_api_surface.py +11 -12
- dnd5e_engine-0.6.0/tests/test_repeat_save_runs_once_per_turn.py +168 -0
- dnd5e_engine-0.6.0/tests/test_resolve_check_c19.py +77 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rest.py +95 -0
- dnd5e_engine-0.6.0/tests/test_rules_character.py +258 -0
- dnd5e_engine-0.6.0/tests/test_rules_choices.py +67 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rules_combat_data_and_skills.py +0 -149
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rules_conditions.py +174 -2
- dnd5e_engine-0.6.0/tests/test_rules_dice_equipment_spells.py +302 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rules_effects_and_gambits.py +0 -133
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rules_smoke.py +1 -8
- dnd5e_engine-0.6.0/tests/test_rules_uses.py +56 -0
- dnd5e_engine-0.6.0/tests/test_save_cover_hydration.py +155 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_scale_resolver.py +29 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_sneak_attack_resolver.py +18 -8
- dnd5e_engine-0.6.0/tests/test_spatial_distance.py +32 -0
- dnd5e_engine-0.6.0/tests/test_spellcasting.py +197 -0
- dnd5e_engine-0.6.0/tests/test_turn_lifecycle.py +318 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_types_smoke.py +0 -73
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_use_feature_intent.py +27 -12
- dnd5e_engine-0.6.0/tests/test_versatile_and_attribution.py +331 -0
- dnd5e_engine-0.6.0/tests/test_weapon_masteries_c15.py +1140 -0
- dnd5e_engine-0.6.0/tests/test_weapon_proficiency.py +333 -0
- dnd5e_engine-0.4.0/CHANGELOG.md +0 -241
- dnd5e_engine-0.4.0/src/dnd5e_engine/activities/apply.py +0 -102
- dnd5e_engine-0.4.0/src/dnd5e_engine/activities/attack.py +0 -639
- dnd5e_engine-0.4.0/src/dnd5e_engine/activities/build_context.py +0 -323
- dnd5e_engine-0.4.0/src/dnd5e_engine/activities/context.py +0 -254
- dnd5e_engine-0.4.0/src/dnd5e_engine/activities/mastery.py +0 -209
- dnd5e_engine-0.4.0/src/dnd5e_engine/activities/resolver.py +0 -90
- dnd5e_engine-0.4.0/src/dnd5e_engine/activities/save_primitive.py +0 -205
- dnd5e_engine-0.4.0/src/dnd5e_engine/build_party.py +0 -138
- dnd5e_engine-0.4.0/src/dnd5e_engine/build_spec.py +0 -101
- dnd5e_engine-0.4.0/src/dnd5e_engine/dispatch.py +0 -367
- dnd5e_engine-0.4.0/src/dnd5e_engine/event_dicts.py +0 -50
- dnd5e_engine-0.4.0/src/dnd5e_engine/events.py +0 -503
- dnd5e_engine-0.4.0/src/dnd5e_engine/orchestrator.py +0 -5523
- dnd5e_engine-0.4.0/src/dnd5e_engine/rules/_class_meta.py +0 -14
- dnd5e_engine-0.4.0/src/dnd5e_engine/rules/_parsing.py +0 -36
- dnd5e_engine-0.4.0/src/dnd5e_engine/rules/combat.py +0 -595
- dnd5e_engine-0.4.0/src/dnd5e_engine/rules/combat_data.py +0 -243
- dnd5e_engine-0.4.0/src/dnd5e_engine/rules/combat_helpers.py +0 -338
- dnd5e_engine-0.4.0/src/dnd5e_engine/rules/equipment.py +0 -121
- dnd5e_engine-0.4.0/src/dnd5e_engine/rules/gambits.py +0 -193
- dnd5e_engine-0.4.0/src/dnd5e_engine/rules/resolution.py +0 -180
- dnd5e_engine-0.4.0/src/dnd5e_engine/rules/spells.py +0 -165
- dnd5e_engine-0.4.0/src/dnd5e_engine/spatial.py +0 -380
- dnd5e_engine-0.4.0/src/dnd5e_engine/types/__init__.py +0 -49
- dnd5e_engine-0.4.0/src/dnd5e_engine/types/combat.py +0 -229
- dnd5e_engine-0.4.0/src/dnd5e_engine/types/dice.py +0 -46
- dnd5e_engine-0.4.0/src/dnd5e_engine/types/intent.py +0 -187
- dnd5e_engine-0.4.0/src/dnd5e_engine/views.py +0 -68
- dnd5e_engine-0.4.0/tests/activities/test_resolve_check_activity.py +0 -349
- dnd5e_engine-0.4.0/tests/test_capability_matrix.py +0 -125
- dnd5e_engine-0.4.0/tests/test_combat_bucket_keys.py +0 -177
- dnd5e_engine-0.4.0/tests/test_combat_flag_advantage.py +0 -154
- dnd5e_engine-0.4.0/tests/test_gen1_deprecation_warnings.py +0 -63
- dnd5e_engine-0.4.0/tests/test_generation_parity_attack.py +0 -179
- dnd5e_engine-0.4.0/tests/test_rules_combat_helpers.py +0 -654
- dnd5e_engine-0.4.0/tests/test_rules_combat_resolution.py +0 -802
- dnd5e_engine-0.4.0/tests/test_rules_dice_equipment_spells.py +0 -563
- dnd5e_engine-0.4.0/tests/test_rules_resolution_and_parsing.py +0 -368
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/.gitignore +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/LICENSE +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/Makefile +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/NOTICE +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/README.md +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/scripts/_smoke_grid_combat.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/scripts/smoke_clean_install.sh +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/cast.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/activities/passive_stats.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/lib_loader.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/outcome.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/py.typed +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/results.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/dice.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/rules/effects.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/testing.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/src/dnd5e_engine/types/conditions.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_cast_level_override.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/activities/test_multiattack_fidelity.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/conftest.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c00_harness_sanity.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c01_dataset_fixes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c02_small_mechanics.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c03_effect_modes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c04_caster_plumbing.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c05_spatial.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c08_passive_projection.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c09_rest.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c10_monster_behavior.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/e2e/test_c11_item_charges.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_active_effect_schema.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_apply_changes_to_check.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_apply_modifiers.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_build_context_abilities.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_build_party.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_concentration_drop_effect_name.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_concentration_repeat_save_identity.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_coverage_backfill_seam_errors.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_determinism_contract.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_docstrings_are_host_agnostic.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_duration_tick_at_turn_end.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_effects_builder_internals.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_enchantment_sidecar_projection.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_end_combat_snapshot.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_expire_clears_active_conditions.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_feature_save_dc_no_spell_override.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_get_actor_active_effects.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_iter11_fixes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_iter13_fixes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_iter5_fixes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_lib_loader.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_lib_loader_configure.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_monster_dash.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_monster_flee_retreat.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_gating_typed.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_item_charges.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_monster_typed.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_orchestrator_move_mark_typed.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_passive_stats.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_phase6_public_surface.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_rage_second_wind_e2e.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_reaction_queue.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_resolve_check.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_resolve_check_flag_advantage.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_resolve_check_specific_buckets.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_smoke.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_start_combat_active_effects.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_start_combat_seeds_lifecycle.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.6.0}/tests/test_weapon_damage_bonus_sidecar.py +0 -0
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.6.0]
|
|
11
|
+
|
|
12
|
+
Lockstep release with `dnd5e-srd-data` 0.6.0 and `nat20-bridge` 0.6.0. The
|
|
13
|
+
engine now depends on `dnd5e-srd-data>=0.6.0` (it reads the typed monster
|
|
14
|
+
traits and `spellcasting_ability`, the legendary pool sizes, and now-filled
|
|
15
|
+
`Monster.ac` values for every monster); `nat20-bridge` requires both at
|
|
16
|
+
`>=0.6.0`.
|
|
17
|
+
|
|
18
|
+
Core-mechanics **foundations** (F1 actor stat projection, F2 unified d20 test,
|
|
19
|
+
F3 turn lifecycle) and clusters **C12 — conditions enforced**, **C13 —
|
|
20
|
+
concentration lifecycle**, **C14 — action economy** (Dodge, Help, Hide,
|
|
21
|
+
Extra Attack, two-weapon fighting, Grapple/Shove/`stand_up`, engine-rolled
|
|
22
|
+
initiative with Surprise, and opportunity attacks through the shared d20
|
|
23
|
+
primitive), **C15 — attack rules** (weapon proficiency, range tiers,
|
|
24
|
+
thrown weapons, Ranged Attacks in Close Combat, Heavy, versatile grip,
|
|
25
|
+
damage attribution, crit-at-0-HP, Loading, and all eight 2024 weapon
|
|
26
|
+
masteries), **C16 — grid AoE targeting** (typed-template enumeration with
|
|
27
|
+
line-of-effect, creature cover, multi-cell grid movement and forced
|
|
28
|
+
movement), **C16b — vision & light** (scene lighting/obscurement and the
|
|
29
|
+
`_combatant_can_see` composite gating Dodge, Ranged Attacks in Close Combat,
|
|
30
|
+
Opportunity Attacks, Hide and Frightened), **C17 — spell slots, rests and
|
|
31
|
+
upcasting** (per-class/
|
|
32
|
+
multiclass/Pact Magic slot derivation, rest-based slot recovery and
|
|
33
|
+
Exhaustion reduction, upcast target-count scaling, Counterspell/readied-cast
|
|
34
|
+
slot+range gating, and out-of-combat Ritual resolution), **C18 — monster
|
|
35
|
+
action economy** (Recharge, Regeneration, Legendary Actions, Legendary
|
|
36
|
+
Resistance, stat-block spellcasting, and the remaining `MonsterTraitMechanic`
|
|
37
|
+
consumers), **C19 — character derivation** (`derive_sheet`/`DerivedSheet`,
|
|
38
|
+
`build_party_member` explicit-wins merge, `CheckSpec` Jack of All Trades /
|
|
39
|
+
Reliable Talent), **C20 — class feature mechanics** (Fighting Style feats,
|
|
40
|
+
Martial Arts, Flurry of Blows and the Focus economy, Action Surge, Bardic
|
|
41
|
+
Inspiration on attack rolls, Rage's end conditions, Lay on Hands' pool,
|
|
42
|
+
feature-gated Cunning Action, every corpus limited-use cap, and live
|
|
43
|
+
multiclass), **C21a — summons foundations** (every concentration spell
|
|
44
|
+
concentrates; Magic Weapon; Spiritual Weapon's caster-owned force; Wild Shape
|
|
45
|
+
and Polymorph as stat-block swaps with Temporary Hit Points; stat-block attack
|
|
46
|
+
commands) and **C21b — roster summons** (Summon Dragon's Draconic Spirit joins
|
|
47
|
+
the initiative order right after its caster, takes the Dodge action unless
|
|
48
|
+
commanded, and leaves at 0 Hit Points or with its caster's concentration),
|
|
49
|
+
closed out by **C23** (a bounded live-combat registry, sorted
|
|
50
|
+
`ActiveEffect.statuses`, three rules corrections and the 0.6.0 floors).
|
|
51
|
+
Nothing public is removed or renamed, but C19 reshapes two
|
|
52
|
+
existing `CombatInstance` fields — `ac` and `attack_bonus` widen from
|
|
53
|
+
defaulted `int`s to `int | None = None`, matching `hp_max`/`hp_current`/
|
|
54
|
+
`base_speed` — and every other new field across this release is optional
|
|
55
|
+
and defaults to the pre-0.6 behaviour.
|
|
56
|
+
C12/C14/C15/C17/C18 do change *results* for hosts that carry conditions,
|
|
57
|
+
exhaustion, turn-keeping attacks, weapon proficiency/mastery data, casters,
|
|
58
|
+
or monsters with a recharge/limited-use/legendary action or a
|
|
59
|
+
newly-consumed trait on a combatant; C19 changes results for hosts that
|
|
60
|
+
build through `build_party_member` and leave `ac`/`attack_bonus`/`hp_max`/
|
|
61
|
+
`hp_current`/`base_speed` unset (now derived instead of defaulted), or that
|
|
62
|
+
rely on the save/skill/weapon proficiencies it now derives.
|
|
63
|
+
C20 changes results for raging barbarians, Rogue 1s taking a Bonus-Action
|
|
64
|
+
Dash, any creature without Cunning Action sent a `disengage` with
|
|
65
|
+
`use_bonus_action=True` (now refused with
|
|
66
|
+
`IntentRejectedError("no_action_economy")` where it used to spend the Action),
|
|
67
|
+
newly capped features and unarmed strikes, for hosts that send `classes`,
|
|
68
|
+
`feats` or `fighting_style`, and wherever an effect makes a concentrating
|
|
69
|
+
caster or a grappler Incapacitated.
|
|
70
|
+
C21a changes results wherever a concentration spell applies no concentration
|
|
71
|
+
effect of its own (it now concentrates, so an earlier concentration ends and
|
|
72
|
+
later damage draws a Constitution save), for readied concentration spells,
|
|
73
|
+
for Magic Weapon, Wild Shape and Polymorph intents that name no weapon or
|
|
74
|
+
form (now refused before anything is spent), and for a host-driven attacker
|
|
75
|
+
with Pack Tactics (it now gets its Advantage).
|
|
76
|
+
C21b changes results only where a summon is in play: a Summon Dragon cast
|
|
77
|
+
seats a creature, or is refused before anything is spent when it has no legal
|
|
78
|
+
space; a `ready` that names it is refused; and foes may attack the creature.
|
|
79
|
+
Behavioural deltas (and the fixtures they move) are enumerated in
|
|
80
|
+
[`docs/migration/v0.5-to-v0.6.md`](../../docs/migration/v0.5-to-v0.6.md).
|
|
81
|
+
|
|
82
|
+
- **Action economy (C14).** Extra Attack reads a caster's granted
|
|
83
|
+
`extra-attack` / `two-extra-attacks` / `three-extra-attacks` feature slugs
|
|
84
|
+
(highest tier wins, never summed) into a per-Action `attacks_remaining`
|
|
85
|
+
counter; a turn now keeps going after a main-hand attack until that
|
|
86
|
+
counter — and any open two-weapon-fighting Bonus Action window — is spent
|
|
87
|
+
(`LiveCombatView.turn: TurnCombatView`). A Light main-hand weapon opens a
|
|
88
|
+
same-turn off-hand Bonus Action swing (SRD 5.2 §Two-Weapon Fighting). Dodge,
|
|
89
|
+
Help (assist-an-attack-roll flavor) and Hide (DC 15 Stealth, cover/
|
|
90
|
+
obscurement gate, Invisible-while-hidden) now have live dispatch handlers.
|
|
91
|
+
New `IntentType` members `"grapple"`, `"shove"`, `"stand_up"`,
|
|
92
|
+
`"escape_grapple"` resolve the Unarmed Strike Grapple/Shove options, the
|
|
93
|
+
SRD 5.2 "Ending a Grapple" escape check and standing from Prone at half
|
|
94
|
+
Speed. `PlayerIntent.shove_push: bool = False` picks the shover's Prone-or-
|
|
95
|
+
push outcome. `PartyMemberSpec.initiative` / `EncounterMemberSpec.initiative`
|
|
96
|
+
widen to `int | None`: `None` rolls an engine `d20 + DEX modifier` (spec
|
|
97
|
+
order, before all other combat draws), with Disadvantage from the new
|
|
98
|
+
`is_surprised: bool = False` field or a seeded incapacitated-implying
|
|
99
|
+
status. Opportunity attacks now roll through `roll_d20_test` like every
|
|
100
|
+
other attack, picking up condition/Dodge/Exhaustion sources.
|
|
101
|
+
|
|
102
|
+
- **Concentration lifecycle (C13).** The damage-triggered save DC is capped at
|
|
103
|
+
the SRD 5.2 maximum of 30. A new concentration cast cascades the drop of the
|
|
104
|
+
caster's prior concentration effect (SRD "the moment you start casting…").
|
|
105
|
+
Concentration now ends on death and on gaining any Incapacitated-implying
|
|
106
|
+
condition. New `IntentType` member `"drop_concentration"`: a voluntary,
|
|
107
|
+
turn-keeping drop that costs no action economy. Concentration spells now
|
|
108
|
+
expire at their maximum typed duration (1 minute = 10 rounds, …) at the
|
|
109
|
+
caster's turn end via the new `engine:concentration-expiry` turn hook, with
|
|
110
|
+
`EffectExpired(reason="duration")`. `LiveCombatView.concentration_chain`
|
|
111
|
+
projects the caster-keyed ownership map for hosts.
|
|
112
|
+
|
|
113
|
+
- **Attack rules (C15).** Weapon proficiency is a real gate: a host that never
|
|
114
|
+
sets `PartyMemberSpec.weapon_proficiencies` is assumed proficient (the R1
|
|
115
|
+
sentinel — legacy behaviour, byte-identical); an explicit (possibly empty)
|
|
116
|
+
list enforces proficiency by the weapon's category or slug, omitting
|
|
117
|
+
Proficiency Bonus (never subtracting it) when unproficient. Attack range now
|
|
118
|
+
has three tiers — normal, a middle disadvantage tier beyond normal range
|
|
119
|
+
(`"range:long"`), and beyond max range is rejected — and a melee weapon with
|
|
120
|
+
the Thrown property can now attack at range using its throw bands (both were
|
|
121
|
+
previously flatly rejected). Ranged Attacks in Close Combat (SRD 5.2):
|
|
122
|
+
disadvantage when a living, sighted, non-Incapacitated hostile is within 5
|
|
123
|
+
ft of the attacker (`"ranged_in_melee"`). Heavy weapons impose disadvantage
|
|
124
|
+
on a wielder with a raw Strength score below 13 — this and the Vex/Sap
|
|
125
|
+
mastery riders below all ride the same `"trait"` advantage-source token.
|
|
126
|
+
`PlayerIntent.two_handed: bool = False` selects a Versatile weapon's
|
|
127
|
+
two-handed damage die on a melee swing. `DamageApplied` gains `source_id`
|
|
128
|
+
(weapon slug / synthesized activity id / `"mastery:<slug>"`; spell/save/heal
|
|
129
|
+
damage paths still report `None`, a C17+ seam) and `is_crit`; a critical hit
|
|
130
|
+
against a target already making death saves now counts as two failures (SRD
|
|
131
|
+
§Damage at 0 Hit Points). Loading weapons cap at one fired shot per actor
|
|
132
|
+
per turn (`AttackFailed(reason="weapon_already_fired")`, gated ahead of the
|
|
133
|
+
Charmed-target gate). All eight 2024 weapon masteries are live: Graze
|
|
134
|
+
(miss-damage), Topple (Con save vs. `prone`, honoring condition immunity),
|
|
135
|
+
Vex (a 2-round Advantage grant against the same target), Sap (a
|
|
136
|
+
Disadvantage mark on the target until the source's next turn), Slow (a
|
|
137
|
+
flat, non-stacking −10 ft Speed penalty, cleared at the source's next turn
|
|
138
|
+
start), Push (a full 10 ft forced move via `push_combatant`; the "Large or
|
|
139
|
+
smaller" size gate is unmodelled), Cleave (a deterministic, once-per-turn
|
|
140
|
+
chained attack against the nearest eligible living hostile within 5 ft of
|
|
141
|
+
the first target and within reach) and Nick (the off-hand extra attack
|
|
142
|
+
spends no Bonus Action — SRD 5.2 lets it ride the Attack action instead).
|
|
143
|
+
|
|
144
|
+
- **Spell slots, rests and upcasting (C17).** `spellcasting.py` derives
|
|
145
|
+
per-class Spellcasting slots (`derive_spell_slots`), multiclass slots
|
|
146
|
+
(`derive_multiclass_slots`, per-class half/third rounding summed, SRD
|
|
147
|
+
§Multiclassing) and Pact Magic (`derive_pact_slots`,
|
|
148
|
+
`derive_multiclass_pact_slots`); `CharacterBuildSpec.classes: dict[str,
|
|
149
|
+
int]` is the multiclass carrier, reconciled with the existing
|
|
150
|
+
`class_slug`/`level` single-class fields. `resolve_long_rest` restores
|
|
151
|
+
Spellcasting/Pact Magic slots and reduces Exhaustion by 1 (floored at 0);
|
|
152
|
+
`resolve_short_rest` restores only Pact Magic (SRD's only
|
|
153
|
+
Short-Rest-recovering slot pool). A Magic Missile-shaped cast now scales
|
|
154
|
+
its DART COUNT, not just its damage dice, via `target.affects.count` /
|
|
155
|
+
`spellcasting.resolve_target_count` and `PlayerIntent.target_ids`. An
|
|
156
|
+
armed Counterspell/readied-cast reaction is now gated on slot
|
|
157
|
+
availability at its readied level and, for Counterspell, its own 60 ft
|
|
158
|
+
range with line of sight — an ineligible reactor's reaction is skipped,
|
|
159
|
+
not consumed. A new `SpellCast` event carries component/material
|
|
160
|
+
metadata (never enforced) on every PC cast path. `PlayerIntent.
|
|
161
|
+
as_ritual` + `spellcasting.resolve_ritual_cast` resolve Ritual-tagged
|
|
162
|
+
spells out-of-combat only; an in-combat ritual cast is rejected
|
|
163
|
+
(`CastFailedReason` gains `"ritual_in_combat"`). An **upcast** Magic
|
|
164
|
+
Missile previously over-rolled its single dart (`Nd4+1`, reading the
|
|
165
|
+
corpus's empty `scaling.mode` as whole-mode scaling); it now rolls
|
|
166
|
+
`1d4+1` once per cast and applies it to N darts, changing the RNG draw
|
|
167
|
+
count for that one path (base-level Magic Missile is unaffected). See
|
|
168
|
+
the migration guide for the full behavioural-delta list, including the
|
|
169
|
+
Magic Missile event-count change and the `build_party_member`
|
|
170
|
+
empty-pool fallback.
|
|
171
|
+
|
|
172
|
+
- **Monster action economy (C18).** A living monster's driven turn now
|
|
173
|
+
runs legendary-pool reset, Recharge rolls (`RechargeRolled`, gates a spent
|
|
174
|
+
recharge action until it succeeds) and Regeneration (unconditional — the
|
|
175
|
+
bundled corpus trait text carries no suppression clause) before action
|
|
176
|
+
selection; a dead monster runs none of them.
|
|
177
|
+
`activities/monster_actions.py::rank_monster_actions` ranks a live
|
|
178
|
+
recharge action first, a Spellcasting action whose N/Day offensive spell
|
|
179
|
+
still has a use left second (it casts that spell, not an at-will one),
|
|
180
|
+
`multiattack` third, then everything else (at-will spells included).
|
|
181
|
+
Stat-block monster spellcasting now selects and casts: the spell book
|
|
182
|
+
comes from the compendium uuids already in the data, the save DC and the
|
|
183
|
+
spell attack bonus use the monster's own `Combatant.spellcasting_ability`
|
|
184
|
+
(new dataset field `Monster.spellcasting_ability`) against its own
|
|
185
|
+
ability score + proficiency bonus, and `SpellCast` is now emitted on the
|
|
186
|
+
monster path too. Legendary Actions: `advance_monster_turn(handle, *,
|
|
187
|
+
legendary=True, actor_id=...)` spends one legendary action from a pool
|
|
188
|
+
that refills at the start of the creature's own turn
|
|
189
|
+
(`LegendaryActionUsed`; pool size from the new dataset field
|
|
190
|
+
`Monster.legendary_action_uses`, 3 for every bundled legendary monster).
|
|
191
|
+
Legendary Resistance: a per-day pool (never reset at turn start) sized
|
|
192
|
+
from the new dataset field `Monster.legendary_resistance_uses` (3, 4 or
|
|
193
|
+
6); a new top-level `resolve_legendary_resistance(handle, entity_id) ->
|
|
194
|
+
int` pre-arms a conversion of the entity's next failed save — every save
|
|
195
|
+
path in the engine honors it, including the Grapple/Shove Unarmed Strike
|
|
196
|
+
save and the damage-triggered concentration check — and emits
|
|
197
|
+
`LegendaryResistanceUsed` after the converted `SaveRolled`. Five more `MonsterTraitMechanic`
|
|
198
|
+
values are now consumed: Pack Tactics (attack advantage from an adjacent,
|
|
199
|
+
non-Incapacitated ally), Sunlight Sensitivity (attack disadvantage via
|
|
200
|
+
the new `GridScene.sunlight: bool = False` scene flag), Undead Fortitude
|
|
201
|
+
(a CON save to hold at 1 HP instead of dropping to 0, live-path
|
|
202
|
+
included), Swarm (no HP or temp HP gain) and Legendary Resistance itself.
|
|
203
|
+
Corpus damage resistances/immunities now hydrate from the monster
|
|
204
|
+
template when a host leaves both fields empty, unconditionally (no
|
|
205
|
+
"nonmagical attacks" qualifier, matching SRD 5.2 stat blocks). `Combatant.
|
|
206
|
+
has_fled: bool = False` persists a fleeing monster's stance across turns,
|
|
207
|
+
and `ended_reason`/`CombatEnded.reason` can now actually be `"flee"` when
|
|
208
|
+
every living foe has fled. See the migration guide for the full
|
|
209
|
+
determinism-affecting delta list — a seeded replay reaching a monster
|
|
210
|
+
turn with a recharge action, an unspent N/Day spell, a legendary action or
|
|
211
|
+
a newly-consumed trait diverges from that point on.
|
|
212
|
+
|
|
213
|
+
- **Class feature mechanics (C20).** The four SRD 5.2 Fighting Style feats
|
|
214
|
+
apply: Defense (+1 AC from `derive_sheet` while armor is worn), Archery (+2
|
|
215
|
+
to Ranged-weapon attack rolls, on top of a pinned `attack_bonus`), Great
|
|
216
|
+
Weapon Fighting (a 1 or 2 on a damage die counts as a 3, with no extra draw,
|
|
217
|
+
for a weapon held in two hands and so never beside a Shield) and Two-Weapon
|
|
218
|
+
Fighting (the Light off-hand swing keeps its ability modifier). Martial
|
|
219
|
+
Arts: an unarmored, shieldless monk's Unarmed Strike or
|
|
220
|
+
Monk weapon rolls the Martial Arts die when it beats the weapon's own, uses
|
|
221
|
+
DEX when higher (the Grapple and Shove DC too), and gets a Bonus Unarmed
|
|
222
|
+
Strike; Flurry of Blows spends 1 Focus Point for the next two Unarmed
|
|
223
|
+
Strikes (three with Heightened Focus). Action Surge grants one extra action
|
|
224
|
+
— never a Magic action, one surge per turn — counted on
|
|
225
|
+
`LiveCombatView.turn.extra_actions_remaining`, and a `special`-activation
|
|
226
|
+
feature that resolves nothing by itself (Action Surge, Sacred Weapon) costs
|
|
227
|
+
no Action. A Bardic Inspiration die is redeemed on a failed attack roll
|
|
228
|
+
(`PlayerIntent.redeem_granted_die`). Rage ends at the end of the
|
|
229
|
+
barbarian's next turn unless an attack roll against an enemy, an enemy's
|
|
230
|
+
saving throw or a Bonus Action (`use_feature rage` again, no use spent)
|
|
231
|
+
extends it, and it ends at once on the Incapacitated condition, however it
|
|
232
|
+
is applied; a Barbarian 15's Persistent Rage needs no extension and ends
|
|
233
|
+
early only on Unconscious. Lay on Hands' Heal draws
|
|
234
|
+
`PlayerIntent.pool_points` from a pool of five times the Paladin level.
|
|
235
|
+
Cunning Action's Bonus-Action Dash and Disengage read the feature at the
|
|
236
|
+
Rogue's own level (Rogue 2), not `class_slug`. Every corpus
|
|
237
|
+
`uses.max` (`@prof`, `max(1, @abilities.<ab>.mod)`,
|
|
238
|
+
`N * @classes.<class>.levels`) now caps its feature, and an invocation
|
|
239
|
+
spends its activity's `consumption.targets` cost.
|
|
240
|
+
`PartyMemberSpec.classes` carries a multiclass character's per-class levels
|
|
241
|
+
into combat: features, scale values and `@classes.<class>.levels` per class.
|
|
242
|
+
See the migration guide for every delta and the determinism notes.
|
|
243
|
+
|
|
244
|
+
- **Summons foundations (C21a).** Every concentration spell concentrates: one
|
|
245
|
+
that applies no concentration effect of its own (Spiritual Weapon, a Hold
|
|
246
|
+
Person every target saved against, a Polymorph whose save succeeded) leaves
|
|
247
|
+
a caster-held anchor effect, on a turn, a monster's cast or a readied cast.
|
|
248
|
+
Four SRD 5.2 sources on an allowlist resolve. Magic Weapon enchants the
|
|
249
|
+
weapon `PlayerIntent.weapon_id` names (+1/+2/+3 to hit and damage, magical,
|
|
250
|
+
on top of a pinned `attack_bonus`; a recast ends the old one; never an
|
|
251
|
+
Unarmed Strike). Spiritual Weapon places a caster-owned force that attacks
|
|
252
|
+
at once and, as a Bonus Action on later turns, moves up to 20 feet and
|
|
253
|
+
attacks again (`attack` with `spell_id`); it ends with its concentration,
|
|
254
|
+
and no monster casts it (BACKLOG). Wild Shape and Polymorph swap in the Beast
|
|
255
|
+
stat block `PlayerIntent.form_id` names, checked before anything is spent
|
|
256
|
+
(the Beast Shapes table; a Challenge Rating up to the target's), with
|
|
257
|
+
ordinary Temporary Hit Points — the Druid level, or the form's Hit Points.
|
|
258
|
+
Wild Shape ends on reuse, Incapacitated, death or a Bonus-Action leave;
|
|
259
|
+
Polymorph with its concentration, when its Temporary Hit Points are gone
|
|
260
|
+
(the rest of that hit landing on the creature) or with the combat (its grant
|
|
261
|
+
stays out of `CombatOutcome.residual_temp_hp`). A transformed creature can't
|
|
262
|
+
cast, ready or release a readied spell, or make a weapon attack; it attacks
|
|
263
|
+
with its form's actions through `PlayerIntent.stat_block_action_id` (or on
|
|
264
|
+
its own monster turn) at the form's real ability scores and Proficiency
|
|
265
|
+
Bonus, with the form's Multiattack count. See the migration guide for every
|
|
266
|
+
delta.
|
|
267
|
+
|
|
268
|
+
- **Roster summons (C21b).** Summon Dragon seats a Draconic Spirit in an
|
|
269
|
+
unoccupied space its caster can see within 60 feet (`target_zone_id`, else
|
|
270
|
+
the nearest free cell), in the initiative slot right after its caster at the
|
|
271
|
+
caster's count, with the spell's numbers: AC 14 + the slot level, HP 50 + 10
|
|
272
|
+
per slot level above 5, the caster's Proficiency Bonus, and half the slot
|
|
273
|
+
level (round down) of Rends per Attack action at the caster's spell attack
|
|
274
|
+
bonus for 1d6 + 4 + the slot level. `advance_monster_turn` has it take the
|
|
275
|
+
Dodge action when uncommanded (a pass when it can't act); a host commands
|
|
276
|
+
each Rend through `stat_block_action_id`, and any other attack, Grapple,
|
|
277
|
+
Shove or spell it is commanded is refused before anything is spent. Its
|
|
278
|
+
allegiance is its caster's, but it never enters `party_ids` or
|
|
279
|
+
`encounter_ids`, and `CombatOutcome` holds no Hit Points, XP or death of its
|
|
280
|
+
own, though a `DeathRecord.killer_id` can name it (resolve it through
|
|
281
|
+
`CombatantJoined.origin_caster_id`). It leaves at 0 Hit Points — no death,
|
|
282
|
+
no XP — or when its caster's concentration ends; if it was the current
|
|
283
|
+
actor, the turn passes on once the intent or legendary action that removed
|
|
284
|
+
it has resolved, and a turn it left during its own intent ends with
|
|
285
|
+
`TurnEnded`. See the migration guide for every delta.
|
|
286
|
+
|
|
287
|
+
### Added
|
|
288
|
+
|
|
289
|
+
- **`activities/actor_stats.py`** — the per-actor D20-test modifier projection
|
|
290
|
+
(SRD 5.2 §D20 Tests / §Proficiency). `save_modifier(combatant, ability)` and
|
|
291
|
+
`check_modifier(combatant, ability, skill=None)` return a typed
|
|
292
|
+
`D20Modifier(ability, ability_mod, proficiency, expertise, total)`;
|
|
293
|
+
`ability_modifier_of` / `proficiency_bonus_of` / `skill_ability` are the
|
|
294
|
+
supporting primitives.
|
|
295
|
+
- **`activities/d20.py`** — the single SRD 5.2 D20 Test primitive.
|
|
296
|
+
`roll_d20_test(rng, modifier, sources, *, forced_natural=None) -> D20Result`
|
|
297
|
+
with a typed `AdvantageSources(advantage, disadvantage)` and
|
|
298
|
+
`resolve_mode(...)` implementing the SRD's cancel-to-normal rule. Normal mode
|
|
299
|
+
still consumes exactly one RNG draw, so pre-0.6 seeded streams are unmoved.
|
|
300
|
+
- **`dnd5e_engine.AdvantageSource`** (new top-level export) — the closed
|
|
301
|
+
`Literal` naming *why* a roll had advantage/disadvantage (`flag`,
|
|
302
|
+
`condition:self`, `condition:target`, …), carried on the roll events.
|
|
303
|
+
- **Roll-breakdown fields on the roll events.** `AttackRolled`, `SaveRolled`,
|
|
304
|
+
`CheckRolled` and `ConcentrationCheck` gained `natural` (the die kept after
|
|
305
|
+
advantage/disadvantage), `modifier` (the flat bonus) and `sources`, plus
|
|
306
|
+
`advantage` on `SaveRolled` / `CheckRolled` / `ConcentrationCheck`. All
|
|
307
|
+
optional; hosts that ignore them are unaffected.
|
|
308
|
+
- **`TurnPhase(phase, actor_id, round_number)` event** — marks each
|
|
309
|
+
`round_start` / `turn_start` / `turn_end` boundary immediately before that
|
|
310
|
+
phase's hooks run. It is a marker event with no mechanical payload; renderers
|
|
311
|
+
should skip event types they do not recognise.
|
|
312
|
+
- **`turn_lifecycle.py`** — the `TurnLifecycle` hook registry
|
|
313
|
+
(`register` / `unregister` / `run`) that the single turn-advance path drives.
|
|
314
|
+
Every "at the start/end of a turn" rule registers here instead of being
|
|
315
|
+
open-coded into the advance path.
|
|
316
|
+
- **Timed effect expiry.** Effect durations in `seconds` (materialised as
|
|
317
|
+
`ceil(seconds / 6)` rounds), `turns`, and `flags["until_end_of_next_turn_of"]`
|
|
318
|
+
now expire, alongside the pre-existing `rounds` counter.
|
|
319
|
+
- **Proficiency fields on `PartyMemberSpec` and `Combatant`** —
|
|
320
|
+
`save_proficiencies`, `skill_proficiencies`, `skill_expertise`,
|
|
321
|
+
`weapon_proficiencies`, plus `Combatant.proficiency_bonus_override` (monsters
|
|
322
|
+
carry their template's CR-derived bonus). All default to empty / `None`, so a
|
|
323
|
+
host that sets none reproduces pre-0.6 behaviour exactly.
|
|
324
|
+
- **`test_capability_matrix.py::test_status_rows_match_code_probes`** pins
|
|
325
|
+
`docs/capabilities.md` status rows to grep-level code probes, in both
|
|
326
|
+
directions.
|
|
327
|
+
- **Conditions enforced (C12).** `rules/conditions.py` gains
|
|
328
|
+
`d20_test_penalty`, `exhaustion_level_of`, `project_speed`,
|
|
329
|
+
`conditions_block_actions`, `conditions_auto_crit_within_5ft`,
|
|
330
|
+
`SPEED_ZERO_CONDITIONS` and `AUTO_CRIT_WITHIN_5FT_CONDITIONS`;
|
|
331
|
+
`conditions_grant_advantage_on_attack` accepts keyword-only `distance_ft`,
|
|
332
|
+
`grappler_id` and `target_id` (the Prone and Grappled rows).
|
|
333
|
+
`SpatialTopology.distance_ft(a, b)` is a new Protocol method implemented by
|
|
334
|
+
both backends. `ActivityResolutionContext` gains `target_distance_ft`,
|
|
335
|
+
`attacker_grappler_id` and `d20_test_penalty`. New reasons on existing
|
|
336
|
+
closed sets: `IntentRejectedError` `"actor_incapacitated"`,
|
|
337
|
+
`MoveFailed.reason="speed_zero"`, `AttackFailed` / `CastFailed`
|
|
338
|
+
`"target_is_charmer"`. No new event class and no new top-level export.
|
|
339
|
+
- **Grid AoE targeting (C16).** `_expand_aoe_target_list` enumerates the typed
|
|
340
|
+
template (`sphere` / `cone` / `line` / `cube` / `cylinder`; Foundry `radius`
|
|
341
|
+
= emanation, `circle`, `square` mapped) via `GridTopology.cells_in_template`
|
|
342
|
+
and drops cells without line of effect from the point of origin (SRD 5.2
|
|
343
|
+
§Point of Origin). New `PlayerIntent.direction: tuple[int, int] | None`
|
|
344
|
+
aims cones, lines and cubes (defaults to caster → named target).
|
|
345
|
+
- **Creature cover.** `GridTopology.cover_between(a, b, occupied_cells=())`
|
|
346
|
+
— every other live combatant on the line grants half cover; blocked cells
|
|
347
|
+
grant total cover and block sight (`has_line_of_sight`). A `cover_cells` tag
|
|
348
|
+
on the **target's own** cell now counts (the origin's still never does); a
|
|
349
|
+
creature on a `"total"` cover cell is therefore untargetable.
|
|
350
|
+
- **AoE cover is measured from the point of origin** (SRD 5.2 §Cover), not from
|
|
351
|
+
the caster: the AoE cast path threads the resolved template origin
|
|
352
|
+
(`_aoe_cover_origin`) into the per-target cover map.
|
|
353
|
+
- **Multi-cell `move` intent — grid backend only.** `_handle_move` paths with
|
|
354
|
+
`GridTopology.shortest_path` (walls block, no diagonal corner-cutting,
|
|
355
|
+
enemies impassable, allies passable, no ending in an occupied cell) and
|
|
356
|
+
charges each leg's `edge_distance`; new `MoveFailed.reason` members
|
|
357
|
+
`unreachable`, `occupied`, `blocked_path`. The legacy zone graph keeps its
|
|
358
|
+
pre-0.6 single-adjacent-step contract, `not_adjacent` rejection included.
|
|
359
|
+
- **Forced movement.** `orchestrator.push_combatant(live, target_id,
|
|
360
|
+
origin_cell, distance_ft)` and the `CombatantMoved(forced=True)` event;
|
|
361
|
+
`activities/forced_movement.py` wires Thunderwave's push.
|
|
362
|
+
- **Vision & light (C16b).** `GridScene.lighting`, `default_lighting`,
|
|
363
|
+
`obscurement_cells` (`LightLevel` / `Obscurement` Literals) and
|
|
364
|
+
`GridTopology.can_see(a, b, senses)`; `ActivityResolutionContext.target_unseen`
|
|
365
|
+
/ `attacker_unseen_by` feed the `unseen` `AdvantageSource` both directions.
|
|
366
|
+
- **Vision & light consumers (C16b).** A new composite predicate,
|
|
367
|
+
`orchestrator.py::_combatant_can_see(live, viewer, target)`, folds Blinded
|
|
368
|
+
(viewer; only blindsight in range sees through it) and Invisible (target;
|
|
369
|
+
blindsight or truesight in range, with line of sight, sees it) — never
|
|
370
|
+
darkvision — on top of `GridTopology.can_see`.
|
|
371
|
+
It now backs every SRD 5.2 "can see" conjunct outside the raw `unseen`
|
|
372
|
+
attack-roll row: Dodge's "if you can see the attacker" attack-disadvantage
|
|
373
|
+
half (both the regular-attack context sites and opportunity attacks),
|
|
374
|
+
Ranged Attacks in Close Combat's "enemy who can see you", the Opportunity
|
|
375
|
+
Attack "creature that you can see" trigger (both PC↔monster directions — a
|
|
376
|
+
sight-blocked reactor spends no Reaction), Hide's "out of any enemy's line
|
|
377
|
+
of sight" gate (skipped only when the hider's own cell already carries
|
|
378
|
+
Three-Quarters/Total cover), and Frightened's line-of-sight gate. The
|
|
379
|
+
Invisible "can somehow see you" carve-out is threaded through
|
|
380
|
+
`rules/conditions.py::conditions_grant_advantage_on_attack`'s new
|
|
381
|
+
keyword-only `attacker_invisibility_pierced` / `target_invisibility_pierced`
|
|
382
|
+
parameters (default `False`, preserving pre-C16b behaviour); Frightened's
|
|
383
|
+
attack-roll disadvantage gets a matching `fear_source_in_sight` keyword.
|
|
384
|
+
New `SpatialTopology.light_on_cell(cell) -> LightLevel` reports per-cell
|
|
385
|
+
light, so a dark cell now also satisfies Hide's "Heavily Obscured" gate
|
|
386
|
+
(SRD 5.2 glossary: "An area of darkness is Heavily Obscured"). New
|
|
387
|
+
`MoveFailed.reason` member `"frightened"`: a `"move"` intent that would
|
|
388
|
+
bring the mover strictly closer to a visible, known, living fear source is
|
|
389
|
+
now rejected (SRD 5.2 "You can't willingly move closer to the source of
|
|
390
|
+
fear"). `AttackRolled` gains additive `advantage_sources` /
|
|
391
|
+
`disadvantage_sources: list[AdvantageSource]` fields — the directional
|
|
392
|
+
split of `sources` — so a mutual-unseen swing now reports `["unseen"]` on
|
|
393
|
+
each list instead of the old `sources = ["unseen", "unseen"]` merge
|
|
394
|
+
(`sources` itself is unchanged).
|
|
395
|
+
- `SaveBlock.ignore_cover` is honoured when present (`roll_save(...,
|
|
396
|
+
ignore_cover=)`); the dataset field itself ships with C22.
|
|
397
|
+
- **C22 dataset consumers.** `Combatant.trait_mechanics` (hydrated from the
|
|
398
|
+
monster template's typed `special_abilities[].mechanic`): **Magic
|
|
399
|
+
Resistance** now rolls saving throws against spells with Advantage
|
|
400
|
+
(`AdvantageSource` gains `"trait"`). `roll_save(..., ignore_cover=)` +
|
|
401
|
+
`SaveBlock.ignore_cover` — Sacred Flame's target gets no cover bonus.
|
|
402
|
+
`apply_damage(..., magical=)` — a magic weapon's (`Weapon.magical`) or a
|
|
403
|
+
spell's Bludgeoning/Piercing/Slashing damage overcomes resistance to
|
|
404
|
+
*nonmagical* B/P/S; `Combatant.physical_resistances_nonmagical_only`
|
|
405
|
+
(also on `EncounterMemberSpec`, default `True`) makes the qualifier explicit.
|
|
406
|
+
Save-path target cover is hydrated for templated spell casts; when an AoE's
|
|
407
|
+
origin cell IS the target's cell, the new `GridTopology.cover_on_cell` reads
|
|
408
|
+
the tag on that cell (`cover_between` itself is unchanged — its target-cell
|
|
409
|
+
walk shipped with C16). Multiattacks for the five opaque-key monsters
|
|
410
|
+
resolve to their exact attack mix.
|
|
411
|
+
- **`dnd5e_engine.spellcasting` (C17)** — pure SRD 5.2 spell-slot tables and
|
|
412
|
+
derivations, zero I/O. `derive_spell_slots(class_slug, progression,
|
|
413
|
+
level)`, `derive_pact_slots(level)`, `multiclass_caster_level(classes)`,
|
|
414
|
+
`slots_for_caster_level(caster_level)`, `effective_caster_level(
|
|
415
|
+
progression, level)`, `resolve_target_count(count_formula, *,
|
|
416
|
+
cast_level=)` (a restricted `ast`-walker roll-data evaluator — never
|
|
417
|
+
`eval`), `count_scales_with_cast_level(count_formula)`,
|
|
418
|
+
`spell_component_metadata(spell)`, `resolve_ritual_cast(spell, *,
|
|
419
|
+
prepared, ritual_adept=False) -> RitualCast`, and the `SPELL_SLOT_TABLE`
|
|
420
|
+
/ `PACT_SLOT_TABLE` constants. `derive_spell_slots`, `derive_multiclass_
|
|
421
|
+
slots`, `derive_pact_slots`, `resolve_ritual_cast` and `RitualCast` are
|
|
422
|
+
new top-level `dnd5e_engine` exports.
|
|
423
|
+
- **`build_spec.derive_multiclass_slots` / `derive_multiclass_pact_slots`**
|
|
424
|
+
— project `CharacterBuildSpec.classes` through a `loader` (explicit or the
|
|
425
|
+
lazy default `get_lib_loader()`) into a Spellcasting/Pact Magic pool;
|
|
426
|
+
`build_party_member` calls both to fill an empty `CombatInstance.
|
|
427
|
+
spell_slots`/`pact_slots`.
|
|
428
|
+
- **`PartyMemberSpec.pact_slots` / `CombatInstance.pact_slots` /
|
|
429
|
+
`LiveCombatView.pact_slots_by_entity`** — the Pact Magic pool, all
|
|
430
|
+
defaulting to `{}` and mirroring the existing `spell_slots` shape.
|
|
431
|
+
- **`PlayerIntent.target_ids: tuple[str, ...] | None`** — explicit
|
|
432
|
+
multi-target aiming for a count-scaled cast (Magic Missile darts);
|
|
433
|
+
`PlayerIntent.as_ritual: bool = False` — request a Ritual cast (rejected
|
|
434
|
+
in-combat).
|
|
435
|
+
- **`resolve_short_rest(..., *, pact_slots=None, pact_slot_max=None)`** and
|
|
436
|
+
**`resolve_long_rest(..., *, spell_slots=None, spell_slot_max=None,
|
|
437
|
+
pact_slots=None, pact_slot_max=None, exhaustion_level=None)`** — all
|
|
438
|
+
keyword-only, all default `None`. `RestOutcome` gains `spell_slots`,
|
|
439
|
+
`pact_slots`, `exhaustion_level`, each populated only when its matching
|
|
440
|
+
input pair was supplied.
|
|
441
|
+
- **`SpellCast` event** — `actor_id`, `spell_id`, `slot_level`, `ritual`,
|
|
442
|
+
`components`, `material`, `material_consumed`, `material_cost_gp`.
|
|
443
|
+
Emitted after the slot gate on every PC cast path (on-turn, readied
|
|
444
|
+
reaction, Counterspell). `CastFailedReason` gains `"ritual_in_combat"`.
|
|
445
|
+
- **Monster action economy surface (C18).** New events: `RechargeRolled
|
|
446
|
+
(monster_id, action_slug, roll, threshold, succeeded)`,
|
|
447
|
+
`LegendaryActionUsed(actor_id, action_slug, uses_remaining)`,
|
|
448
|
+
`LegendaryResistanceUsed(actor_id, uses_remaining)`. New top-level
|
|
449
|
+
function: `resolve_legendary_resistance(handle, entity_id) -> int`.
|
|
450
|
+
`advance_monster_turn` gains keyword-only `legendary: bool = False`,
|
|
451
|
+
`actor_id: str | None = None`. New `types.combat.MonsterActionUses`
|
|
452
|
+
dataclass (`recharge_spent`, `uses_remaining`). `Combatant` gains
|
|
453
|
+
`legendary_actions_max` / `legendary_actions_remaining` /
|
|
454
|
+
`legendary_resistances_max` / `legendary_resistances_remaining` /
|
|
455
|
+
`has_fled: bool = False` / `spellcasting_ability: str | None = None`.
|
|
456
|
+
`LiveCombatView` gains `monster_action_uses_by_entity`,
|
|
457
|
+
`legendary_actions_by_entity`, `legendary_resistances_by_entity` (a new
|
|
458
|
+
`views.MonsterActionUsesView` projects the first). `GridScene.sunlight:
|
|
459
|
+
bool = False` — a whole-scene Sunlight Sensitivity flag. All additive
|
|
460
|
+
with defaults reproducing pre-0.6 behaviour.
|
|
461
|
+
- **Class feature surface (C20).** `PlayerIntent.pool_points: int | None`,
|
|
462
|
+
`PlayerIntent.redeem_granted_die: GrantedDie | None`;
|
|
463
|
+
`PartyMemberSpec.classes: dict[str, int]`, `.feats: tuple[str, ...]`,
|
|
464
|
+
`.fighting_style: FightingStyle | None`; `Combatant.classes`,
|
|
465
|
+
`.fighting_styles`, `.worn_armor`, `.shield_equipped`,
|
|
466
|
+
`.flurry_strikes_remaining`, `.extra_actions_remaining`,
|
|
467
|
+
`.action_surge_used_this_turn`; `TurnCombatView.extra_actions_remaining`;
|
|
468
|
+
`types.combat.FightingStyle` / `WornArmor`;
|
|
469
|
+
`ActivityResolutionContext.scaling_value` / `martial_arts` / `granted_die` /
|
|
470
|
+
`granted_die_rolls` and the matching `build_activity_context` keywords;
|
|
471
|
+
`build_scale_values(classes=)`; `roll_damage_part(die_floor=)`; the pure
|
|
472
|
+
`rules/uses.py` (`UsesRollData`, `evaluate_uses_formula`).
|
|
473
|
+
`EffectExpiryReason` gains `"expended"`, `"not_extended"` and
|
|
474
|
+
`"incapacitated"`; `AttackFailed.reason` gains `"no_granted_die"`. All
|
|
475
|
+
additive, with defaults that reproduce the pre-C20 behaviour.
|
|
476
|
+
- **Summons foundations surface (C21a).** `PlayerIntent.form_id: str | None`,
|
|
477
|
+
`PlayerIntent.stat_block_action_id: str | None`; `views.ConstructView`,
|
|
478
|
+
`views.TransformView`, `LiveCombatView.constructs` / `.transformations`; the
|
|
479
|
+
pure `activities/conjuration.py` (`CONJURATION_ALLOWLIST`, `ConjurationKind`,
|
|
480
|
+
`TransformSource`, `ConjurationCarrier`, `ConstructRequest`,
|
|
481
|
+
`TransformRequest`, `ENCHANTED_WEAPON_FLAG`, `TRANSFORM_FORM_FLAG`,
|
|
482
|
+
`enchant_weapon`, `ConstructSpec`, `CONSTRUCTS`, `construct_attack_activity`,
|
|
483
|
+
`StatBlockMagnitudes`, `WildShapeTier`, `WILD_SHAPE_TIERS`,
|
|
484
|
+
`wild_shape_tier`, `uses_summon_roll_data`, `TransformRider`,
|
|
485
|
+
`TRANSFORM_RIDERS`); `ActivityResolutionContext.conjuration` /
|
|
486
|
+
`construct_requests` / `transform_requests` / `weapon_enchantment_to_hit` /
|
|
487
|
+
`stat_block_magnitudes` and the matching `build_activity_context` keywords;
|
|
488
|
+
`extra_flags=` on `apply_activity_effects` and
|
|
489
|
+
`passive_effect_to_active_effect`; `build_context.spell_attack_magnitudes`;
|
|
490
|
+
`monster_actions.multiattack_count`. `CastFailedReason` gains
|
|
491
|
+
`"invalid_form"` and `"no_spellcasting"`, `AttackFailed.reason`
|
|
492
|
+
`"action_unavailable"`, `EffectExpiryReason` `"temp_hp_depleted"`. All
|
|
493
|
+
additive, with defaults that reproduce the pre-C21a behaviour; no event type
|
|
494
|
+
is added and `dnd5e_engine.__all__` is unchanged.
|
|
495
|
+
- **Roster summons surface (C21b).** `events.CombatantJoined(entity_id, name,
|
|
496
|
+
stat_block_slug, origin_caster_id, spell_id, initiative_count,
|
|
497
|
+
after_entity_id, zone_id, hp_max, ac)`, `events.CombatantLeft(entity_id,
|
|
498
|
+
reason)` and `events.CombatantLeftReason` (`"concentration_drop"`,
|
|
499
|
+
`"spell_ended"`, `"zero_hp"`); both classes join the `CombatEvent` union and
|
|
500
|
+
`ALL_COMBAT_EVENT_TYPES`. `views.SummonView`, `LiveCombatView.summons`. In
|
|
501
|
+
`activities/conjuration.py`: `SummonSpec`, `SUMMONS`, `SummonRollData`,
|
|
502
|
+
`evaluate_summon_formula`, `summon_attack_count`, `summon_magnitudes`,
|
|
503
|
+
`SummonRequest`, the `"summon"` member of `ConjurationKind`, and
|
|
504
|
+
`StatBlockMagnitudes.attack_bonus` / `.attack_damage_bonus`;
|
|
505
|
+
`ActivityResolutionContext.summon_requests`. All additive, with defaults
|
|
506
|
+
that reproduce the pre-C21b behaviour; `dnd5e_engine.__all__` is unchanged.
|
|
507
|
+
|
|
508
|
+
### Changed
|
|
509
|
+
|
|
510
|
+
- **Saving throws apply real modifiers.** All three save paths (activity saves,
|
|
511
|
+
the damage-triggered concentration check, the end-of-turn repeat save) now add
|
|
512
|
+
`ability modifier + proficiency bonus (if proficient)` for all six abilities,
|
|
513
|
+
where v0.5 projected DEX only and the two orchestrator paths rolled a raw d20.
|
|
514
|
+
Monster ability scores, save/skill proficiencies and proficiency bonus hydrate
|
|
515
|
+
from `monster_template_slug`.
|
|
516
|
+
- **Ability checks apply the actor projection** and fold the
|
|
517
|
+
`abilities.check` / `abilities.skill` / `abilities.<ab>.save` (and Foundry-native
|
|
518
|
+
`system.bonuses.abilities.*`) effect-change families, where
|
|
519
|
+
`build_activity_context` previously hard-coded `check_modifiers={}`.
|
|
520
|
+
- **Activity attack rolls honour advantage/disadvantage**: attacker
|
|
521
|
+
`flags.advantage.attack` / `flags.disadvantage.attack` effects plus the
|
|
522
|
+
condition-derived sources, cancelling per SRD §Advantage and Disadvantage.
|
|
523
|
+
Opportunity attacks still roll flat `normal`.
|
|
524
|
+
- **Every d20 in the engine goes through `roll_d20_test`** — attacks, saves,
|
|
525
|
+
ability/skill checks, death saves and the concentration check.
|
|
526
|
+
- **`ConcentrationCheck` is now emitted** for the damage-triggered concentration
|
|
527
|
+
save. *Transitional:* the legacy `SaveRolled(ability="con")` for the same roll
|
|
528
|
+
is emitted alongside it until v0.7; a host that counts saves must filter one
|
|
529
|
+
of the pair out.
|
|
530
|
+
- **`D20Result.first`** is the first die drawn, deliberately not named
|
|
531
|
+
`natural` — `AttackRolled.natural` is the *kept* die (the one the natural-20
|
|
532
|
+
crit / natural-1 fumble test reads).
|
|
533
|
+
- Turn advancement runs through one shared path, so every boundary rule fires
|
|
534
|
+
in a single, registration-ordered place.
|
|
535
|
+
- **The SRD 5.2 conditions now have teeth (C12)** — the Incapacitated
|
|
536
|
+
action/bonus/reaction gate, Speed 0, exhaustion's `-2 × level` on every D20
|
|
537
|
+
Test (death saves included) and `-5 ft × level` Speed, the Prone and Grappled
|
|
538
|
+
attack rows, auto-crit within 5 ft of a Paralyzed/Unconscious target, the
|
|
539
|
+
Charmed target restriction (on the monster turn path as well as the player's
|
|
540
|
+
— a charmed monster will not select its charmer), and Unconscious at 0 HP —
|
|
541
|
+
including a Character *hydrated* into combat already at 0 HP — with
|
|
542
|
+
massive-damage instant death plus one death-save failure per damaging hit
|
|
543
|
+
while down. Four SRD condition rows remain unenforced and the capability row
|
|
544
|
+
stays `⚠️ Partial`; see `docs/capabilities.md` and `BACKLOG.md`.
|
|
545
|
+
- **`conditions_grant_disadvantage_on_ability_checks` /
|
|
546
|
+
`project_passive_check_modifiers` changed their answer for two slugs**
|
|
547
|
+
(both PyPI-live): `exhaustion` no longer reports disadvantage (SRD 5.2
|
|
548
|
+
replaced the 2014 rule with the numeric penalty), and `frightened` now does.
|
|
549
|
+
See the migration guide.
|
|
550
|
+
- **Condition immunity is honoured by the state folds, not only the event.**
|
|
551
|
+
A status on a creature listed in `Combatant.condition_immunities` no longer
|
|
552
|
+
lands on `Combatant.conditions` or `active_conditions` — neither through the
|
|
553
|
+
`start_combat` effect seed nor the runtime `EffectApplied` fold, which
|
|
554
|
+
previously unioned it in behind the already-suppressed `ConditionApplied`.
|
|
555
|
+
Before C12 that was near-cosmetic; with the gates above it decided whether an
|
|
556
|
+
immune creature could act at all.
|
|
557
|
+
- **`ConditionApplied` / `ConditionRemoved` now materialise on
|
|
558
|
+
`Combatant.conditions`**, so a condition applied mid-combat reaches every
|
|
559
|
+
condition consumer (a Topple-proned target is attacked at advantage in
|
|
560
|
+
melee). Reviving from 0 HP leaves the creature Prone: `ConditionRemoved`
|
|
561
|
+
(`unconscious`) followed by `ConditionApplied` (`prone`).
|
|
562
|
+
- **The two orchestrator-level save paths** (the concentration check and the
|
|
563
|
+
end-of-turn repeat save) now honour the condition projections: STR/DEX
|
|
564
|
+
auto-fail, Restrained DEX disadvantage and the exhaustion penalty.
|
|
565
|
+
Effect-derived `passive_save_bonus` (Bless/Bane) on these two paths is
|
|
566
|
+
still unenforced; see `BACKLOG.md`.
|
|
567
|
+
- Walls now block movement as well as sight; `edge_distance` returns `None` for
|
|
568
|
+
an illegal step. On the grid, `ActorMoved` carries the whole intent's distance
|
|
569
|
+
(one event per `move`); the zone backend still emits one per step.
|
|
570
|
+
- **`start_combat(grid_scene=...)` raises `ValueError`** when a combatant's
|
|
571
|
+
`zone_id` is out of bounds or blocked, instead of silently seating them on an
|
|
572
|
+
unusable cell.
|
|
573
|
+
- **A main-hand attack keeps the turn (R1, C14)** when `attacks_remaining > 0`
|
|
574
|
+
after the swing, or when a two-weapon-fighting off-hand window is still
|
|
575
|
+
open; the turn ends only once neither condition holds. A subsequent swing
|
|
576
|
+
in the same Attack action no longer re-pays the Action (R2): only the
|
|
577
|
+
first swing hard-gates on `action_available`, and an exhausted
|
|
578
|
+
`attacks_remaining` on a later swing is a turn-KEEPING `AttackFailed`
|
|
579
|
+
rather than the "no Action" rejection every other Action-costed intent
|
|
580
|
+
raises. `AttackFailed` / `CheckRolled` (Hide, `escape_grapple`) /
|
|
581
|
+
`SaveRolled` (Grapple, Shove) / `CombatantMoved` (Shove's push option) are
|
|
582
|
+
new observable events on these paths; see the migration guide for the
|
|
583
|
+
exact keep-turn predicate and draw discipline.
|
|
584
|
+
- **A monster with no `monster_template_slug` now attacks.** The legacy
|
|
585
|
+
evaluator that used to swing template-less monsters was retired without a
|
|
586
|
+
replacement, so they silently passed every turn; `_synthesize_attack_from_legacy_fields`
|
|
587
|
+
now builds one typed `AttackActivity` per turn from the spec-level
|
|
588
|
+
`attack_bonus` / `damage_dice` / `damage_type` fields (unparseable dice
|
|
589
|
+
still no-op). Hosts with template-less monster party members will see them
|
|
590
|
+
start attacking; seeded streams containing a template-less monster's turn
|
|
591
|
+
move.
|
|
592
|
+
- **Weapon proficiency gates the Proficiency Bonus on an attack roll (C15).**
|
|
593
|
+
`Combatant.weapon_proficiencies: list[str] | None` — `None` (the field
|
|
594
|
+
never explicitly set on `PartyMemberSpec`) assumes proficient with every
|
|
595
|
+
weapon; an explicit list, even empty, is enforced by weapon category or
|
|
596
|
+
slug. Monsters are always proficient (never carry the field explicitly).
|
|
597
|
+
- **Attack range widens to three tiers (C15).** `_weapon_attack_range_ft`
|
|
598
|
+
returns `(normal, max)`; a shot between `normal` and `max` now resolves at
|
|
599
|
+
disadvantage (`"range:long"`) instead of the pre-C15 binary in-range/
|
|
600
|
+
rejected split, and a melee weapon carrying the Thrown property can attack
|
|
601
|
+
beyond its melee reach using its thrown bands instead of being rejected.
|
|
602
|
+
- **Ranged Attacks in Close Combat (C15).** A ranged (or effectively-ranged
|
|
603
|
+
thrown) attack rolls with disadvantage (`"ranged_in_melee"`) when a living,
|
|
604
|
+
sighted (`SpatialTopology.can_see`), non-Incapacitated hostile is within 5
|
|
605
|
+
ft of the attacker.
|
|
606
|
+
- **Heavy-property disadvantage (C15).** A wielder with a raw Strength score
|
|
607
|
+
below 13 rolls a Heavy weapon's attack with disadvantage.
|
|
608
|
+
- **Versatile grip (C15).** `PlayerIntent.two_handed: bool = False` — when
|
|
609
|
+
`True` and the weapon carries `versatile_damage`, a melee swing uses the
|
|
610
|
+
two-handed damage die instead of the one-handed die.
|
|
611
|
+
- **Damage attribution and crit tracking (C15).** `DamageApplied.source_id:
|
|
612
|
+
str | None` (weapon slug / synthesized activity id / `"mastery:<slug>"`)
|
|
613
|
+
and `DamageApplied.is_crit: bool = False`. A critical hit against a target
|
|
614
|
+
already making death saves counts as two failures instead of one (SRD
|
|
615
|
+
§Damage at 0 Hit Points).
|
|
616
|
+
- **Loading property (C15).** `Combatant.loading_weapon_fired_this_turn:
|
|
617
|
+
bool = False`, reset at the actor's own turn start. A second Loading-weapon
|
|
618
|
+
shot in the same turn is rejected pre-resolution with
|
|
619
|
+
`AttackFailed(reason="weapon_already_fired")`, gated ahead of the Charmed
|
|
620
|
+
target gate.
|
|
621
|
+
- **All eight 2024 weapon masteries (C15).** `activities/mastery.py`
|
|
622
|
+
resolves Graze (flat governing-ability-mod damage on a miss) and Topple
|
|
623
|
+
(Constitution save vs. `8 + proficiency + governing-ability mod`, prone on
|
|
624
|
+
a failure, gated by the shared `is_condition_immune` helper) directly.
|
|
625
|
+
Vex, Sap, Slow, Push, Cleave and Nick report through
|
|
626
|
+
`ActivityResolutionContext.mastery_procs`, which the orchestrator folds
|
|
627
|
+
into live combat state: Vex grants Advantage against the same target for
|
|
628
|
+
the attacker's next 2 attack-roll turns (one-use); Sap marks the target
|
|
629
|
+
with Disadvantage until the attacker's next turn start (one-use); Slow
|
|
630
|
+
applies a flat, non-stacking −10 ft Speed penalty cleared at the source's
|
|
631
|
+
next turn start; Push forces a full 10 ft move via `push_combatant`
|
|
632
|
+
(controller ruling: always the full distance; the "Large or smaller" size
|
|
633
|
+
gate is unmodelled); Cleave chains one extra attack+damage roll against the
|
|
634
|
+
nearest eligible living hostile within 5 ft of the first target and within
|
|
635
|
+
reach (deterministic tie-break by `entity_id`, once per turn, no re-proc);
|
|
636
|
+
Nick's off-hand extra attack spends no Bonus Action and does not require
|
|
637
|
+
one — `_offhand_window_open` now keeps the turn open after any Light
|
|
638
|
+
main-hand swing even when the Bonus Action is already spent, so a host
|
|
639
|
+
must submit `"pass"` to end such a turn (see the migration guide).
|
|
640
|
+
- **`test_capability_matrix.py`** gains six probes pinning the C15 rows in
|
|
641
|
+
`docs/capabilities.md` (attack rolls, conditions, forced movement, weapon
|
|
642
|
+
mastery).
|
|
643
|
+
- **A count-scaled cast now emits N `DamageApplied`, not one (C17).** A
|
|
644
|
+
`damage`-kind activity whose `target.affects.count` formula references
|
|
645
|
+
`@item.level` (Magic Missile) resolves N separate `DamageApplied` events
|
|
646
|
+
sharing one rolled damage instance — 3 darts at slot level 1, +1 per slot
|
|
647
|
+
level above 1. Total damage now scales ×N for every such cast; the draw
|
|
648
|
+
count is unchanged (shared roll, applied N times). Targeting more
|
|
649
|
+
entities than the resolved count, or an unknown `target_ids` entry, is
|
|
650
|
+
rejected pre-slot with `CastFailed(reason="target_invalid")`.
|
|
651
|
+
- **An armed Counterspell/readied-cast reaction with no slot at its readied
|
|
652
|
+
level is now skipped, not fired (C17).** `_pop_pending_reaction` gained
|
|
653
|
+
an `eligible=` predicate; an ineligible reactor's reaction stays queued
|
|
654
|
+
and its Reaction/slot are untouched. Counterspell is additionally gated
|
|
655
|
+
on its own 60 ft range with line of sight (`_in_range_with_los`) — an
|
|
656
|
+
out-of-range or LoS-blocked reactor's Counterspell is likewise skipped.
|
|
657
|
+
- **`build_party_member` fills an empty `spell_slots`/`pact_slots` from
|
|
658
|
+
`CharacterBuildSpec.classes` (C17).** Previously an empty
|
|
659
|
+
`CombatInstance.spell_slots` was copied verbatim (a caster with no
|
|
660
|
+
slots); now an empty (falsy) pool falls back to `derive_multiclass_slots`
|
|
661
|
+
/ `derive_multiclass_pact_slots`. A host that relied on an empty
|
|
662
|
+
`spell_slots` dict to make every cast fail with `CastFailed(reason=
|
|
663
|
+
"no_slot")` will now see real, derived slots instead. A non-empty pool
|
|
664
|
+
is untouched.
|
|
665
|
+
- **`CharacterBuildSpec.classes: dict[str, int]`** is the multiclass
|
|
666
|
+
carrier (C17), reconciled with the single-class `class_slug`/`level`
|
|
667
|
+
fields by a `model_validator(mode="before")`; `model_copy(update=...)`
|
|
668
|
+
bypasses that validator (Pydantic does not re-run `mode="before"` on
|
|
669
|
+
`model_copy`), so changing only one of `level`/`classes` via `model_copy`
|
|
670
|
+
can desync them — construct a fresh instance instead.
|
|
671
|
+
- **Pack Tactics applies to a host-driven attacker (C21a).** SRD 5.2: "The
|
|
672
|
+
tough has Advantage on an attack roll against a creature if at least one of
|
|
673
|
+
the tough's allies is within 5 feet of the creature and the ally doesn't
|
|
674
|
+
have the Incapacitated condition." The trait resolved only on a monster's
|
|
675
|
+
own `advance_monster_turn`; an attack through `submit_player_intent` by a
|
|
676
|
+
creature carrying it (a template NPC or monster the host drives: a Tough, a
|
|
677
|
+
Hobgoblin Warrior, a Kobold Warrior, a Wolf) now rolls with Advantage too,
|
|
678
|
+
one more d20, whenever such an ally stands within 5 feet of the target. A
|
|
679
|
+
seeded stream that has one moves from that roll on.
|
|
680
|
+
- **Summon Dragon seats a creature, or is refused before anything is spent
|
|
681
|
+
(C21b).** The cast used to spend its slot and Action and anchor its
|
|
682
|
+
concentration, with nothing else. With no legal space it is now refused with
|
|
683
|
+
`CastFailed` — `target_invalid` for a named cell that is off the map,
|
|
684
|
+
blocked, occupied or not the grid's own `col,row` id, `out_of_range` for one
|
|
685
|
+
beyond range or out of sight, when no cell in range is free, or when the
|
|
686
|
+
caster is Blinded (unless its Blindsight reaches the space) —
|
|
687
|
+
and a `ready` naming it with `target_invalid`.
|
|
688
|
+
- **Allegiance reads a summon's caster (C21b).** The monster AI picks its
|
|
689
|
+
lowest-HP target among every enemy (`_is_enemy`), a summon included, instead
|
|
690
|
+
of among `party_ids`; Help, Cleave, Sneak Attack and Pack Tactics adjacency,
|
|
691
|
+
the ranged attack's close-combat check, Hide and grid movement treat a
|
|
692
|
+
summon as its caster's ally. For a roster without a summon each reads as
|
|
693
|
+
before.
|
|
694
|
+
- **Monster turn-start mechanics are keyed on the creature (C21b).** The
|
|
695
|
+
legendary action reset, recharge rolls and regeneration run once per round
|
|
696
|
+
and creature rather than per round and turn index, so a removal mid-round
|
|
697
|
+
neither skips nor repeats them. Unchanged while the roster never changes.
|
|
698
|
+
- **`ActiveEffect.statuses` serializes sorted (C23).** The field stays a
|
|
699
|
+
`set[str]`; every dump mode emits a sorted list, so a multi-status
|
|
700
|
+
effect's JSON no longer follows the process's hash seed.
|
|
701
|
+
|
|
702
|
+
### Fixed
|
|
703
|
+
|
|
704
|
+
- **A `PartyMemberSpec` that never set `attack_bonus` was pinned to a 0
|
|
705
|
+
to-hit bonus (2026-09-02, C15 Task 1).** `Combatant.attack_bonus` widens
|
|
706
|
+
from `int = 0` to `int | None = None`; `None` (the host never explicitly
|
|
707
|
+
set `PartyMemberSpec.attack_bonus`) now correctly falls through to the
|
|
708
|
+
real governing-ability-modifier + proficiency-bonus computation instead of
|
|
709
|
+
being silently treated as a genuine `0` override. A host-supplied value
|
|
710
|
+
(including every monster's, always threaded as a concrete int) is
|
|
711
|
+
unaffected — byte-identical to every pre-C15 fixture. The engine's own
|
|
712
|
+
`build_party.py` host-party-building path always sets `attack_bonus`
|
|
713
|
+
explicitly (from the pre-computed character sheet), so this fix does not
|
|
714
|
+
change its output — pre-existing behaviour there, unchanged.
|
|
715
|
+
- **Lay on Hands' Heal no longer crashes (C20).** Its `@scaling` healing
|
|
716
|
+
formula raised `ValueError: Unhandled roll-data token` out of
|
|
717
|
+
`submit_player_intent` after the Bonus Action was spent; `@scaling` now
|
|
718
|
+
resolves to the points drawn.
|
|
719
|
+
- **The free Patient Defense no longer spends a Focus Point (C20).** A feature
|
|
720
|
+
activity is charged its own `consumption.targets` cost, and one that declares
|
|
721
|
+
none is free when a sibling activity of the same feature declares one.
|
|
722
|
+
- **An Incapacitated condition an effect imposes now breaks concentration and
|
|
723
|
+
releases grapples (C20).** Hold Person's Paralyzed, or any effect status that
|
|
724
|
+
is Incapacitated or implies it, left the creature concentrating and
|
|
725
|
+
grappling: the effect's fold wrote the condition onto the combatant first,
|
|
726
|
+
so the `ConditionApplied` fold that runs those consequences found it already
|
|
727
|
+
there and skipped them. Whichever fold writes the condition first now runs
|
|
728
|
+
them once, Rage's end included; for an effect, their events follow its
|
|
729
|
+
`EffectApplied`.
|
|
730
|
+
- **A readied concentration spell now concentrates (C21a).** A readied cast
|
|
731
|
+
ran none of the concentration bookkeeping, so it never started
|
|
732
|
+
concentration, never ended an earlier one and never made later damage draw
|
|
733
|
+
a Constitution save.
|
|
734
|
+
- **A stat-block on-hit trait's escape DC no longer crashes (C21a).**
|
|
735
|
+
`activities/formula.py` had no handler for `@skills.<code>.passive` — the
|
|
736
|
+
attacker's own passive score for a Foundry skill code, e.g. the Crocodile's
|
|
737
|
+
Bite: "the target ... has the Grappled condition (escape DC 12)" — so any
|
|
738
|
+
caster hitting the token, transformed or not, raised `ValueError: Unhandled
|
|
739
|
+
roll-data token`. It now resolves `10 + ability modifier + proficiency bonus
|
|
740
|
+
(doubled with Expertise) when the caster is proficient in that skill`: a
|
|
741
|
+
transformed creature's form gives the SRD escape DC (a Crocodile's 12), while
|
|
742
|
+
a template monster's uniform `attack_bonus` numbers give `10 + attack_bonus`
|
|
743
|
+
(BACKLOG).
|
|
744
|
+
- **A save action whose DC names an ability no longer crashes (C21a).**
|
|
745
|
+
`activities/save.py` resolved only the `spellcasting` and `flat` DC
|
|
746
|
+
calculations, so a save resolved without a fixed DC — a transformed
|
|
747
|
+
creature's stat-block action, a feature's save through `use_feature` —
|
|
748
|
+
raised `ValueError` on an ability-code DC (a Giant Spider's Web, a
|
|
749
|
+
Dragonborn's Breath Weapon) or a bare formula (a Swarm of Ravens'
|
|
750
|
+
Cacophony), and `@mod` in a monster save action's damage raised on the
|
|
751
|
+
monster's own turn. The DC is now 8 + that ability's modifier + the
|
|
752
|
+
Proficiency Bonus, or the formula, and `@mod` reads the ability the DC names
|
|
753
|
+
(Giant Constrictor Snake Constrict: 2d8 + 4).
|
|
754
|
+
- **Ended combats no longer stay in memory for the life of the process
|
|
755
|
+
(C23).** `end_combat` keeps the 64 most recently ended combats readable
|
|
756
|
+
(a repeat close, `get_live`, `drain_pending_events`, `narration_events`)
|
|
757
|
+
and releases older ones; their handles raise `UnknownHandleError`. A live
|
|
758
|
+
combat, or a new one registered under a reused handle id, is never
|
|
759
|
+
released.
|
|
760
|
+
- **Truesight no longer lets a Blinded creature see (C23).** SRD 5.2 lets
|
|
761
|
+
only Blindsight work "even if you have the Blinded condition"; the "can
|
|
762
|
+
see" composite and Summon Dragon's placement now agree. Truesight still
|
|
763
|
+
sees an Invisible creature.
|
|
764
|
+
- **A host-moved foe no longer draws opportunity attacks from its own side
|
|
765
|
+
(C23).** The monster-reactor direction takes only the mover's foes.
|
|
766
|
+
- **An engine-rolled Initiative reads a template's Dexterity (C23).** An
|
|
767
|
+
encounter member with a resolvable `monster_template_slug` and
|
|
768
|
+
`dexterity` left at 10 adds the template's modifier, the Dexterity it
|
|
769
|
+
fights with.
|
|
770
|
+
- **`nat20-bridge` reports its own version (C23).** `/v1/health` said
|
|
771
|
+
`"0.3.2"` since 0.3.2; `nat20_bridge.__version__` now reads the installed
|
|
772
|
+
package.
|
|
773
|
+
- **`GET /v1/combat/{cid}` returns a `grid` block.** The response now
|
|
774
|
+
carries the combat's `GridScene`, or `null` for a zone-graph combat — the
|
|
775
|
+
SillyTavern extension's map view reads it.
|
|
776
|
+
|
|
777
|
+
### Deprecated
|
|
778
|
+
|
|
779
|
+
- `start_combat(scene_zones=...)` — `DeprecationWarning` since 0.6.0, removed in
|
|
780
|
+
0.7.0. Pass `grid_scene=GridScene(...)` instead.
|
|
781
|
+
|
|
782
|
+
## [0.5.0]
|
|
783
|
+
|
|
784
|
+
Lockstep release with `dnd5e-srd-data` 0.5.0 and `nat20-bridge` 0.5.0.
|
|
785
|
+
|
|
786
|
+
Removes the legacy (Gen 1) rules surface deprecated in 0.4.0. The engine now
|
|
787
|
+
ships exactly one, seedable, documented rules implementation
|
|
788
|
+
(`orchestrator` + `activities/`). See `docs/migration/v0.4-to-v0.5.md`.
|
|
789
|
+
|
|
790
|
+
### Removed
|
|
791
|
+
|
|
792
|
+
- Modules `dispatch`, `event_dicts`, `types.dice`, `types.intent`, and
|
|
793
|
+
`rules.{combat, combat_data, combat_helpers, equipment, gambits, resolution,
|
|
794
|
+
spells}` (plus the private `rules._parsing` / `rules._class_meta`).
|
|
795
|
+
- `dnd5e_engine.ActionType`, `CombatNPC`, and the lazy
|
|
796
|
+
`dnd5e_engine.types.{ActionType, CombatOutcome, DiceOutcome, CombatNPC}`
|
|
797
|
+
re-exports; the 0.4.0 `DeathSaveState` / `BehaviorProfile` re-export shims.
|
|
798
|
+
- The cross-generation parity test; its Gen 2 assertions live on in
|
|
799
|
+
`tests/test_attack_classification.py`.
|
|
800
|
+
|
|
801
|
+
## [0.4.0]
|
|
802
|
+
|
|
803
|
+
Lockstep release with `dnd5e-srd-data` 0.4.0 and `nat20-bridge` 0.4.0.
|
|
804
|
+
|
|
805
|
+
Deprecates the legacy (Gen 1) rules surface. **Nothing is removed**; every
|
|
806
|
+
deprecated import warns. Removal lands in 0.5.0 — see
|
|
807
|
+
`docs/migration/v0.3-to-v0.4.md`.
|
|
808
|
+
|
|
809
|
+
### Deprecated
|
|
810
|
+
|
|
811
|
+
- **The legacy (Gen 1) rules surface — removed in 0.5.0.** `dispatch`,
|
|
812
|
+
`event_dicts`, `types.dice`, `types.intent`, and `rules.{combat, combat_data,
|
|
813
|
+
combat_helpers, equipment, gambits, resolution, spells}` emit a
|
|
814
|
+
`DeprecationWarning` on import. The top-level `dnd5e_engine.ActionType` and
|
|
815
|
+
`dnd5e_engine.types.{ActionType, CombatOutcome, DiceOutcome, CombatNPC}`
|
|
816
|
+
re-exports now resolve lazily and warn on access. Every symbol's supported
|
|
817
|
+
route is in `docs/migration/v0.3-to-v0.4.md`. Rationale: two drifting
|
|
818
|
+
implementations of the same SRD rules, a non-seedable legacy path, and host
|
|
819
|
+
transport/persistence shapes inside a zero-I/O package.
|
|
820
|
+
|
|
821
|
+
### Changed
|
|
822
|
+
|
|
823
|
+
- `DeathSaveState` canonical home is now `dnd5e_engine.death_saves`;
|
|
824
|
+
`BehaviorProfile` is now `dnd5e_engine.types.combat`. The old locations
|
|
825
|
+
re-export until 0.5.0. The supported engine's import tree no longer reaches
|
|
826
|
+
any legacy module (pinned by `tests/test_gen2_has_no_gen1_imports.py`).
|
|
827
|
+
|
|
828
|
+
### Added
|
|
829
|
+
|
|
830
|
+
- Cross-generation attack-roll parity test
|
|
831
|
+
(`tests/test_generation_parity_attack.py`): `rules/combat.attack_roll` /
|
|
832
|
+
`resolve_player_attack` and `activities/attack.py` are fed the same d20
|
|
833
|
+
stream and must keep the same natural die and agree on hit / crit / miss
|
|
834
|
+
under advantage, disadvantage and flat rolls. This is the guard that would
|
|
835
|
+
have caught the drift fixed in 0.3.1 (#11). No runtime behavior change.
|
|
836
|
+
|
|
837
|
+
## [0.3.2]
|
|
838
|
+
|
|
839
|
+
Lockstep release with `dnd5e-srd-data` 0.3.2 and `nat20-bridge` 0.3.2.
|
|
840
|
+
|
|
841
|
+
Additive maturity pass from an independent review of the packages as a
|
|
842
|
+
standalone engine. **No public name is removed or changed shape.**
|
|
843
|
+
|
|
844
|
+
### Fixed
|
|
845
|
+
|
|
846
|
+
- **Multiattack fan-out resolved the wrong attack mix for most monsters.** The
|
|
847
|
+
prose parser only matched `[[/item .id]]{label}`, but the corpus mostly ships
|
|
848
|
+
`[[/item Name]]` or bare mnemonic ids, so 174 of 180 multiattacks fell back to
|
|
849
|
+
"repeat one sibling N times" — silently wrong for every heterogeneous
|
|
850
|
+
multiattacker. The parser now reads per-token counts from the multiattack's
|
|
851
|
+
first sentence (excluding "It can replace one attack with …" riders) and
|
|
852
|
+
recovers names from Foundry's mnemonic ids. Precise joins: **6/180 → 119/180**.
|
|
853
|
+
Chuul, Otyugh, Cloaker, Unicorn, Mummy, Pit Fiend, Xorn and Marilith now make
|
|
854
|
+
their correct attack sequences. **Behaviour change for consumers:** affected
|
|
855
|
+
monsters deal their correct (higher) damage per round and consume more of the
|
|
856
|
+
dice stream, so combat transcripts seeded against 0.3.x will diverge.
|
|
857
|
+
- `__version__` now derives from installed package metadata instead of a
|
|
858
|
+
hand-maintained literal, so it cannot drift from `pyproject.toml` (0.3.0 shipped
|
|
859
|
+
reporting `0.2.0`). Pinned by `tests/test_determinism_contract.py`.
|
|
860
|
+
|
|
861
|
+
### Added
|
|
862
|
+
|
|
863
|
+
- **`CheckSpec.rng`** — `resolve_check` previously drew its d20 from the
|
|
864
|
+
process-global `random` module with no way to inject a generator,
|
|
865
|
+
contradicting the documented determinism guarantee. Pass a seeded
|
|
866
|
+
`random.Random` for a reproducible standalone check; `None` preserves the old
|
|
867
|
+
behaviour. `rules.dice`, `rules.skills` and `roll_dice_str` /
|
|
868
|
+
`apply_changes_to_check` gain matching optional `rng` parameters. Combat
|
|
869
|
+
resolution was already seeded and is unchanged.
|
|
870
|
+
- **`docs/capabilities.md`** — a per-mechanic matrix of what the engine
|
|
871
|
+
resolves, what loads but produces no events, and what is not modelled. Its
|
|
872
|
+
published counts are recomputed from the corpus by
|
|
873
|
+
`tests/test_capability_matrix.py`.
|
|
874
|
+
- Concept pages for reactions (the pre-armed model) and monsters (the built-in
|
|
875
|
+
AI, multiattack join rate, absent legendary actions).
|
|
876
|
+
- `tests/test_docstrings_are_host_agnostic.py` — fails the build on a docstring
|
|
877
|
+
that names a private downstream application, retired tooling, or a doc path
|
|
878
|
+
that does not exist in this repo. 104 such references were rendered onto the
|
|
879
|
+
public API site; all removed.
|
|
880
|
+
|
|
881
|
+
### Changed
|
|
882
|
+
|
|
883
|
+
- `CONDITION_EFFECTS[EXHAUSTION]` / `[PRONE]` descriptive strings corrected from
|
|
884
|
+
2014 to SRD 5.2 wording and labelled as not-enforced; the enforced exhaustion
|
|
885
|
+
rule is still the 2014 one (tracked in `BACKLOG.md`).
|
|
886
|
+
- Sphinx cross-reference roles in docstrings, which rendered as literal markup
|
|
887
|
+
on the API site, converted to inline code.
|
|
888
|
+
- Monkeypatching `roll_dice_str` now requires a two-parameter double
|
|
889
|
+
(`lambda expr, rng=None: …`).
|
|
890
|
+
|
|
891
|
+
## [0.3.0]
|
|
892
|
+
|
|
893
|
+
Lockstep release with `dnd5e-srd-data` 0.3.0 — the item charge lifecycle: gate
|
|
894
|
+
→ spend → cast-delegate → upcast → recharge → observe. See
|
|
895
|
+
`docs/migration/v0.2-to-v0.3.md` for the full, host-facing migration guide.
|
|
896
|
+
The engine now depends on `dnd5e-srd-data>=0.3.0` (the charge-gate and
|
|
897
|
+
cast-delegation paths read the new `Item.uses` / `Spell.foundry_uuid` schema).
|
|
898
|
+
|
|
899
|
+
### Added
|
|
900
|
+
|
|
901
|
+
- **Item charges** — `use_item` now validates and spends `consumption.targets[type=itemUses]`
|
|
902
|
+
costs against the item's `uses.max` pool, tracked in the `custom_counters` sidecar under
|
|
903
|
+
`item_use:<slug>` (`{"spent": n}`). Exhausted pools reject with
|
|
904
|
+
`CastFailed(reason="no_charges_remaining")` before the action budget is touched. Items
|
|
905
|
+
without a pool are unaffected. `CastFailedReason` also gains `invalid_charge_spend`, emitted
|
|
906
|
+
when a `charges_to_spend` request violates the activity's `consumption.scaling` (see
|
|
907
|
+
"Charge upcasting" below).
|
|
908
|
+
- **Item recharge** — `dnd5e_engine.rest.recover_item_uses` (+ `ITEM_USE_COUNTER_PREFIX`,
|
|
909
|
+
submodule-only export): pure recharge over `item_use:` pools. The engine's own
|
|
910
|
+
`rest.RecoveryPeriod` gains `"dawn"`/`"day"`/`"dusk"` (previously just `"sr"`/`"lr"`;
|
|
911
|
+
the data package's `RecoveryRule` already knew `"day"` — this widens the engine's
|
|
912
|
+
narrower rest-period literal to match); formula recovery now rolls dice ("1d6 + 1")
|
|
913
|
+
through an optional `rng` (also available on `recover_feature_uses`).
|
|
914
|
+
- **Cast delegation** — a used item's `CastActivity` now resolves its referenced spell
|
|
915
|
+
(uuid→Spell via `dnd5e-srd-data`'s `get_spell_by_uuid`) on the PC-intent path, honoring
|
|
916
|
+
the item's flat DC / attack / level overrides. `cast_spell_unresolved` now signals a
|
|
917
|
+
genuinely missing uuid, not missing plumbing.
|
|
918
|
+
- **Charge upcasting** — `PlayerIntent.charges_to_spend` casts at
|
|
919
|
+
`base level + extra charges`, validated against `consumption.scaling`
|
|
920
|
+
(`invalid_charge_spend` on violation). New `ActivityResolutionContext.cast_level_override`.
|
|
921
|
+
- **Charge read path** — `LiveCombatView.custom_counters_by_entity` (three-level snapshot
|
|
922
|
+
copy): hosts mirror `item_use:` / `feature_use:` pools per turn the same way they mirror
|
|
923
|
+
`spell_slots_by_entity`.
|
|
924
|
+
|
|
925
|
+
## [0.2.0]
|
|
926
|
+
|
|
927
|
+
Lockstep release with `dnd5e-srd-data` 0.2.0 — the outcome of the gap-closing
|
|
928
|
+
campaign (ten scenario clusters). See `docs/migration/v0.1-to-v0.2.md` for the
|
|
929
|
+
full, host-facing migration guide. The engine now depends on
|
|
930
|
+
`dnd5e-srd-data>=0.2.0` (the rest-cap path reads the new `Feature.uses` schema).
|
|
931
|
+
|
|
932
|
+
### Added
|
|
933
|
+
|
|
934
|
+
- **Rest & recovery** — new public, zero-I/O module `dnd5e_engine.rest`, called
|
|
935
|
+
*between* combats: `resolve_short_rest`, `resolve_long_rest`,
|
|
936
|
+
`recover_feature_uses`, the `HitDicePool` / `RestOutcome` value types, the
|
|
937
|
+
`RecoveryPeriod` literal — all added to the top-level `__all__`.
|
|
938
|
+
`FEATURE_USE_COUNTER_PREFIX` is also public, but is exported from the
|
|
939
|
+
submodule only: `from dnd5e_engine.rest import FEATURE_USE_COUNTER_PREFIX`.
|
|
940
|
+
- **Spatial (grid) surface** — `WallSegment` value type (also a top-level
|
|
941
|
+
export); `GridScene.wall_segments`, `GridScene.cover_cells`, and
|
|
942
|
+
`GridScene.difficult_terrain_cells` fields (all default empty/off);
|
|
943
|
+
`SpatialTopology.cover_between(...)`; and `GridTopology.cells_in_template(...)`
|
|
944
|
+
for sphere/cone/line AoE templates.
|
|
945
|
+
- **Passive-stat fields** — `condition_immunities` and `damage_vulnerabilities`
|
|
946
|
+
on `PartyMemberSpec` / `EncounterMemberSpec` / `Combatant`, and
|
|
947
|
+
`movement_modes` (`CombatantMovementModes`) plus `walk_speed_bonus` on
|
|
948
|
+
`PartyMemberSpec` / `Combatant`. All default empty and only take effect when
|
|
949
|
+
the backing dataset carries the trait.
|
|
950
|
+
- **Intent field** `PlayerIntent.activity_id` (default `None`) — disambiguates a
|
|
951
|
+
`USE_FEATURE` invocation of a multi-activity feature (Channel Divinity,
|
|
952
|
+
Cunning Strike).
|
|
953
|
+
- `PartyMemberSpec.reach_ft` (default `5`) — threads onto the live
|
|
954
|
+
`Combatant.melee_reach_ft`, making a reach weapon's opportunity-attack reach
|
|
955
|
+
reachable from the boundary spec.
|
|
956
|
+
- **Reaction surface** — `"ready"` intents now register a pre-armed reaction;
|
|
957
|
+
`PlayerIntent.reaction_trigger` narrows to the `ReactionTrigger` literal.
|
|
958
|
+
- `CastFailedReason` gained the values `"countered"` (Counterspell) and
|
|
959
|
+
`"no_uses_remaining"` (a capped, rest-recharged feature invoked over its cap).
|
|
960
|
+
- New additive `ActivityResolutionContext` sidecar fields backing the above
|
|
961
|
+
(cover, per-category damage-bonus buckets, Sneak Attack gates, reaction AC
|
|
962
|
+
bonus) — all default empty.
|
|
963
|
+
|
|
964
|
+
### Changed
|
|
965
|
+
|
|
966
|
+
- **Spell save DC is now the real SRD 5.2 formula** (`8 + proficiency +
|
|
967
|
+
spellcasting-ability modifier`) for PC caster spells, replacing a flat
|
|
968
|
+
Avrae-era approximation — the campaign's widest-blast-radius change. Any host
|
|
969
|
+
casting a PC save-DC spell sees a different, SRD-correct DC. Item casts and
|
|
970
|
+
monster casters are unchanged.
|
|
971
|
+
- **Sneak Attack rider** — a qualifying finesse/ranged rogue hit now deals the
|
|
972
|
+
`@scale`-resolved extra damage, doubled on a critical hit.
|
|
973
|
+
- **Passive-projection behavioral flips** — Rage damage resistance now halves
|
|
974
|
+
matching damage; an immune condition never attaches; a matching damage
|
|
975
|
+
vulnerability doubles the hit; movement-mode / walk-speed bonuses (Fast
|
|
976
|
+
Movement, Roving) now fold onto `base_speed`.
|
|
977
|
+
- **Monster behavior** — fleeing monsters now retreat away from their nearest
|
|
978
|
+
threat instead of standing still; a mixed melee+ranged multiattack now fires
|
|
979
|
+
the weapon that fits the current distance instead of always the first-listed
|
|
980
|
+
one; monsters Dash to close an out-of-reach movement gap.
|
|
981
|
+
- **Reactions & action economy** — a countered `cast_spell` does **not** expend
|
|
982
|
+
its spell slot; a reaction-applied 1-round buff (Shield's +5 AC) now survives
|
|
983
|
+
until the owner's next turn; `"disengage"` is now a real turn-non-ending
|
|
984
|
+
action; PCs moving out of a monster's reach now provoke opportunity attacks.
|
|
985
|
+
- **Active-effect changes** — `multiply` / `upgrade` / `downgrade` change modes
|
|
986
|
+
now apply; the `rwak` / `msak` / `rsak` damage and `spell.dc` bonus buckets
|
|
987
|
+
now fold; a weapon-tagged `damage.bonus` change now reaches swing damage.
|
|
988
|
+
- Feature-owned `@scale` ids now resolve; the Foundry `(@scale.x)dN` dice-count
|
|
989
|
+
idiom now normalizes; capped features (Second Wind) are now per-rest limited.
|
|
990
|
+
|
|
991
|
+
### Removed
|
|
992
|
+
|
|
993
|
+
- `ActionType.SHORT_REST` (`dnd5e_engine.types.intent`) — a dead, unreachable
|
|
994
|
+
member of the legacy dispatch enum with no handler. It was never the
|
|
995
|
+
live-combat intent surface (`events.py::IntentType` has no `"short_rest"`).
|
|
996
|
+
Hosts wanting rest resolution use the new `dnd5e_engine.rest` module.
|
|
997
|
+
- `rules/gambits.select_action` (with the private helpers `_PASS_ACTION` /
|
|
998
|
+
`_get_alive_targets` only it used) — the legacy per-profile gambit AI the live
|
|
999
|
+
monster-turn path never called. Every behaviour it offered now lives in
|
|
1000
|
+
`advance_monster_turn` / `activities/monster_actions.py`. Verified caller-free;
|
|
1001
|
+
it was not a top-level `__all__` export.
|
|
1002
|
+
(`rules/gambits.assign_behavior_profile` was reviewed and **retained** — it is
|
|
1003
|
+
a host-facing utility for constructing `EncounterMemberSpec.behavior_profile`
|
|
1004
|
+
from raw monster stats, not dead code.)
|
|
1005
|
+
## [0.1.2]
|
|
1006
|
+
|
|
1007
|
+
### Added
|
|
1008
|
+
- `ActionType.USE_ITEM` parser-facing action member.
|
|
1009
|
+
|
|
1010
|
+
## [0.1.1]
|
|
1011
|
+
|
|
1012
|
+
### Added
|
|
1013
|
+
- `LiveCombatView` — public read-model projection of live combat state.
|
|
1014
|
+
- `get_live(handle) -> LiveCombatView` is now part of the public API (`__all__`);
|
|
1015
|
+
it returns a point-in-time snapshot view instead of the private `_LiveCombat`.
|
|
1016
|
+
- `roll_dice_str` — the dice-expression evaluator is now a public test seam
|
|
1017
|
+
(renamed from the private `_roll_dice_str`).
|
|
1018
|
+
|
|
1019
|
+
## [0.1.0]
|
|
1020
|
+
|
|
1021
|
+
First public release.
|
|
1022
|
+
|
|
1023
|
+
### Added
|
|
1024
|
+
|
|
1025
|
+
- Pure-Python, host-agnostic D&D 5e SRD rules engine: combat orchestration,
|
|
1026
|
+
ability checks and saving throws, and combat-scoped effects.
|
|
1027
|
+
- SRD 5.2 (2024) content resolution against the typed-Activity corpus via
|
|
1028
|
+
`BundledAssetLoader`, reading the canonical dataset from the
|
|
1029
|
+
`dnd5e-srd-data` package.
|
|
1030
|
+
- Curated public API surface (`__all__`) with a surface guard test.
|
|
1031
|
+
- `py.typed` marker — the package ships inline type information.
|
|
1032
|
+
- Zero-I/O guarantee: no DB, network, or async dependencies in the engine.
|