dnd5e-engine 0.4.0__tar.gz → 0.5.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.4.0 → dnd5e_engine-0.5.0}/CHANGELOG.md +19 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/PKG-INFO +1 -1
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/pyproject.toml +2 -4
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/__init__.py +0 -23
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/specs.py +1 -1
- dnd5e_engine-0.5.0/src/dnd5e_engine/types/__init__.py +18 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/types/combat.py +2 -32
- dnd5e_engine-0.5.0/tests/activities/test_save_dc_and_damage_exprs.py +129 -0
- dnd5e_engine-0.5.0/tests/test_attack_classification.py +84 -0
- dnd5e_engine-0.5.0/tests/test_combat_bucket_keys.py +61 -0
- dnd5e_engine-0.5.0/tests/test_combat_flag_advantage.py +25 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_gen2_has_no_gen1_imports.py +6 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_public_api_surface.py +0 -12
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rules_combat_data_and_skills.py +0 -149
- dnd5e_engine-0.5.0/tests/test_rules_dice_equipment_spells.py +302 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rules_effects_and_gambits.py +0 -133
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rules_smoke.py +1 -8
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_types_smoke.py +0 -73
- 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/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/types/__init__.py +0 -49
- 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/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.5.0}/.gitignore +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/LICENSE +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/Makefile +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/NOTICE +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/README.md +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/scripts/_smoke_grid_combat.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/scripts/smoke_clean_install.sh +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/apply.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/attack.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/build_context.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/cast.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/check.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/context.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/damage.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/dice.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/effects.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/formula.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/heal.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/mastery.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/monster_actions.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/passive_stats.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/resolver.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/save.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/save_primitive.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/activities/scale.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/build_party.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/build_spec.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/check.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/death_saves.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/events.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/lib_loader.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/orchestrator.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/outcome.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/py.typed +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rest.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/results.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/conditions.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/dice.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/effects.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/rules/skills.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/spatial.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/testing.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/types/conditions.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/types/effects.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/src/dnd5e_engine/views.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_build_context.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_cast_level_override.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_monster_actions.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_multiattack_fidelity.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_resolve_check_activity.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_save_primitive.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/activities/test_weapon_mastery.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/__init__.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/conftest.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/harness.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c00_harness_sanity.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c01_dataset_fixes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c02_small_mechanics.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c03_effect_modes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c04_caster_plumbing.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c05_spatial.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c06_reactions.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c07_sneak_and_repertoire.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c08_passive_projection.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c09_rest.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c10_monster_behavior.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/e2e/test_c11_item_charges.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_active_effect_resistance_fold.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_active_effect_schema.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_apply_changes_to_check.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_apply_modifiers.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_build_context_abilities.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_build_party.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_build_party_passives.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_capability_matrix.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_concentration_drop_effect_name.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_concentration_repeat_save_identity.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_cover_bonus_sidecar.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_coverage_backfill_death_saves.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_coverage_backfill_seam_errors.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_determinism_contract.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_docstrings_are_host_agnostic.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_duration_tick_at_turn_end.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_effects_builder_internals.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_enchantment_sidecar_projection.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_end_combat_snapshot.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_expire_clears_active_conditions.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_feature_save_dc_no_spell_override.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_get_actor_active_effects.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_grid_topology.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_iter11_fixes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_iter13_fixes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_iter5_fixes.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_lib_loader.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_lib_loader_configure.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_live_combat_view.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_monster_dash.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_monster_flee_retreat.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_gating_typed.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_grid_combat.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_item_charges.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_monster_typed.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_move_mark_typed.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_orchestrator_pc_resolution_typed.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_party_member_reach_ft.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_passive_stats.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_phase6_public_surface.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rage_second_wind_e2e.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_reaction_queue.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_resolve_check.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_resolve_check_flag_advantage.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_resolve_check_specific_buckets.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rest.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_rules_conditions.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_scale_resolver.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_smoke.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_sneak_attack_resolver.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_start_combat_active_effects.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_start_combat_seeds_lifecycle.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_use_feature_intent.py +0 -0
- {dnd5e_engine-0.4.0 → dnd5e_engine-0.5.0}/tests/test_weapon_damage_bonus_sidecar.py +0 -0
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0]
|
|
11
|
+
|
|
12
|
+
Lockstep release with `dnd5e-srd-data` 0.5.0 and `nat20-bridge` 0.5.0.
|
|
13
|
+
|
|
14
|
+
Removes the legacy (Gen 1) rules surface deprecated in 0.4.0. The engine now
|
|
15
|
+
ships exactly one, seedable, documented rules implementation
|
|
16
|
+
(`orchestrator` + `activities/`). See `docs/migration/v0.4-to-v0.5.md`.
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
|
|
20
|
+
- Modules `dispatch`, `event_dicts`, `types.dice`, `types.intent`, and
|
|
21
|
+
`rules.{combat, combat_data, combat_helpers, equipment, gambits, resolution,
|
|
22
|
+
spells}` (plus the private `rules._parsing` / `rules._class_meta`).
|
|
23
|
+
- `dnd5e_engine.ActionType`, `CombatNPC`, and the lazy
|
|
24
|
+
`dnd5e_engine.types.{ActionType, CombatOutcome, DiceOutcome, CombatNPC}`
|
|
25
|
+
re-exports; the 0.4.0 `DeathSaveState` / `BehaviorProfile` re-export shims.
|
|
26
|
+
- The cross-generation parity test; its Gen 2 assertions live on in
|
|
27
|
+
`tests/test_attack_classification.py`.
|
|
28
|
+
|
|
10
29
|
## [0.4.0]
|
|
11
30
|
|
|
12
31
|
Lockstep release with `dnd5e-srd-data` 0.4.0 and `nat20-bridge` 0.4.0.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dnd5e-engine"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.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"
|
|
@@ -88,10 +88,8 @@ max-complexity = 23
|
|
|
88
88
|
[tool.pytest.ini_options]
|
|
89
89
|
testpaths = ["tests"]
|
|
90
90
|
filterwarnings = [
|
|
91
|
-
# Any DeprecationWarning the engine itself raises is a bug
|
|
92
|
-
# 0.4.0 legacy-surface deprecations, which the suite exercises on purpose.
|
|
91
|
+
# Any DeprecationWarning the engine itself raises is a bug.
|
|
93
92
|
"error::DeprecationWarning:dnd5e_engine",
|
|
94
|
-
"ignore:.*legacy \\(Gen 1\\) surface.*:DeprecationWarning",
|
|
95
93
|
]
|
|
96
94
|
|
|
97
95
|
[tool.coverage.run]
|
|
@@ -82,7 +82,6 @@ from dnd5e_engine.types.effects import (
|
|
|
82
82
|
|
|
83
83
|
__all__ = [
|
|
84
84
|
"AbilityScores",
|
|
85
|
-
"ActionType",
|
|
86
85
|
"ActiveEffect",
|
|
87
86
|
"ActiveEffectChange",
|
|
88
87
|
"ActiveEffectDuration",
|
|
@@ -129,25 +128,3 @@ __all__ = [
|
|
|
129
128
|
"start_combat",
|
|
130
129
|
"submit_player_intent",
|
|
131
130
|
]
|
|
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}")
|
|
@@ -170,7 +170,7 @@ class EncounterMemberSpec(BaseModel):
|
|
|
170
170
|
# Person targets humanoids; Sleep autopasses undead/elves; etc.).
|
|
171
171
|
creature_type: str | None = None
|
|
172
172
|
# SRD §Damage Resistance / §Damage Immunity — per-monster type lists,
|
|
173
|
-
# populated from
|
|
173
|
+
# populated from the host's monster template record. Empty
|
|
174
174
|
# by default for fixtures that don't specify.
|
|
175
175
|
damage_resistances: list[str] = Field(default_factory=list)
|
|
176
176
|
damage_immunities: list[str] = Field(default_factory=list)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""dnd5e-engine type surface — host-agnostic Pydantic + Literal types."""
|
|
2
|
+
|
|
3
|
+
from dnd5e_engine.types.combat import Combatant
|
|
4
|
+
from dnd5e_engine.types.conditions import ActiveCondition, ConditionScope
|
|
5
|
+
from dnd5e_engine.types.effects import (
|
|
6
|
+
ActiveEffect,
|
|
7
|
+
ActiveEffectChange,
|
|
8
|
+
ActiveEffectDuration,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ActiveCondition",
|
|
13
|
+
"ActiveEffect",
|
|
14
|
+
"ActiveEffectChange",
|
|
15
|
+
"ActiveEffectDuration",
|
|
16
|
+
"Combatant",
|
|
17
|
+
"ConditionScope",
|
|
18
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Combat participant types — ``Combatant``
|
|
1
|
+
"""Combat participant types — ``Combatant`` and ``BehaviorProfile``.
|
|
2
2
|
|
|
3
3
|
Host-agnostic value types: stdlib + pydantic + the engine's own
|
|
4
4
|
``ActiveCondition``. ``Combatant`` is the engine's per-creature runtime combat
|
|
@@ -24,35 +24,6 @@ class BehaviorProfile(StrEnum):
|
|
|
24
24
|
DEFENSIVE = "DEFENSIVE"
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class CombatNPC(BaseModel):
|
|
28
|
-
"""Ephemeral sidecar record for an NPC in combat. Host-scoped;
|
|
29
|
-
cleared at end of combat. Owns template-derived combat stats that don't
|
|
30
|
-
fit on the narrow Combatant: saves, resistances, immunities, ability
|
|
31
|
-
scores, behavior_profile. Symmetric to CombatMonster for non-Character
|
|
32
|
-
entities."""
|
|
33
|
-
|
|
34
|
-
npc_id: str # persistent NPC node ID (npc:hex12)
|
|
35
|
-
template_id: str # MonsterTemplate node ID used at materialization
|
|
36
|
-
name: str
|
|
37
|
-
hp_current: int
|
|
38
|
-
hp_max: int
|
|
39
|
-
ac: int
|
|
40
|
-
attack_bonus: int
|
|
41
|
-
damage_dice: str
|
|
42
|
-
damage_type: str
|
|
43
|
-
has_ranged_attack: bool = False
|
|
44
|
-
dexterity: int = 10
|
|
45
|
-
strength: int = 10
|
|
46
|
-
constitution: int = 10
|
|
47
|
-
wisdom: int = 10
|
|
48
|
-
intelligence: int = 10
|
|
49
|
-
charisma: int = 10
|
|
50
|
-
behavior_profile: str = "AGGRESSIVE"
|
|
51
|
-
damage_resistances: list[str] = Field(default_factory=list)
|
|
52
|
-
damage_immunities: list[str] = Field(default_factory=list)
|
|
53
|
-
is_alive: bool = True
|
|
54
|
-
|
|
55
|
-
|
|
56
27
|
class Combatant(BaseModel):
|
|
57
28
|
entity_id: str
|
|
58
29
|
entity_type: str # "Character" | "Monster" | "NPC"
|
|
@@ -110,7 +81,7 @@ class Combatant(BaseModel):
|
|
|
110
81
|
# application path (``activities/effects.py::apply_activity_effects``)
|
|
111
82
|
# suppresses a ``ConditionApplied`` whose condition is in this list
|
|
112
83
|
# . Empty by default. NOTE: distinct from the dead, host-supplied
|
|
113
|
-
# ``
|
|
84
|
+
# legacy dispatch surface's ``condition_immunities`` (removed in 0.5.0).
|
|
114
85
|
condition_immunities: list[str] = Field(default_factory=list)
|
|
115
86
|
# SRD §Senses — special senses in feet (darkvision/blindsight/tremorsense/
|
|
116
87
|
# truesight). Projected from PC species + always-on feature passive_effects
|
|
@@ -224,6 +195,5 @@ class Combatant(BaseModel):
|
|
|
224
195
|
|
|
225
196
|
__all__ = [
|
|
226
197
|
"BehaviorProfile",
|
|
227
|
-
"CombatNPC",
|
|
228
198
|
"Combatant",
|
|
229
199
|
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Direct pins on ``activities/save.py::_resolve_dc`` and the damage-expression
|
|
2
|
+
builders in ``activities/dice.py`` / ``activities/formula.py``.
|
|
3
|
+
|
|
4
|
+
The end-to-end scenarios reach these only through the corpus' common shapes
|
|
5
|
+
(spellcasting DC, automatic ``{n}d{d}`` formulas); the flat-DC, custom-formula
|
|
6
|
+
and scaling-token branches are pinned here.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from dnd5e_srd_data.loader import BundledAssetLoader
|
|
15
|
+
from dnd5e_srd_data.schema.common import (
|
|
16
|
+
DamageCustomBlock,
|
|
17
|
+
DamagePart,
|
|
18
|
+
DamagePartBlock,
|
|
19
|
+
DamageScalingBlock,
|
|
20
|
+
SaveActivity,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from dnd5e_engine.activities.context import ActivityResolutionContext
|
|
24
|
+
from dnd5e_engine.activities.dice import damage_part_to_expr
|
|
25
|
+
from dnd5e_engine.activities.formula import resolve_damage_block
|
|
26
|
+
from dnd5e_engine.activities.save import _resolve_dc
|
|
27
|
+
from dnd5e_engine.types.combat import Combatant
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ctx(**overrides: object) -> ActivityResolutionContext:
|
|
31
|
+
kwargs: dict[str, object] = dict(
|
|
32
|
+
rng=random.Random(1),
|
|
33
|
+
caster=Combatant(
|
|
34
|
+
entity_id="char:wiz",
|
|
35
|
+
entity_type="Character",
|
|
36
|
+
name="Wiz",
|
|
37
|
+
initiative=10,
|
|
38
|
+
hp_current=10,
|
|
39
|
+
hp_max=10,
|
|
40
|
+
),
|
|
41
|
+
targets=[],
|
|
42
|
+
event_emitter=lambda ev: None,
|
|
43
|
+
caster_abilities={"str": 10, "dex": 10, "con": 10, "int": 16, "wis": 10, "cha": 10},
|
|
44
|
+
caster_proficiency_bonus=2,
|
|
45
|
+
spellcasting_ability="int",
|
|
46
|
+
)
|
|
47
|
+
kwargs.update(overrides)
|
|
48
|
+
return ActivityResolutionContext(**kwargs) # type: ignore[arg-type]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture(scope="module")
|
|
52
|
+
def fireball_save() -> SaveActivity:
|
|
53
|
+
spell = BundledAssetLoader().get_spell("fireball")
|
|
54
|
+
assert spell is not None
|
|
55
|
+
activity = next(a for a in spell.activities if a.kind == "save")
|
|
56
|
+
assert isinstance(activity, SaveActivity)
|
|
57
|
+
return activity
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _with_dc(activity: SaveActivity, calculation: str, formula: str = "") -> SaveActivity:
|
|
61
|
+
dc = activity.save.dc.model_copy(update={"calculation": calculation, "formula": formula})
|
|
62
|
+
return activity.model_copy(update={"save": activity.save.model_copy(update={"dc": dc})})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── _resolve_dc ──────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_spellcasting_dc_is_8_plus_prof_plus_mod(fireball_save: SaveActivity) -> None:
|
|
69
|
+
assert _resolve_dc(fireball_save, _ctx()) == 8 + 2 + 3
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_spellcasting_dc_without_a_casting_ability_is_loud(fireball_save: SaveActivity) -> None:
|
|
73
|
+
with pytest.raises(ValueError, match="spellcasting ability"):
|
|
74
|
+
_resolve_dc(fireball_save, _ctx(spellcasting_ability=None))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_flat_dc_resolves_the_formula(fireball_save: SaveActivity) -> None:
|
|
78
|
+
assert _resolve_dc(_with_dc(fireball_save, "flat", "13"), _ctx()) == 13
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_flat_dc_resolves_roll_data_tokens(fireball_save: SaveActivity) -> None:
|
|
82
|
+
# @prof folds to the caster's proficiency bonus before evaluation.
|
|
83
|
+
assert _resolve_dc(_with_dc(fireball_save, "flat", "10 + @prof"), _ctx()) == 12
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_unknown_dc_calculation_is_loud(fireball_save: SaveActivity) -> None:
|
|
87
|
+
with pytest.raises(ValueError, match="not resolvable"):
|
|
88
|
+
_resolve_dc(_with_dc(fireball_save, "mystery"), _ctx())
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_save_dc_override_bypasses_the_calculation(fireball_save: SaveActivity) -> None:
|
|
92
|
+
assert _resolve_dc(_with_dc(fireball_save, "mystery"), _ctx(save_dc_override=18)) == 18
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── damage_part_to_expr ──────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_weapon_damage_part_passes_its_dice_string_through() -> None:
|
|
99
|
+
assert damage_part_to_expr(DamagePart(dice="2d6+3", damage_type="slashing")) == "2d6+3"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_activity_block_builds_the_automatic_formula() -> None:
|
|
103
|
+
assert damage_part_to_expr(DamagePartBlock(number=2, denomination=6, bonus="3")) == "2d6+3"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_activity_block_with_only_a_bonus_is_the_bare_bonus() -> None:
|
|
107
|
+
assert damage_part_to_expr(DamagePartBlock(bonus="5")) == "5"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_activity_block_custom_formula_wins_when_enabled() -> None:
|
|
111
|
+
block = DamagePartBlock(
|
|
112
|
+
number=2, denomination=6, custom=DamageCustomBlock(enabled=True, formula="1d4")
|
|
113
|
+
)
|
|
114
|
+
assert damage_part_to_expr(block) == "1d4"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ── resolve_damage_block ─────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_scaling_formula_tokens_are_resolved_off_the_context() -> None:
|
|
121
|
+
block = DamagePartBlock(number=1, denomination=8, scaling=DamageScalingBlock(formula="@prof"))
|
|
122
|
+
resolved = resolve_damage_block(block, _ctx(), ability="int")
|
|
123
|
+
assert resolved.scaling.formula == "2"
|
|
124
|
+
assert resolved.number == 1 # untouched fields survive the copy
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_block_without_tokens_is_returned_unchanged() -> None:
|
|
128
|
+
block = DamagePartBlock(number=1, denomination=8, bonus="2")
|
|
129
|
+
assert resolve_damage_block(block, _ctx(), ability="int") is block
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Pin the SRD hit / crit / miss classification of the attack resolver.
|
|
2
|
+
|
|
3
|
+
Carried over from the (now retired) cross-generation parity test: these are
|
|
4
|
+
the rules whose drift between the two former implementations motivated the
|
|
5
|
+
0.5.0 consolidation, so they stay pinned on the surviving path.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import random
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from dnd5e_srd_data.loader import BundledAssetLoader
|
|
14
|
+
from dnd5e_srd_data.schema.common import AttackActivity
|
|
15
|
+
|
|
16
|
+
from dnd5e_engine.activities.attack import _resolve_hit_outcome, _roll_natural_d20
|
|
17
|
+
from dnd5e_engine.activities.context import ActivityResolutionContext
|
|
18
|
+
from dnd5e_engine.types.combat import Combatant
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture(scope="module")
|
|
22
|
+
def attack() -> AttackActivity:
|
|
23
|
+
longsword = BundledAssetLoader().get_weapon("longsword")
|
|
24
|
+
assert longsword is not None
|
|
25
|
+
activity = next(a for a in longsword.activities if a.kind == "attack")
|
|
26
|
+
assert isinstance(activity, AttackActivity)
|
|
27
|
+
return activity
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ctx(seed: int) -> ActivityResolutionContext:
|
|
31
|
+
hero = Combatant(
|
|
32
|
+
entity_id="char:hero",
|
|
33
|
+
entity_type="Character",
|
|
34
|
+
name="Hero",
|
|
35
|
+
initiative=10,
|
|
36
|
+
hp_current=10,
|
|
37
|
+
hp_max=10,
|
|
38
|
+
)
|
|
39
|
+
return ActivityResolutionContext(
|
|
40
|
+
rng=random.Random(seed),
|
|
41
|
+
caster=hero,
|
|
42
|
+
targets=[],
|
|
43
|
+
event_emitter=lambda ev: None,
|
|
44
|
+
caster_abilities={"str": 10, "dex": 10, "con": 10, "int": 10, "wis": 10, "cha": 10},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_natural_one_always_misses_even_with_a_huge_bonus(attack: AttackActivity) -> None:
|
|
49
|
+
assert _resolve_hit_outcome(1, 1 + 50, 10, attack) == (False, False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_natural_twenty_always_crits_even_against_impossible_ac(attack: AttackActivity) -> None:
|
|
53
|
+
assert _resolve_hit_outcome(20, 20, 99, attack) == (True, True)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_meeting_the_ac_exactly_is_a_hit(attack: AttackActivity) -> None:
|
|
57
|
+
assert _resolve_hit_outcome(10, 15, 15, attack) == (False, True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_falling_one_short_of_the_ac_is_a_miss(attack: AttackActivity) -> None:
|
|
61
|
+
assert _resolve_hit_outcome(10, 14, 15, attack) == (False, False)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize("seed", range(32))
|
|
65
|
+
def test_advantage_keeps_the_higher_and_disadvantage_the_lower_die(seed: int) -> None:
|
|
66
|
+
rng = random.Random(seed)
|
|
67
|
+
a, b = rng.randint(1, 20), rng.randint(1, 20)
|
|
68
|
+
assert _roll_natural_d20(_ctx(seed), "advantage") == max(a, b)
|
|
69
|
+
assert _roll_natural_d20(_ctx(seed), "disadvantage") == min(a, b)
|
|
70
|
+
assert _roll_natural_d20(_ctx(seed), "normal") == a
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_disadvantage_does_not_crit_on_the_discarded_die(attack: AttackActivity) -> None:
|
|
74
|
+
# Find a seed whose two dice are (20, x<20): the kept die under disadvantage
|
|
75
|
+
# must not be the discarded 20.
|
|
76
|
+
for seed in range(500):
|
|
77
|
+
rng = random.Random(seed)
|
|
78
|
+
a, b = rng.randint(1, 20), rng.randint(1, 20)
|
|
79
|
+
if 20 in (a, b) and min(a, b) < 20:
|
|
80
|
+
natural = _roll_natural_d20(_ctx(seed), "disadvantage")
|
|
81
|
+
assert natural == min(a, b)
|
|
82
|
+
assert _resolve_hit_outcome(natural, natural, 99, attack)[0] is False
|
|
83
|
+
return
|
|
84
|
+
pytest.fail("no seed with a discarded 20 in 500 tries")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Regression — combat resolvers use Foundry-shaped bucket keys (Phase 6).
|
|
2
|
+
|
|
3
|
+
Codex review of Phase 6 caught that ``rules/combat.py`` was passing legacy
|
|
4
|
+
bucket strings (``"saving_throw"``, ``"damage"``, ``"attack_roll"``, ``"ac"``)
|
|
5
|
+
into ``apply_changes_to_check``. The seed templates emit Foundry-shaped keys
|
|
6
|
+
(``save.bonus``, ``damage.bonus``, ``attack.roll.bonus``, ``ac.bonus``); the
|
|
7
|
+
helper does exact key matching, so the resolvers were silently no-op-ing
|
|
8
|
+
every effect. These tests lock the alignment.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dnd5e_engine.types.effects import (
|
|
14
|
+
ActiveEffect,
|
|
15
|
+
ActiveEffectChange,
|
|
16
|
+
ActiveEffectDuration,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _bless(target_id: str = "char:hero") -> ActiveEffect:
|
|
21
|
+
"""Seed-shaped Bless: +1d4 on attack rolls and saves (Foundry keys)."""
|
|
22
|
+
return ActiveEffect(
|
|
23
|
+
id="effect:bless",
|
|
24
|
+
name="Bless",
|
|
25
|
+
origin="cast:bless:1",
|
|
26
|
+
target_id=target_id,
|
|
27
|
+
duration=ActiveEffectDuration(rounds=10),
|
|
28
|
+
changes=[
|
|
29
|
+
ActiveEffectChange(key="attack.roll.bonus", mode="add", value="1d4"),
|
|
30
|
+
ActiveEffectChange(key="save.bonus", mode="add", value="1d4"),
|
|
31
|
+
],
|
|
32
|
+
flags={"concentration": True},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cloak(target_id: str = "char:hero") -> ActiveEffect:
|
|
37
|
+
"""Seed-shaped Cloak of Protection: +1 AC and +1 saves (Foundry keys)."""
|
|
38
|
+
return ActiveEffect(
|
|
39
|
+
id="effect:cloak_of_protection",
|
|
40
|
+
name="Cloak of Protection",
|
|
41
|
+
origin="item:cloak_of_protection:1",
|
|
42
|
+
target_id=target_id,
|
|
43
|
+
changes=[
|
|
44
|
+
ActiveEffectChange(key="ac.bonus", mode="add", value=1),
|
|
45
|
+
ActiveEffectChange(key="save.bonus", mode="add", value=1),
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _plus_one_sword(target_id: str = "char:hero") -> ActiveEffect:
|
|
51
|
+
"""Seed-shaped +1 Weapon: +1 attack and +1 damage (Foundry keys)."""
|
|
52
|
+
return ActiveEffect(
|
|
53
|
+
id="effect:weapon_plus_1",
|
|
54
|
+
name="+1 Weapon",
|
|
55
|
+
origin="item:weapon_plus_1:1",
|
|
56
|
+
target_id=target_id,
|
|
57
|
+
changes=[
|
|
58
|
+
ActiveEffectChange(key="attack.roll.bonus", mode="add", value=1),
|
|
59
|
+
ActiveEffectChange(key="damage.bonus", mode="add", value=1),
|
|
60
|
+
],
|
|
61
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Regression — combat resolvers honor flag-based advantage / disadvantage.
|
|
2
|
+
|
|
3
|
+
Codex Phase 6 review iter-12 P2: combat.py's saving_throw and
|
|
4
|
+
resolve_player_attack didn't read flag-based advantage / disadvantage
|
|
5
|
+
from active_effects, so Restrained / Guidance-style / Faerie Fire /
|
|
6
|
+
Invisible effects didn't shift the d20 mechanic in combat. Now both
|
|
7
|
+
mirror the iter-10/11 resolve_check fix.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dnd5e_engine.types.effects import (
|
|
13
|
+
ActiveEffect,
|
|
14
|
+
ActiveEffectChange,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _flag_effect(key: str, target_id: str = "char:hero") -> ActiveEffect:
|
|
19
|
+
return ActiveEffect(
|
|
20
|
+
id=f"effect:test_{key}",
|
|
21
|
+
name=f"test_{key}",
|
|
22
|
+
origin=f"cast:test:{key}",
|
|
23
|
+
target_id=target_id,
|
|
24
|
+
changes=[ActiveEffectChange(key=key, mode="override", value=True)],
|
|
25
|
+
)
|
|
@@ -7,6 +7,7 @@ the supported ones.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import ast
|
|
10
|
+
import importlib.util
|
|
10
11
|
import subprocess
|
|
11
12
|
import sys
|
|
12
13
|
|
|
@@ -50,3 +51,8 @@ def test_gen2_entry_points_load_no_gen1_module() -> None:
|
|
|
50
51
|
loaded = _loaded_modules()
|
|
51
52
|
leaked = [m for m in loaded if m in GEN1]
|
|
52
53
|
assert leaked == [], leaked
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_gen1_modules_no_longer_exist() -> None:
|
|
57
|
+
present = [m for m in GEN1 if importlib.util.find_spec(m) is not None]
|
|
58
|
+
assert present == [], present
|
|
@@ -5,7 +5,6 @@ import dnd5e_engine
|
|
|
5
5
|
|
|
6
6
|
TOP_LEVEL = {
|
|
7
7
|
"AbilityScores",
|
|
8
|
-
"ActionType",
|
|
9
8
|
"ActiveEffect",
|
|
10
9
|
"ActiveEffectChange",
|
|
11
10
|
"ActiveEffectDuration",
|
|
@@ -54,21 +53,12 @@ TOP_LEVEL = {
|
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
PUBLIC_MODULES = [
|
|
57
|
-
"dnd5e_engine.rules.combat",
|
|
58
56
|
"dnd5e_engine.rules.conditions",
|
|
59
57
|
"dnd5e_engine.rules.dice",
|
|
60
|
-
"dnd5e_engine.rules.equipment",
|
|
61
|
-
"dnd5e_engine.rules.gambits",
|
|
62
|
-
"dnd5e_engine.rules.resolution",
|
|
63
58
|
"dnd5e_engine.rules.skills",
|
|
64
|
-
"dnd5e_engine.rules.spells",
|
|
65
|
-
"dnd5e_engine.rules.combat_data",
|
|
66
|
-
"dnd5e_engine.rules.combat_helpers",
|
|
67
59
|
"dnd5e_engine.events",
|
|
68
|
-
"dnd5e_engine.event_dicts",
|
|
69
60
|
"dnd5e_engine.death_saves",
|
|
70
61
|
"dnd5e_engine.outcome",
|
|
71
|
-
"dnd5e_engine.dispatch",
|
|
72
62
|
"dnd5e_engine.specs",
|
|
73
63
|
"dnd5e_engine.spatial",
|
|
74
64
|
"dnd5e_engine.lib_loader",
|
|
@@ -77,10 +67,8 @@ PUBLIC_MODULES = [
|
|
|
77
67
|
"dnd5e_engine.build_spec",
|
|
78
68
|
"dnd5e_engine.build_party",
|
|
79
69
|
"dnd5e_engine.results",
|
|
80
|
-
"dnd5e_engine.types.intent",
|
|
81
70
|
"dnd5e_engine.types.combat",
|
|
82
71
|
"dnd5e_engine.types.conditions",
|
|
83
|
-
"dnd5e_engine.types.dice",
|
|
84
72
|
"dnd5e_engine.types.effects",
|
|
85
73
|
"dnd5e_engine.testing",
|
|
86
74
|
]
|