dnd5e-engine 0.2.0__tar.gz → 0.3.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.2.0 → dnd5e_engine-0.3.0}/CHANGELOG.md +34 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/PKG-INFO +2 -2
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/pyproject.toml +4 -2
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/__init__.py +2 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/build_context.py +10 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/cast.py +37 -8
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/context.py +5 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/events.py +7 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/orchestrator.py +363 -22
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rest.py +90 -27
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/specs.py +3 -3
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/views.py +5 -0
- dnd5e_engine-0.3.0/tests/activities/test_cast_level_override.py +121 -0
- dnd5e_engine-0.3.0/tests/e2e/test_c11_item_charges.py +276 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_live_combat_view.py +28 -0
- dnd5e_engine-0.3.0/tests/test_orchestrator_item_charges.py +906 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_public_api_surface.py +1 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_rest.py +46 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/.gitignore +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/LICENSE +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/Makefile +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/NOTICE +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/README.md +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/scripts/_smoke_grid_combat.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/scripts/smoke_clean_install.sh +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/__init__.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/apply.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/attack.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/check.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/damage.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/dice.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/effects.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/formula.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/heal.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/mastery.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/monster_actions.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/passive_stats.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/resolver.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/save.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/save_primitive.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/activities/scale.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/build_party.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/build_spec.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/check.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/death_saves.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/dispatch.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/event_dicts.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/lib_loader.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/outcome.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/py.typed +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/results.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/__init__.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/_class_meta.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/_parsing.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/combat.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/combat_data.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/combat_helpers.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/conditions.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/dice.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/effects.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/equipment.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/gambits.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/resolution.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/skills.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/rules/spells.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/spatial.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/testing.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/types/__init__.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/types/combat.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/types/conditions.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/types/dice.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/types/effects.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/src/dnd5e_engine/types/intent.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/__init__.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/activities/__init__.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/activities/test_build_context.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/activities/test_monster_actions.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/__init__.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/conftest.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/harness.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c00_harness_sanity.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c01_dataset_fixes.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c02_small_mechanics.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c03_effect_modes.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c04_caster_plumbing.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c05_spatial.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c06_reactions.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c07_sneak_and_repertoire.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c08_passive_projection.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c09_rest.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/e2e/test_c10_monster_behavior.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_active_effect_resistance_fold.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_active_effect_schema.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_apply_changes_to_check.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_apply_modifiers.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_build_context_abilities.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_build_party.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_build_party_passives.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_combat_bucket_keys.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_combat_flag_advantage.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_concentration_drop_effect_name.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_concentration_repeat_save_identity.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_cover_bonus_sidecar.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_coverage_backfill_death_saves.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_coverage_backfill_seam_errors.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_duration_tick_at_turn_end.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_effects_builder_internals.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_enchantment_sidecar_projection.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_end_combat_snapshot.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_expire_clears_active_conditions.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_feature_save_dc_no_spell_override.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_get_actor_active_effects.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_grid_topology.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_iter11_fixes.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_iter13_fixes.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_iter5_fixes.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_lib_loader.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_monster_dash.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_monster_flee_retreat.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_orchestrator_gating_typed.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_orchestrator_grid_combat.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_orchestrator_monster_typed.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_orchestrator_move_mark_typed.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_orchestrator_pc_resolution_typed.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_party_member_reach_ft.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_passive_stats.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_phase6_public_surface.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_rage_second_wind_e2e.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_reaction_queue.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_resolve_check.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_resolve_check_flag_advantage.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_resolve_check_specific_buckets.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_rules_smoke.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_scale_resolver.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_smoke.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_sneak_attack_resolver.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_start_combat_active_effects.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_start_combat_seeds_lifecycle.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_types_smoke.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_use_feature_intent.py +0 -0
- {dnd5e_engine-0.2.0 → dnd5e_engine-0.3.0}/tests/test_weapon_damage_bonus_sidecar.py +0 -0
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0]
|
|
11
|
+
|
|
12
|
+
Lockstep release with `dnd5e-srd-data` 0.3.0 — the item charge lifecycle: gate
|
|
13
|
+
→ spend → cast-delegate → upcast → recharge → observe. See
|
|
14
|
+
`docs/migration/v0.2-to-v0.3.md` for the full, host-facing migration guide.
|
|
15
|
+
The engine now depends on `dnd5e-srd-data>=0.3.0` (the charge-gate and
|
|
16
|
+
cast-delegation paths read the new `Item.uses` / `Spell.foundry_uuid` schema).
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Item charges** — `use_item` now validates and spends `consumption.targets[type=itemUses]`
|
|
21
|
+
costs against the item's `uses.max` pool, tracked in the `custom_counters` sidecar under
|
|
22
|
+
`item_use:<slug>` (`{"spent": n}`). Exhausted pools reject with
|
|
23
|
+
`CastFailed(reason="no_charges_remaining")` before the action budget is touched. Items
|
|
24
|
+
without a pool are unaffected. `CastFailedReason` also gains `invalid_charge_spend`, emitted
|
|
25
|
+
when a `charges_to_spend` request violates the activity's `consumption.scaling` (see
|
|
26
|
+
"Charge upcasting" below).
|
|
27
|
+
- **Item recharge** — `dnd5e_engine.rest.recover_item_uses` (+ `ITEM_USE_COUNTER_PREFIX`,
|
|
28
|
+
submodule-only export): pure recharge over `item_use:` pools. The engine's own
|
|
29
|
+
`rest.RecoveryPeriod` gains `"dawn"`/`"day"`/`"dusk"` (previously just `"sr"`/`"lr"`;
|
|
30
|
+
the data package's `RecoveryRule` already knew `"day"` — this widens the engine's
|
|
31
|
+
narrower rest-period literal to match); formula recovery now rolls dice ("1d6 + 1")
|
|
32
|
+
through an optional `rng` (also available on `recover_feature_uses`).
|
|
33
|
+
- **Cast delegation** — a used item's `CastActivity` now resolves its referenced spell
|
|
34
|
+
(uuid→Spell via `dnd5e-srd-data`'s `get_spell_by_uuid`) on the PC-intent path, honoring
|
|
35
|
+
the item's flat DC / attack / level overrides. `cast_spell_unresolved` now signals a
|
|
36
|
+
genuinely missing uuid, not missing plumbing.
|
|
37
|
+
- **Charge upcasting** — `PlayerIntent.charges_to_spend` casts at
|
|
38
|
+
`base level + extra charges`, validated against `consumption.scaling`
|
|
39
|
+
(`invalid_charge_spend` on violation). New `ActivityResolutionContext.cast_level_override`.
|
|
40
|
+
- **Charge read path** — `LiveCombatView.custom_counters_by_entity` (three-level snapshot
|
|
41
|
+
copy): hosts mirror `item_use:` / `feature_use:` pools per turn the same way they mirror
|
|
42
|
+
`spell_slots_by_entity`.
|
|
43
|
+
|
|
10
44
|
## [0.2.0]
|
|
11
45
|
|
|
12
46
|
Lockstep release with `dnd5e-srd-data` 0.2.0 — the outcome of the gap-closing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dnd5e-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Pure-Python D&D 5e SRD rules engine — combat, checks, effects. Host-agnostic, zero I/O.
|
|
5
5
|
Author: Tapestria contributors
|
|
6
6
|
License: MIT License
|
|
@@ -34,7 +34,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
34
34
|
Classifier: Typing :: Typed
|
|
35
35
|
Requires-Python: >=3.12
|
|
36
36
|
Requires-Dist: d20>=1.1.2
|
|
37
|
-
Requires-Dist: dnd5e-srd-data>=0.
|
|
37
|
+
Requires-Dist: dnd5e-srd-data>=0.3.0
|
|
38
38
|
Requires-Dist: pydantic>=2.10.0
|
|
39
39
|
Provides-Extra: dev
|
|
40
40
|
Requires-Dist: bandit>=1.8.0; extra == 'dev'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dnd5e-engine"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.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"
|
|
@@ -24,7 +24,9 @@ dependencies = [
|
|
|
24
24
|
# lib_loader imports BundledAssetLoader at runtime; the typed-Activity
|
|
25
25
|
# resolver reads the canonical corpus through it. The data lib's >=3.12
|
|
26
26
|
# floor is why this package now also requires >=3.12 (see requires-python).
|
|
27
|
-
|
|
27
|
+
# Lockstep floor: the charge gate reads Item.uses / Spell.foundry_uuid,
|
|
28
|
+
# which srd-data carries from 0.3.0 on.
|
|
29
|
+
"dnd5e-srd-data>=0.3.0",
|
|
28
30
|
]
|
|
29
31
|
|
|
30
32
|
[project.optional-dependencies]
|
|
@@ -46,6 +46,7 @@ from dnd5e_engine.rest import (
|
|
|
46
46
|
RecoveryPeriod,
|
|
47
47
|
RestOutcome,
|
|
48
48
|
recover_feature_uses,
|
|
49
|
+
recover_item_uses,
|
|
49
50
|
resolve_long_rest,
|
|
50
51
|
resolve_short_rest,
|
|
51
52
|
)
|
|
@@ -107,6 +108,7 @@ __all__ = [
|
|
|
107
108
|
"narration_events",
|
|
108
109
|
"parse_cell",
|
|
109
110
|
"recover_feature_uses",
|
|
111
|
+
"recover_item_uses",
|
|
110
112
|
"resolve_check",
|
|
111
113
|
"resolve_long_rest",
|
|
112
114
|
"resolve_short_rest",
|
|
@@ -147,6 +147,7 @@ def build_activity_context(
|
|
|
147
147
|
target_cover: dict[str, str] | None = None,
|
|
148
148
|
scale_values: dict[str, int | str] | None = None,
|
|
149
149
|
class_levels: dict[str, int] | None = None,
|
|
150
|
+
cast_level_override: int | None = None,
|
|
150
151
|
is_feature_invocation: bool = False,
|
|
151
152
|
active_effects: Sequence[ActiveEffect] = (),
|
|
152
153
|
sneak_attack_spent: dict[str, bool] | None = None,
|
|
@@ -185,6 +186,14 @@ def build_activity_context(
|
|
|
185
186
|
So for a feature invocation the override is omitted (``None``), letting the
|
|
186
187
|
save resolver fall through to ``save.dc.calculation``. The spell / item
|
|
187
188
|
path keeps the blanket override.
|
|
189
|
+
|
|
190
|
+
``cast_level_override`` passes straight through to
|
|
191
|
+
:class:`ActivityResolutionContext` — a ``use_item`` charges_to_spend
|
|
192
|
+
upcast's forced delegated-cast level (orchestrator-computed off the
|
|
193
|
+
charge gate's own cost accounting). ``None`` (the default) leaves
|
|
194
|
+
``resolve_cast`` (``activities/cast.py``) to fall back to the wrapper
|
|
195
|
+
activity's own/base spell level, unchanged from before this field
|
|
196
|
+
existed.
|
|
188
197
|
"""
|
|
189
198
|
mod = _caster_mod(caster)
|
|
190
199
|
if caster.entity_type == "Character":
|
|
@@ -346,4 +355,5 @@ def build_activity_context(
|
|
|
346
355
|
spell_book=spell_book,
|
|
347
356
|
scale_values=scale_values or {},
|
|
348
357
|
class_levels=class_levels or {},
|
|
358
|
+
cast_level_override=cast_level_override,
|
|
349
359
|
)
|
|
@@ -26,8 +26,15 @@ honored: 42 canonical cast activities (Dragon Orb DC 18, Circlet of Blasting +5,
|
|
|
26
26
|
…) carry a FIXED save DC or attack bonus. The handler threads the fixed value
|
|
27
27
|
into the child context (``save_dc_override`` / ``attack_bonus_override``); the
|
|
28
28
|
delegated save/attack handler uses it verbatim instead of the wielder's stats.
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
A non-override cast still CLEARS the field for a NESTED cast (a grandchild
|
|
30
|
+
never inherits a parent scroll's DC) — but a TOP-LEVEL item wrapper (nothing
|
|
31
|
+
above it in ``ctx.parent_chain``) has no ``spellcasting_ability`` of its own,
|
|
32
|
+
so clearing unconditionally would strip the blanket flat DC / attack bonus
|
|
33
|
+
``build_activity_context`` computed for the item path, and the delegated
|
|
34
|
+
save/attack handler would either raise (``save.py``, ``calculation ==
|
|
35
|
+
"spellcasting"`` with no ability) or silently drop the ability-mod term
|
|
36
|
+
(``attack.py``). A top-level wrapper without its own override therefore
|
|
37
|
+
FALLS BACK to the inherited ``ctx`` value instead of clearing.
|
|
31
38
|
"""
|
|
32
39
|
|
|
33
40
|
from __future__ import annotations
|
|
@@ -61,7 +68,12 @@ def resolve_cast(activity: CastActivity, ctx: ActivityResolutionContext) -> None
|
|
|
61
68
|
# A cantrip's base level is 0 (``0 <= 0 <= 9`` passes). An out-of-range
|
|
62
69
|
# cast (e.g. a scroll forged at level 10, or below the spell's base) is a
|
|
63
70
|
# LOUD no-op — never an absurd upcast (cure-wounds at level 10 → 20d8).
|
|
64
|
-
|
|
71
|
+
if ctx.cast_level_override is not None:
|
|
72
|
+
cast_level = ctx.cast_level_override
|
|
73
|
+
elif activity.spell.level is not None:
|
|
74
|
+
cast_level = activity.spell.level
|
|
75
|
+
else:
|
|
76
|
+
cast_level = spell.level
|
|
65
77
|
if not (spell.level <= cast_level <= 9):
|
|
66
78
|
_LOGGER.warning(
|
|
67
79
|
"cast_invalid_level uuid=%s cast_level=%s base_level=%s",
|
|
@@ -78,9 +90,13 @@ def resolve_cast(activity: CastActivity, ctx: ActivityResolutionContext) -> None
|
|
|
78
90
|
# A fixed item challenge (``spell.challenge.override``) threads a verbatim
|
|
79
91
|
# save DC / attack bonus into the child so the delegated save/attack handler
|
|
80
92
|
# uses the item's number, not the wielder's stats (Dragon Orb DC 18, Circlet
|
|
81
|
-
# of Blasting +5).
|
|
82
|
-
#
|
|
83
|
-
#
|
|
93
|
+
# of Blasting +5). An override is honored for THIS wrapper. Absent an
|
|
94
|
+
# override: a NESTED cast (``ctx.parent_chain`` non-empty — this wrapper is
|
|
95
|
+
# itself inside another cast's delegation) CLEARS the field so a grandchild
|
|
96
|
+
# never inherits a parent scroll's DC; a TOP-LEVEL wrapper (nothing above us
|
|
97
|
+
# in the chain) has no ``spellcasting_ability`` to fall through to, so it
|
|
98
|
+
# INHERITS ``ctx``'s value — the item-path blanket DC / attack bonus
|
|
99
|
+
# ``build_activity_context`` already computed for it.
|
|
84
100
|
from .resolver import resolve_activity # function-local: breaks the resolver↔cast import cycle
|
|
85
101
|
|
|
86
102
|
challenge = activity.spell.challenge
|
|
@@ -91,8 +107,21 @@ def resolve_cast(activity: CastActivity, ctx: ActivityResolutionContext) -> None
|
|
|
91
107
|
base_spell_level=spell.level,
|
|
92
108
|
concentration=spell.concentration,
|
|
93
109
|
source_passive_effects=spell.passive_effects,
|
|
94
|
-
save_dc_override=(
|
|
95
|
-
|
|
110
|
+
save_dc_override=(
|
|
111
|
+
challenge.save
|
|
112
|
+
if challenge.override
|
|
113
|
+
else (ctx.save_dc_override if not ctx.parent_chain else None)
|
|
114
|
+
),
|
|
115
|
+
attack_bonus_override=(
|
|
116
|
+
challenge.attack
|
|
117
|
+
if challenge.override
|
|
118
|
+
else (ctx.attack_bonus_override if not ctx.parent_chain else None)
|
|
119
|
+
),
|
|
120
|
+
# M1: a nested cast must never inherit the item wrapper's forced cast
|
|
121
|
+
# level (``use_item`` charges_to_spend upcast) — only the wrapper's own
|
|
122
|
+
# ``CastActivity`` consumes it; the referenced spell's own activities
|
|
123
|
+
# cast at ``cast_level`` computed above, not the raw override again.
|
|
124
|
+
cast_level_override=None,
|
|
96
125
|
parent_chain=(*ctx.parent_chain, uuid),
|
|
97
126
|
)
|
|
98
127
|
for child_activity in spell.activities:
|
|
@@ -58,6 +58,11 @@ class ActivityResolutionContext:
|
|
|
58
58
|
# (`spell.challenge.override` + `attack`): when set, the attack handler uses
|
|
59
59
|
# this verbatim to-hit instead of ability + proficiency + weapon. None otherwise.
|
|
60
60
|
attack_bonus_override: int | None = None
|
|
61
|
+
# Cast level forced by a variable-charge item invocation (wand upcast):
|
|
62
|
+
# ``base activity level + extra charges``. When set, resolve_cast uses it
|
|
63
|
+
# verbatim (bounds-checked) instead of the activity/spell level. None for
|
|
64
|
+
# every non-item cast.
|
|
65
|
+
cast_level_override: int | None = None
|
|
61
66
|
# Player-supplied damage-type choice per activity id, for parts that offer a
|
|
62
67
|
# CHOICE of damage type (``DamagePartBlock.types`` with >1 entry — e.g.
|
|
63
68
|
# Chromatic Orb's [acid, cold, fire, ...]). Keyed by activity id; the chosen
|
|
@@ -84,6 +84,13 @@ CastFailedReason = Literal[
|
|
|
84
84
|
# rest. Mirrors the ``no_action_economy`` reject shape, extended from a
|
|
85
85
|
# per-turn budget to a per-encounter / per-rest one.
|
|
86
86
|
"no_uses_remaining",
|
|
87
|
+
# SRD 5.2 §Charges — a charged item invoked with fewer charges remaining
|
|
88
|
+
# than the activity's cost (``consumption.targets[type=itemUses]``).
|
|
89
|
+
"no_charges_remaining",
|
|
90
|
+
# A variable-charge invocation that violates the activity's
|
|
91
|
+
# ``consumption.scaling`` contract (not allowed, below base cost, or
|
|
92
|
+
# above the evaluated max). Emitted by the use_item charge gate.
|
|
93
|
+
"invalid_charge_spend",
|
|
87
94
|
]
|
|
88
95
|
|
|
89
96
|
IntentType = Literal[
|