dnd5e-engine 0.3.1__tar.gz → 0.4.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.3.1 → dnd5e_engine-0.4.0}/CHANGELOG.md +90 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/Makefile +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/PKG-INFO +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/pyproject.toml +7 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/__init__.py +43 -10
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/__init__.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/apply.py +2 -2
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/attack.py +6 -6
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/build_context.py +14 -50
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/cast.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/check.py +2 -2
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/context.py +5 -5
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/damage.py +3 -3
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/dice.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/effects.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/formula.py +2 -2
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/heal.py +3 -3
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/monster_actions.py +143 -48
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/passive_stats.py +10 -10
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/resolver.py +2 -2
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/save.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/save_primitive.py +4 -4
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/scale.py +5 -5
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/build_party.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/check.py +33 -21
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/death_saves.py +92 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/dispatch.py +23 -10
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/event_dicts.py +15 -2
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/events.py +4 -4
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/lib_loader.py +9 -4
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/orchestrator.py +140 -121
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/outcome.py +5 -5
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rest.py +7 -7
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/results.py +2 -2
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/combat.py +33 -13
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/combat_data.py +13 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/combat_helpers.py +14 -90
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/conditions.py +36 -18
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/dice.py +39 -27
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/effects.py +15 -7
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/equipment.py +13 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/gambits.py +14 -7
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/resolution.py +14 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/skills.py +16 -9
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/spells.py +15 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/spatial.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/specs.py +10 -3
- dnd5e_engine-0.4.0/src/dnd5e_engine/types/__init__.py +49 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/combat.py +20 -10
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/conditions.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/dice.py +15 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/effects.py +5 -5
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/types/intent.py +28 -15
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/views.py +4 -1
- dnd5e_engine-0.4.0/tests/activities/test_multiattack_fidelity.py +127 -0
- dnd5e_engine-0.4.0/tests/activities/test_resolve_check_activity.py +349 -0
- dnd5e_engine-0.4.0/tests/activities/test_save_primitive.py +347 -0
- dnd5e_engine-0.4.0/tests/activities/test_weapon_mastery.py +335 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_apply_changes_to_check.py +2 -2
- dnd5e_engine-0.4.0/tests/test_capability_matrix.py +125 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_combat_bucket_keys.py +1 -1
- dnd5e_engine-0.4.0/tests/test_determinism_contract.py +131 -0
- dnd5e_engine-0.4.0/tests/test_docstrings_are_host_agnostic.py +60 -0
- dnd5e_engine-0.4.0/tests/test_gen1_deprecation_warnings.py +63 -0
- dnd5e_engine-0.4.0/tests/test_gen2_has_no_gen1_imports.py +52 -0
- dnd5e_engine-0.4.0/tests/test_generation_parity_attack.py +179 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_iter11_fixes.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_orchestrator_pc_resolution_typed.py +1 -1
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_public_api_surface.py +9 -4
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_resolve_check.py +2 -2
- dnd5e_engine-0.4.0/tests/test_rules_combat_data_and_skills.py +451 -0
- dnd5e_engine-0.4.0/tests/test_rules_combat_helpers.py +654 -0
- dnd5e_engine-0.4.0/tests/test_rules_combat_resolution.py +802 -0
- dnd5e_engine-0.4.0/tests/test_rules_conditions.py +428 -0
- dnd5e_engine-0.4.0/tests/test_rules_dice_equipment_spells.py +563 -0
- dnd5e_engine-0.4.0/tests/test_rules_effects_and_gambits.py +580 -0
- dnd5e_engine-0.4.0/tests/test_rules_resolution_and_parsing.py +368 -0
- dnd5e_engine-0.3.1/src/dnd5e_engine/types/__init__.py +0 -24
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/.gitignore +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/LICENSE +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/NOTICE +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/README.md +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/scripts/_smoke_grid_combat.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/scripts/smoke_clean_install.sh +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/activities/mastery.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/build_spec.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/py.typed +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/__init__.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/_class_meta.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/rules/_parsing.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/src/dnd5e_engine/testing.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/__init__.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/activities/__init__.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/activities/test_build_context.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/activities/test_cast_level_override.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/activities/test_monster_actions.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/__init__.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/conftest.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/harness.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c00_harness_sanity.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c01_dataset_fixes.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c02_small_mechanics.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c03_effect_modes.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c04_caster_plumbing.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c05_spatial.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c06_reactions.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c07_sneak_and_repertoire.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c08_passive_projection.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c09_rest.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c10_monster_behavior.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/e2e/test_c11_item_charges.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_active_effect_resistance_fold.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_active_effect_schema.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_apply_modifiers.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_build_context_abilities.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_build_party.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_build_party_passives.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_combat_flag_advantage.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_concentration_drop_effect_name.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_concentration_repeat_save_identity.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_cover_bonus_sidecar.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_coverage_backfill_death_saves.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_coverage_backfill_seam_errors.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_duration_tick_at_turn_end.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_effects_builder_internals.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_enchantment_sidecar_projection.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_end_combat_snapshot.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_expire_clears_active_conditions.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_feature_save_dc_no_spell_override.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_get_actor_active_effects.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_grid_topology.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_iter13_fixes.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_iter5_fixes.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_lib_loader.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_lib_loader_configure.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_live_combat_view.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_monster_dash.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_monster_flee_retreat.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_orchestrator_gating_typed.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_orchestrator_grid_combat.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_orchestrator_item_charges.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_orchestrator_monster_typed.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_orchestrator_move_mark_typed.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_party_member_reach_ft.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_passive_stats.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_phase6_public_surface.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_rage_second_wind_e2e.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_reaction_queue.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_resolve_check_flag_advantage.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_resolve_check_specific_buckets.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_rest.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_rules_smoke.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_scale_resolver.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_smoke.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_sneak_attack_resolver.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_start_combat_active_effects.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_start_combat_seeds_lifecycle.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_types_smoke.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_use_feature_intent.py +0 -0
- {dnd5e_engine-0.3.1 → dnd5e_engine-0.4.0}/tests/test_weapon_damage_bonus_sidecar.py +0 -0
|
@@ -7,6 +7,96 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0]
|
|
11
|
+
|
|
12
|
+
Lockstep release with `dnd5e-srd-data` 0.4.0 and `nat20-bridge` 0.4.0.
|
|
13
|
+
|
|
14
|
+
Deprecates the legacy (Gen 1) rules surface. **Nothing is removed**; every
|
|
15
|
+
deprecated import warns. Removal lands in 0.5.0 — see
|
|
16
|
+
`docs/migration/v0.3-to-v0.4.md`.
|
|
17
|
+
|
|
18
|
+
### Deprecated
|
|
19
|
+
|
|
20
|
+
- **The legacy (Gen 1) rules surface — removed in 0.5.0.** `dispatch`,
|
|
21
|
+
`event_dicts`, `types.dice`, `types.intent`, and `rules.{combat, combat_data,
|
|
22
|
+
combat_helpers, equipment, gambits, resolution, spells}` emit a
|
|
23
|
+
`DeprecationWarning` on import. The top-level `dnd5e_engine.ActionType` and
|
|
24
|
+
`dnd5e_engine.types.{ActionType, CombatOutcome, DiceOutcome, CombatNPC}`
|
|
25
|
+
re-exports now resolve lazily and warn on access. Every symbol's supported
|
|
26
|
+
route is in `docs/migration/v0.3-to-v0.4.md`. Rationale: two drifting
|
|
27
|
+
implementations of the same SRD rules, a non-seedable legacy path, and host
|
|
28
|
+
transport/persistence shapes inside a zero-I/O package.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- `DeathSaveState` canonical home is now `dnd5e_engine.death_saves`;
|
|
33
|
+
`BehaviorProfile` is now `dnd5e_engine.types.combat`. The old locations
|
|
34
|
+
re-export until 0.5.0. The supported engine's import tree no longer reaches
|
|
35
|
+
any legacy module (pinned by `tests/test_gen2_has_no_gen1_imports.py`).
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Cross-generation attack-roll parity test
|
|
40
|
+
(`tests/test_generation_parity_attack.py`): `rules/combat.attack_roll` /
|
|
41
|
+
`resolve_player_attack` and `activities/attack.py` are fed the same d20
|
|
42
|
+
stream and must keep the same natural die and agree on hit / crit / miss
|
|
43
|
+
under advantage, disadvantage and flat rolls. This is the guard that would
|
|
44
|
+
have caught the drift fixed in 0.3.1 (#11). No runtime behavior change.
|
|
45
|
+
|
|
46
|
+
## [0.3.2]
|
|
47
|
+
|
|
48
|
+
Lockstep release with `dnd5e-srd-data` 0.3.2 and `nat20-bridge` 0.3.2.
|
|
49
|
+
|
|
50
|
+
Additive maturity pass from an independent review of the packages as a
|
|
51
|
+
standalone engine. **No public name is removed or changed shape.**
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **Multiattack fan-out resolved the wrong attack mix for most monsters.** The
|
|
56
|
+
prose parser only matched `[[/item .id]]{label}`, but the corpus mostly ships
|
|
57
|
+
`[[/item Name]]` or bare mnemonic ids, so 174 of 180 multiattacks fell back to
|
|
58
|
+
"repeat one sibling N times" — silently wrong for every heterogeneous
|
|
59
|
+
multiattacker. The parser now reads per-token counts from the multiattack's
|
|
60
|
+
first sentence (excluding "It can replace one attack with …" riders) and
|
|
61
|
+
recovers names from Foundry's mnemonic ids. Precise joins: **6/180 → 119/180**.
|
|
62
|
+
Chuul, Otyugh, Cloaker, Unicorn, Mummy, Pit Fiend, Xorn and Marilith now make
|
|
63
|
+
their correct attack sequences. **Behaviour change for consumers:** affected
|
|
64
|
+
monsters deal their correct (higher) damage per round and consume more of the
|
|
65
|
+
dice stream, so combat transcripts seeded against 0.3.x will diverge.
|
|
66
|
+
- `__version__` now derives from installed package metadata instead of a
|
|
67
|
+
hand-maintained literal, so it cannot drift from `pyproject.toml` (0.3.0 shipped
|
|
68
|
+
reporting `0.2.0`). Pinned by `tests/test_determinism_contract.py`.
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **`CheckSpec.rng`** — `resolve_check` previously drew its d20 from the
|
|
73
|
+
process-global `random` module with no way to inject a generator,
|
|
74
|
+
contradicting the documented determinism guarantee. Pass a seeded
|
|
75
|
+
`random.Random` for a reproducible standalone check; `None` preserves the old
|
|
76
|
+
behaviour. `rules.dice`, `rules.skills` and `roll_dice_str` /
|
|
77
|
+
`apply_changes_to_check` gain matching optional `rng` parameters. Combat
|
|
78
|
+
resolution was already seeded and is unchanged.
|
|
79
|
+
- **`docs/capabilities.md`** — a per-mechanic matrix of what the engine
|
|
80
|
+
resolves, what loads but produces no events, and what is not modelled. Its
|
|
81
|
+
published counts are recomputed from the corpus by
|
|
82
|
+
`tests/test_capability_matrix.py`.
|
|
83
|
+
- Concept pages for reactions (the pre-armed model) and monsters (the built-in
|
|
84
|
+
AI, multiattack join rate, absent legendary actions).
|
|
85
|
+
- `tests/test_docstrings_are_host_agnostic.py` — fails the build on a docstring
|
|
86
|
+
that names a private downstream application, retired tooling, or a doc path
|
|
87
|
+
that does not exist in this repo. 104 such references were rendered onto the
|
|
88
|
+
public API site; all removed.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- `CONDITION_EFFECTS[EXHAUSTION]` / `[PRONE]` descriptive strings corrected from
|
|
93
|
+
2014 to SRD 5.2 wording and labelled as not-enforced; the enforced exhaustion
|
|
94
|
+
rule is still the 2014 one (tracked in `BACKLOG.md`).
|
|
95
|
+
- Sphinx cross-reference roles in docstrings, which rendered as literal markup
|
|
96
|
+
on the API site, converted to inline code.
|
|
97
|
+
- Monkeypatching `roll_dice_str` now requires a two-parameter double
|
|
98
|
+
(`lambda expr, rng=None: …`).
|
|
99
|
+
|
|
10
100
|
## [0.3.0]
|
|
11
101
|
|
|
12
102
|
Lockstep release with `dnd5e-srd-data` 0.3.0 — the item charge lifecycle: gate
|
|
@@ -12,7 +12,7 @@ test:
|
|
|
12
12
|
uv run pytest -q
|
|
13
13
|
# Baseline-and-ratchet coverage gate (75% observed today; floor set 2pts under).
|
|
14
14
|
coverage:
|
|
15
|
-
uv run pytest -q --cov=dnd5e_engine --cov-report=term-missing --cov-fail-under=
|
|
15
|
+
uv run pytest -q --cov=dnd5e_engine --cov-report=term-missing --cov-fail-under=90
|
|
16
16
|
security:
|
|
17
17
|
uv run bandit -r src/ -c pyproject.toml -q
|
|
18
18
|
smoke:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dnd5e-engine"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Pure-Python D&D 5e SRD rules engine — combat, checks, effects. Host-agnostic, zero I/O."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -87,6 +87,12 @@ max-complexity = 23
|
|
|
87
87
|
|
|
88
88
|
[tool.pytest.ini_options]
|
|
89
89
|
testpaths = ["tests"]
|
|
90
|
+
filterwarnings = [
|
|
91
|
+
# Any DeprecationWarning the engine itself raises is a bug — except the
|
|
92
|
+
# 0.4.0 legacy-surface deprecations, which the suite exercises on purpose.
|
|
93
|
+
"error::DeprecationWarning:dnd5e_engine",
|
|
94
|
+
"ignore:.*legacy \\(Gen 1\\) surface.*:DeprecationWarning",
|
|
95
|
+
]
|
|
90
96
|
|
|
91
97
|
[tool.coverage.run]
|
|
92
98
|
source = ["dnd5e_engine"]
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
"""dnd5e-engine — host-agnostic D&D 5e SRD rules engine.
|
|
1
|
+
"""dnd5e-engine — a host-agnostic, zero-I/O D&D 5e SRD 5.2 rules engine.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Everything exported here is listed in ``__all__`` below; that list is the
|
|
4
|
+
package's stable contract and is pinned by ``tests/test_public_api_surface.py``.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
The four combat-loop coroutines are ``start_combat``, ``submit_player_intent``,
|
|
7
|
+
``advance_monster_turn`` and ``end_combat``; ``resolve_check`` is the standalone
|
|
8
|
+
ability/skill/saving-throw resolver, and ``build_party_member`` turns a
|
|
9
|
+
``CharacterBuildSpec`` into a combat-ready ``PartyMemberSpec``.
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
Rules content is never bundled with the engine. It is read at runtime from the
|
|
12
|
+
companion ``dnd5e-srd-data`` package; call ``configure_lib_loader`` to
|
|
13
|
+
substitute your own typed corpus.
|
|
14
|
+
|
|
15
|
+
See the ``docs/`` tree (published at https://tapestria.github.io/nat20/) for the
|
|
16
|
+
combat model, the activity corpus, and a per-mechanic capability matrix
|
|
17
|
+
describing exactly which SRD rules this engine resolves today.
|
|
12
18
|
"""
|
|
13
19
|
|
|
14
20
|
from __future__ import annotations
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
from importlib.metadata import PackageNotFoundError
|
|
23
|
+
from importlib.metadata import version as _pkg_version
|
|
24
|
+
|
|
25
|
+
try: # pragma: no cover - trivial packaging fallback
|
|
26
|
+
__version__ = _pkg_version("dnd5e-engine")
|
|
27
|
+
except PackageNotFoundError: # pragma: no cover - source tree without install
|
|
28
|
+
__version__ = "0.0.0+unknown"
|
|
17
29
|
|
|
18
30
|
from dnd5e_engine.build_party import build_party_member
|
|
19
31
|
from dnd5e_engine.build_spec import (
|
|
@@ -67,7 +79,6 @@ from dnd5e_engine.types.effects import (
|
|
|
67
79
|
ActiveEffectChange,
|
|
68
80
|
ActiveEffectDuration,
|
|
69
81
|
)
|
|
70
|
-
from dnd5e_engine.types.intent import ActionType
|
|
71
82
|
|
|
72
83
|
__all__ = [
|
|
73
84
|
"AbilityScores",
|
|
@@ -118,3 +129,25 @@ __all__ = [
|
|
|
118
129
|
"start_combat",
|
|
119
130
|
"submit_player_intent",
|
|
120
131
|
]
|
|
132
|
+
|
|
133
|
+
_GEN1_LAZY = {"ActionType": ("dnd5e_engine.types.intent", "ActionType")}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def __getattr__(name: str) -> object:
|
|
137
|
+
# PEP 562 — keep the 0.3.x names resolvable without importing the
|
|
138
|
+
# deprecated modules eagerly (which would warn on every ``import dnd5e_engine``).
|
|
139
|
+
if name in _GEN1_LAZY:
|
|
140
|
+
import importlib
|
|
141
|
+
import warnings
|
|
142
|
+
|
|
143
|
+
module, attr = _GEN1_LAZY[name]
|
|
144
|
+
warnings.warn(
|
|
145
|
+
f"dnd5e_engine.{name} belongs to the legacy (Gen 1) surface — a host-side "
|
|
146
|
+
f"shape, not an engine type — and will be removed in dnd5e-engine 0.5.0. "
|
|
147
|
+
f"Import it from {module} (also deprecated) or copy it into your host; "
|
|
148
|
+
f"see docs/migration/v0.3-to-v0.4.md.",
|
|
149
|
+
DeprecationWarning,
|
|
150
|
+
stacklevel=2,
|
|
151
|
+
)
|
|
152
|
+
return getattr(importlib.import_module(module), attr)
|
|
153
|
+
raise AttributeError(f"module 'dnd5e_engine' has no attribute {name!r}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Typed Foundry Activity resolver.
|
|
2
2
|
|
|
3
|
-
Parallel to the
|
|
3
|
+
Parallel to the legacy ``effects/`` package: consumes the typed
|
|
4
4
|
``dnd5e_srd_data`` ``Activity`` discriminated union and emits the engine's
|
|
5
5
|
existing ``CombatEvent`` union via an ``event_emitter`` callback.
|
|
6
6
|
"""
|
|
@@ -4,7 +4,7 @@ target-side vulnerability / resistance / immunity, emit ``DamageApplied``.
|
|
|
4
4
|
MIRRORS, does not import from, ``effects/damage.py``:
|
|
5
5
|
|
|
6
6
|
* Apply order is vulnerability ×2 → resistance //2 (integer floor) → immunity ⇒ 0,
|
|
7
|
-
matching ``_apply_resistance`` (
|
|
7
|
+
matching ``_apply_resistance`` (the legacy ``do_resistances`` order). An ``"all"``
|
|
8
8
|
wildcard is honored in each list (SRD §Conditions/Petrified emits "all").
|
|
9
9
|
* ``is_overkill`` mirrors ``effects/damage.py:192`` — ``final_amount >
|
|
10
10
|
target.hp_current`` (strictly greater).
|
|
@@ -88,7 +88,7 @@ def _apply_modifiers(
|
|
|
88
88
|
immunities: set[str],
|
|
89
89
|
vulnerabilities: set[str],
|
|
90
90
|
) -> int:
|
|
91
|
-
"""Apply vuln (×2) → resist (//2 floor) → immune (⇒0) in
|
|
91
|
+
"""Apply vuln (×2) → resist (//2 floor) → immune (⇒0) in the legacy evaluator order.
|
|
92
92
|
|
|
93
93
|
Mirrors ``effects/damage.py:_apply_resistance``. The ``"all"`` wildcard is
|
|
94
94
|
honored in each list (SRD §Conditions/Petrified resistance-to-all).
|
|
@@ -106,7 +106,7 @@ def resolve_attack(
|
|
|
106
106
|
|
|
107
107
|
# SRD §Advantage and Disadvantage — read the attacker's own
|
|
108
108
|
# ``flags.advantage.attack`` / ``flags.disadvantage.attack`` override
|
|
109
|
-
# changes (both present cancel to normal,
|
|
109
|
+
# changes (both present cancel to normal, the legacy evaluator ``reconcile_adv``). These
|
|
110
110
|
# gate the SRD §Sneak Attack trigger below; they deliberately do NOT change
|
|
111
111
|
# the natural-d20 ``mode`` on this live path. Doing so would shift the
|
|
112
112
|
# seeded dice stream and the crit outcome of the base attack, perturbing
|
|
@@ -127,7 +127,7 @@ def resolve_attack(
|
|
|
127
127
|
# target's EFFECTIVE AC for this attack only; total cover is filtered
|
|
128
128
|
# upstream (the target is never reachable as a resolver target at all).
|
|
129
129
|
# SRD Shield — "+5 bonus to AC, including against the triggering
|
|
130
|
-
# attack"
|
|
130
|
+
# attack" a pre-armed reaction can land an AC-bonus active
|
|
131
131
|
# effect on the target between the trigger and this comparison; the
|
|
132
132
|
# natural roll itself is never touched, only this comparison.
|
|
133
133
|
effective_ac = (
|
|
@@ -193,7 +193,7 @@ def attacker_advantage_flags(ctx: ActivityResolutionContext) -> tuple[bool, bool
|
|
|
193
193
|
has_advantage = True
|
|
194
194
|
elif ch.key == "flags.disadvantage.attack":
|
|
195
195
|
has_disadvantage = True
|
|
196
|
-
# Both present cancel to normal (
|
|
196
|
+
# Both present cancel to normal (the legacy evaluator ``reconcile_adv``).
|
|
197
197
|
if has_advantage and has_disadvantage:
|
|
198
198
|
return False, False
|
|
199
199
|
return has_advantage, has_disadvantage
|
|
@@ -290,7 +290,7 @@ def _is_ranged_weapon(weapon: Weapon | None) -> bool:
|
|
|
290
290
|
"""True iff ``weapon`` is a ranged weapon (Foundry rwak scope).
|
|
291
291
|
|
|
292
292
|
The ranged analog of ``_is_melee_weapon`` — the scope of
|
|
293
|
-
``system.bonuses.rwak.damage``
|
|
293
|
+
``system.bonuses.rwak.damage`` .
|
|
294
294
|
"""
|
|
295
295
|
return weapon is not None and weapon.weapon_category in _RANGED_CATEGORIES
|
|
296
296
|
|
|
@@ -311,7 +311,7 @@ def _weapon_default_ability(weapon: Weapon, ctx: ActivityResolutionContext) -> s
|
|
|
311
311
|
"""SRD default attack/damage ability for a weapon with no explicit ability.
|
|
312
312
|
|
|
313
313
|
* ranged weapon (``weapon_category`` in the ranged set) → DEX.
|
|
314
|
-
* finesse weapon (the ``finesse``
|
|
314
|
+
* finesse weapon (the ``finesse`` ``WeaponProperty``) → the better of the
|
|
315
315
|
caster's STR/DEX modifier (SRD §Finesse: the wielder chooses).
|
|
316
316
|
* otherwise (melee non-finesse) → STR.
|
|
317
317
|
|
|
@@ -482,7 +482,7 @@ def _apply_on_hit_damage(
|
|
|
482
482
|
if melee_bonus_expr:
|
|
483
483
|
by_type[first_type] += roll_expr(melee_bonus_expr, ctx.rng)
|
|
484
484
|
|
|
485
|
-
#
|
|
485
|
+
# / Foundry ``system.bonuses.rwak.damage`` — a ranged weapon
|
|
486
486
|
# attack damage bonus, the ranged analog of the melee-only bonus just
|
|
487
487
|
# above. Add it once to the first damage type, RANGED WEAPON only.
|
|
488
488
|
if first_type is not None and _is_ranged_weapon(weapon):
|
|
@@ -1,49 +1,13 @@
|
|
|
1
|
-
"""Build an
|
|
1
|
+
"""Build an ``ActivityResolutionContext`` from live combat state.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
This is the bridge between the orchestrator's mutable ``_LiveCombat`` and the
|
|
4
|
+
pure activity resolvers. It reads the caster's real six ability scores,
|
|
5
|
+
level-derived proficiency bonus, spellcasting ability and ``@scale`` values,
|
|
6
|
+
folds in the passive attack/damage/save/check modifiers projected from active
|
|
7
|
+
effects and conditions, and hands the resolvers a single immutable context.
|
|
5
8
|
|
|
6
|
-
The
|
|
7
|
-
|
|
8
|
-
(``attack_bonus``, ``dexterity`` only — no per-ability scores, proficiency,
|
|
9
|
-
or spellcasting ability). A Character caster's real six-ability scores +
|
|
10
|
-
``character_level``-derived proficiency bonus have since landed (piece 4)
|
|
11
|
-
and, per Cluster 4 (C04-S01), its save DC now runs the honest SRD 5.2
|
|
12
|
-
formula (``8 + proficiency bonus + spellcasting-ability mod``, ``_save_dc``
|
|
13
|
-
below) whenever a spellcasting ability resolves. The OLD flat
|
|
14
|
-
approximations below are the byte-for-byte FALLBACK for the paths that still
|
|
15
|
-
have no better ground truth:
|
|
16
|
-
|
|
17
|
-
* PC fallback (no resolvable spellcasting ability — e.g. a non-caster class,
|
|
18
|
-
or a ``use_item`` cast, which never sets one) mirrors
|
|
19
|
-
``intent_resolver._spellcasting_mod`` / ``_proficiency_bonus`` /
|
|
20
|
-
``_spell_save_dc``: ``mod = max(0, attack_bonus-2)``, ``pb = 2``,
|
|
21
|
-
``save_dc = 8 + pb + mod``.
|
|
22
|
-
* Monster magnitudes (unaffected by Cluster 4 — no monster stat block threads
|
|
23
|
-
a per-ability sheet through here) mirror ``monster_ai._monster_save_dc``:
|
|
24
|
-
``mod = attack_bonus``, ``save_dc = 8 + attack_bonus``.
|
|
25
|
-
|
|
26
|
-
``caster_abilities`` is set uniformly across all six abilities to ``10 +
|
|
27
|
-
2*mod`` FOR A MONSTER caster so the attack/damage handler — which resolves
|
|
28
|
-
the governing ability *dynamically* (explicit ``attack.ability`` → weapon
|
|
29
|
-
SRD default → finesse better-of-str/dex → spellcasting ability) — yields the
|
|
30
|
-
old uniform ``@mod`` regardless of which ability it picks. A Character
|
|
31
|
-
caster's ``caster_abilities`` are its real six scores (see below).
|
|
32
|
-
|
|
33
|
-
This module is PURE: no I/O, no orchestrator import, no double-compute. The
|
|
34
|
-
orchestrator already builds the per-entity passive sidecars (its
|
|
35
|
-
``_build_hydration_payload`` projection) and passes the two dicts this builder
|
|
36
|
-
needs — ``passive_damage_modifiers`` and ``save_modifiers`` — in as parameters at
|
|
37
|
-
the Task 5/6 cutover. This builder only RESHAPES them into the typed context:
|
|
38
|
-
|
|
39
|
-
* ``save_modifiers[id]["saves"]`` → ``passive_save_modifiers[id]`` ({ability:int})
|
|
40
|
-
* ``save_modifiers[id]["passive_save_bonus"]`` (signed dice string) → carried verbatim
|
|
41
|
-
* ``save_modifiers[id]["passive_save_adv"/"_dis"/"_auto_fail"]`` (UPPER-case
|
|
42
|
-
ability-code lists) → carried verbatim
|
|
43
|
-
|
|
44
|
-
so the typed save path (``save_primitive.roll_save``) consumes the SAME bless/bane
|
|
45
|
-
bonus, advantage/disadvantage, and auto-fail the OLD Avrae ``effects/save.py``
|
|
46
|
-
path did. Empty / absent sidecar fields reproduce the prior behavior exactly.
|
|
9
|
+
The resolvers never touch live combat state directly — everything they need
|
|
10
|
+
arrives on the context, which is what keeps them pure and testable.
|
|
47
11
|
"""
|
|
48
12
|
|
|
49
13
|
from __future__ import annotations
|
|
@@ -99,7 +63,7 @@ def _save_dc(
|
|
|
99
63
|
intent such as ``use_item``, which never sets ``spellcasting_ability``)
|
|
100
64
|
fall back to the OLD flat approximation byte-for-byte: Monster
|
|
101
65
|
``8 + attack_bonus`` (``_monster_save_dc``); PC ``8 + 2 + mod`` (the
|
|
102
|
-
|
|
66
|
+
the legacy evaluator-era ``_spell_save_dc``, ``pb`` hardcoded to ``2``).
|
|
103
67
|
"""
|
|
104
68
|
if caster.entity_type == "Monster":
|
|
105
69
|
return 8 + caster.attack_bonus
|
|
@@ -154,9 +118,9 @@ def build_activity_context(
|
|
|
154
118
|
sneak_attack_ally_adjacent: dict[str, bool] | None = None,
|
|
155
119
|
) -> ActivityResolutionContext:
|
|
156
120
|
"""Adapt the caster + the pre-computed hydration sidecars into the typed
|
|
157
|
-
|
|
121
|
+
``ActivityResolutionContext`` the new resolver consumes.
|
|
158
122
|
|
|
159
|
-
Caster magnitudes reproduce the OLD
|
|
123
|
+
Caster magnitudes reproduce the OLD the legacy evaluator approximations (see module
|
|
160
124
|
docstring). ``passive_damage_modifiers`` and ``save_modifiers`` are the two
|
|
161
125
|
dicts the orchestrator's ``_build_hydration_payload`` already produces —
|
|
162
126
|
passed IN so this builder stays pure (no orchestrator import, no I/O, no
|
|
@@ -188,7 +152,7 @@ def build_activity_context(
|
|
|
188
152
|
path keeps the blanket override.
|
|
189
153
|
|
|
190
154
|
``cast_level_override`` passes straight through to
|
|
191
|
-
|
|
155
|
+
``ActivityResolutionContext`` — a ``use_item`` charges_to_spend
|
|
192
156
|
upcast's forced delegated-cast level (orchestrator-computed off the
|
|
193
157
|
charge gate's own cost accounting). ``None`` (the default) leaves
|
|
194
158
|
``resolve_cast`` (``activities/cast.py``) to fall back to the wrapper
|
|
@@ -209,7 +173,7 @@ def build_activity_context(
|
|
|
209
173
|
}
|
|
210
174
|
caster_proficiency_bonus = proficiency_bonus(caster.character_level)
|
|
211
175
|
else:
|
|
212
|
-
# Monsters/NPCs have no per-ability sheet here: keep the
|
|
176
|
+
# Monsters/NPCs have no per-ability sheet here: keep the legacy evaluator-era
|
|
213
177
|
# uniform fake (10 + 2*mod across all six abilities, PB 2) so the
|
|
214
178
|
# dynamic governing-ability resolution in attack.py reproduces the old
|
|
215
179
|
# uniform @mod for any ability it picks.
|
|
@@ -253,7 +217,7 @@ def build_activity_context(
|
|
|
253
217
|
for entity_id, dmg_entry in passive_damage_modifiers.items():
|
|
254
218
|
# ``passive_damage_modifiers`` is a WIDE dict: resistance/immunity/
|
|
255
219
|
# vulnerability lists PLUS the signed-dice ``passive_to_hit_bonus`` STRING
|
|
256
|
-
# the orchestrator projection wedges in (mirrors the OLD
|
|
220
|
+
# the orchestrator projection wedges in (mirrors the OLD the legacy evaluator sidecar).
|
|
257
221
|
to_hit: object = dmg_entry.get("passive_to_hit_bonus")
|
|
258
222
|
if isinstance(to_hit, str) and to_hit:
|
|
259
223
|
passive_attack_bonus[entity_id] = to_hit
|
|
@@ -8,7 +8,7 @@ level, the referenced spell's base level / concentration flag / own
|
|
|
8
8
|
``resolve_activity`` for each activity on the referenced spell. ``cast`` emits
|
|
9
9
|
NOTHING itself — the delegated child activities emit their own events.
|
|
10
10
|
|
|
11
|
-
MIRRORS, does not import from, ``effects/spell.py`` (the
|
|
11
|
+
MIRRORS, does not import from, ``effects/spell.py`` (the legacy ``type:
|
|
12
12
|
"spell"`` analogue). Reproduces its semantics with the typed lib shapes:
|
|
13
13
|
|
|
14
14
|
* Spell-within-itself cycle guard FIRST (cheap, before the lookup): a ``uuid``
|
|
@@ -20,7 +20,7 @@ MIRRORS, does not import from, ``effects/check.py``:
|
|
|
20
20
|
* The skill→ability mapping mirrors ``effects/check.py:_SKILL_TO_ABILITY`` but is
|
|
21
21
|
keyed by the Foundry 3-letter skill *codes* the canonical ``check.associated``
|
|
22
22
|
field actually carries (``"ath"``, ``"slt"``, ``"inv"``, ...) rather than the
|
|
23
|
-
|
|
23
|
+
the legacy evaluator long-form (``"athletics"``). Source: ``CONFIG.DND5E.skills`` in
|
|
24
24
|
``foundry/module/config.mjs``.
|
|
25
25
|
* The modifier is the RESOLVED integer off a per-actor sidecar
|
|
26
26
|
(``ctx.check_modifiers``), mirroring ``effects/check.py:_read_check_modifiers``
|
|
@@ -67,7 +67,7 @@ _ABILITIES: Final[frozenset[str]] = frozenset(get_args(Ability))
|
|
|
67
67
|
# Foundry 3-letter skill code → governing SRD ability. Source: CONFIG.DND5E.skills
|
|
68
68
|
# in foundry/module/config.mjs (each skill's ``ability`` field). The canonical
|
|
69
69
|
# ``check.associated`` field carries these codes verbatim. Mirrors
|
|
70
|
-
# ``effects/check.py:_SKILL_TO_ABILITY`` (which keys the
|
|
70
|
+
# ``effects/check.py:_SKILL_TO_ABILITY`` (which keys the legacy evaluator long-form names).
|
|
71
71
|
_SKILL_TO_ABILITY: Final[dict[str, Ability]] = {
|
|
72
72
|
"acr": "dex",
|
|
73
73
|
"ani": "wis",
|
|
@@ -76,7 +76,7 @@ class ActivityResolutionContext:
|
|
|
76
76
|
passive_damage_modifiers: dict[str, dict[str, list[str]]] = field(default_factory=dict)
|
|
77
77
|
# Per-target saving-throw modifier sidecar, keyed entity_id -> {ability ->
|
|
78
78
|
# resolved save bonus}. Mirrors how ``effects/save.py`` sources the target
|
|
79
|
-
# save modifier (``
|
|
79
|
+
# save modifier (``the host effect store._save_modifiers[target_id]["saves"][ability]``)
|
|
80
80
|
# — the resolved per-ability integer, NOT rebuilt from ability score +
|
|
81
81
|
# proficiency. ``Combatant`` carries no per-ability save table, so the save
|
|
82
82
|
# handler reads this sidecar; an absent target / ability contributes +0
|
|
@@ -85,8 +85,8 @@ class ActivityResolutionContext:
|
|
|
85
85
|
passive_save_modifiers: dict[str, dict[str, int]] = field(default_factory=dict)
|
|
86
86
|
# Per-target additive save bonus, keyed entity_id -> a signed dice-expression
|
|
87
87
|
# STRING (e.g. ``"+1d4"`` for Bless, ``"-1d4"`` for Bane; stacked sources
|
|
88
|
-
# pre-joined as ``"a + b"``). Mirrors the OLD
|
|
89
|
-
# ``
|
|
88
|
+
# pre-joined as ``"a + b"``). Mirrors the OLD the legacy evaluator path's
|
|
89
|
+
# ``the host effect store._save_modifiers[id]["passive_save_bonus"]`` (orchestrator
|
|
90
90
|
# hydration, ``_build_hydration_payload``); rolled through ``ctx.rng`` so the
|
|
91
91
|
# bless/bane d4 lands in the same seed stream as the save d20. Absent target →
|
|
92
92
|
# +0 (no bonus). Empty default keeps the golden corpus identical.
|
|
@@ -148,7 +148,7 @@ class ActivityResolutionContext:
|
|
|
148
148
|
# ``"STR"``, ``"DEX"``, ...), keyed entity_id -> list[ability]. Mirrors the
|
|
149
149
|
# OLD path's ``passive_save_adv`` / ``passive_save_dis`` (Faerie Fire,
|
|
150
150
|
# Restrained, etc.). An ability present in both cancels to normal
|
|
151
|
-
# (
|
|
151
|
+
# (the legacy evaluator ``reconcile_adv``). Empty defaults keep the golden corpus identical.
|
|
152
152
|
passive_save_adv: dict[str, list[str]] = field(default_factory=dict)
|
|
153
153
|
passive_save_dis: dict[str, list[str]] = field(default_factory=dict)
|
|
154
154
|
# Per-target auto-fail ability-code list (UPPER-case), keyed entity_id ->
|
|
@@ -176,7 +176,7 @@ class ActivityResolutionContext:
|
|
|
176
176
|
# ``_fold_active_effect_changes`` + extracted in ``build_activity_context``);
|
|
177
177
|
# consumed in ``attack.py`` alongside the cover-AC fold, before the
|
|
178
178
|
# ``total >= target_ac`` comparison. Absent target -> +0. Empty default
|
|
179
|
-
# keeps the golden corpus identical
|
|
179
|
+
# keeps the golden corpus identical .
|
|
180
180
|
passive_ac_bonus: dict[str, int] = field(default_factory=dict)
|
|
181
181
|
# Per-actor ability/skill-check modifier sidecar, mirroring
|
|
182
182
|
# ``effects/check.py:_read_check_modifiers``'s shape
|
|
@@ -6,7 +6,7 @@ SRD 5.2 corpus: a single Magic Missile dart (``1d4+1`` force), or Divine Smite's
|
|
|
6
6
|
radiant burst (``2d8``/``3d8`` radiant) nested under a melee hit.
|
|
7
7
|
|
|
8
8
|
Crit rule (``DamageActivityDamageBlock.critical`` is a
|
|
9
|
-
|
|
9
|
+
``SaveDamageCriticalBlock`` — ``{allow, bonus}``): this activity crits ONLY
|
|
10
10
|
when ``critical.allow`` is True AND ``ctx.variables["in_crit"]`` is truthy (the
|
|
11
11
|
caller sets ``in_crit`` when the activity is nested under a crit attack). On crit
|
|
12
12
|
the dice double (``roll_damage_part(crit=True)``) and the parsed ``critical.bonus``
|
|
@@ -24,7 +24,7 @@ coercion). A typeless part (empty ``types`` — a DATA defect in a handful of
|
|
|
24
24
|
SRD spells; e.g. Call Lightning's 4d10 part) is logged loudly
|
|
25
25
|
(``damage_part_untyped``) and skipped: ``apply_damage`` keys strictly by SRD
|
|
26
26
|
type and has no untyped bucket, and the resolver cannot invent the SRD-correct
|
|
27
|
-
type. Tracked as a ``dnd5e-srd-data`` data bug in ``
|
|
27
|
+
type. Tracked as a ``dnd5e-srd-data`` data bug in ``BACKLOG.md``.
|
|
28
28
|
|
|
29
29
|
MIRRORS, does not import from, ``effects/damage.py``: @-token resolution runs
|
|
30
30
|
through ``formula.resolve_damage_block`` (governing ability = the caster's
|
|
@@ -111,7 +111,7 @@ def _part_type(
|
|
|
111
111
|
* Empty list: a DATA defect (the SRD part should carry a type). There is no
|
|
112
112
|
untyped bucket in ``apply_damage``, so the part is logged loudly
|
|
113
113
|
(``damage_part_untyped``) and skipped. Tracked as a ``dnd5e-srd-data`` bug
|
|
114
|
-
(see ``
|
|
114
|
+
(see ``BACKLOG.md``); the resolver must not invent the SRD-correct type.
|
|
115
115
|
"""
|
|
116
116
|
if not part.types:
|
|
117
117
|
_LOGGER.warning(
|
|
@@ -302,7 +302,7 @@ def _cantrip_dice_count(character_level: int) -> int:
|
|
|
302
302
|
def _double_dice(ast: d20.ast.Node) -> d20.ast.Node:
|
|
303
303
|
"""SRD §Critical Hits — double every ``Dice.num``, leave literals alone.
|
|
304
304
|
|
|
305
|
-
Mirrors ``effects/damage.py:_apply_crit_doubling`` (
|
|
305
|
+
Mirrors ``effects/damage.py:_apply_crit_doubling`` (the legacy default
|
|
306
306
|
``crit_mapper``): ``d20.utils.tree_map`` rebuilds each ``Dice(num, size)``
|
|
307
307
|
as ``Dice(num*2, size)``; numeric modifiers are untouched.
|
|
308
308
|
"""
|
|
@@ -228,7 +228,7 @@ def apply_activity_effects(
|
|
|
228
228
|
# gated on the mapping.
|
|
229
229
|
for status in sorted(pe.statuses):
|
|
230
230
|
if status in _CONDITION_VALUES:
|
|
231
|
-
#
|
|
231
|
+
# condition-immunity gate. A target immune to this
|
|
232
232
|
# condition (Nature's Ward → "poisoned") never has it attach —
|
|
233
233
|
# the ConditionApplied is SUPPRESSED outright (the EffectApplied
|
|
234
234
|
# rider above still fired narratively). Suppress, not
|
|
@@ -6,11 +6,11 @@ their dice/DC formula fields — e.g. Cure Wounds' healing bonus is verbatim
|
|
|
6
6
|
read these tokens, so each one is replaced with its caster-derived integer (as a
|
|
7
7
|
string) before the dice helper parses the formula.
|
|
8
8
|
|
|
9
|
-
MIRRORS, does not import from, the old
|
|
9
|
+
MIRRORS, does not import from, the old the legacy path's
|
|
10
10
|
``intent_resolver._walk_and_patch`` / ``_parse_with_injection``: that path
|
|
11
11
|
injected the same caster magnitudes (ability mods, proficiency, spell DC) into
|
|
12
12
|
formula strings before rolling. The caster's numbers live on
|
|
13
|
-
|
|
13
|
+
``ActivityResolutionContext``; the typed Activity only declares WHICH
|
|
14
14
|
ability/DC-calc applies.
|
|
15
15
|
|
|
16
16
|
Scope — the roll-data tokens that appear in canonical DICE/DC FORMULA fields
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""``heal`` kind handler for the Activity resolver.
|
|
2
2
|
|
|
3
3
|
Foundry ``heal-data.mjs`` stores healing as a single ``DamagePartBlock``
|
|
4
|
-
(
|
|
4
|
+
(``healing``), not a list. The ``types`` list carries a
|
|
5
5
|
single token: ``"healing"`` for HP restoration, ``"temphp"`` for a temporary
|
|
6
6
|
HP buffer — both confirmed against canonical SRD 5.2 data (cure-wounds.json
|
|
7
7
|
emits ``"healing"``; false-life.json emits ``"temphp"``). When ``temphp`` is
|
|
8
|
-
present we emit
|
|
8
|
+
present we emit ``TempHpApplied``, otherwise ``HealingApplied``.
|
|
9
9
|
|
|
10
10
|
MIRRORS, does not import from, ``effects/temphp.py`` for the temp-hp token
|
|
11
|
-
distinction. The keep-higher replacement policy lives only on the
|
|
11
|
+
distinction. The keep-higher replacement policy lives only on the legacy
|
|
12
12
|
``effects/`` path (which carries an existing-temp-HP lookup); the typed
|
|
13
13
|
Activity resolver emits the rolled grant directly, matching how ``apply.py``
|
|
14
14
|
emits the rolled magnitude for damage.
|