Skip to content

Fix the magic shield crash and report a melee with nothing left to strike - #222

Merged
mmacy merged 5 commits into
mainfrom
chunk/playtest-fixes
Oct 31, 2026
Merged

mmacy merged 5 commits into
mainfrom
chunk/playtest-fixes

Conversation

@mmacy

@mmacy mmacy commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Two defects that The Snoring King found while playtesting the 2.3.0 build:

  • A crash with a magic shield. When an attack ignores the defender's shield (against a retreating defender, or from behind), the kernel read shield.template, which a magic shield doesn't have. So a monster attacking a retreating member with a magic shield raised AttributeError. This was already in 2.2.0. The shield's bonus is now worked out by one helper that Character armour class and the kernel share, and the whole bonus is ignored, including the enchantment. A cursed shield's penalty still counts, because the OSE SRD negates a shield's bonus, and a cursed shield has none.
  • A silent melee. When the archers or a caster killed a group earlier in the round, the members who declared melee on it lost their attacks with no event. They now get battle.melee.fizzled with reason battle.declaration.no_target. A member engaged with a group that runs out of the battle now fizzles with combat.attack.out_of_reach too.

phase4_delve.json gains two such fizzle events just before the victory, and nothing else in it changes. No other golden changes.

https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD

mmacy added 5 commits October 31, 2026 01:50
When an attack ignores the defender's shield, because the defender is retreating or the attacker strikes from behind, the attack is made against the armour class the defender would have without the shield. For a magic shield, that takes away its enchantment along with the +1 every shield gives. A cursed shield that makes armour class worse still counts, because the rule negates a shield's bonus and not its penalty. Today a magic shield in that case raises AttributeError, because a MagicItemInstance has no template.

When a member's declared melee attack comes up and its group has no living, visible monster left to strike, the attack fizzles with a DeclarationFizzledEvent at battle.melee.fizzled, whose reason is battle.declaration.no_target. The session checks this before the member's position, so it applies whether or not the member closed. A slow attack gets the same check at the end of the round. Today such an attack vanishes with no event.

The new tests that fail today are marked xfail for the chunk playtest-fixes. The docstrings and the adaptations register state both rules.

Claude-Session: https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD
An attack on a retreating defender, or an attack from behind, raised AttributeError when the defender carried a magic shield, because the attack roll read the shield's template and a MagicItemInstance has none. The attack roll now reads the shield through a new private helper in items.py, _shield_ac_parts, which Character._armour_parts uses too, so the two can't disagree about what a shield gives.

The attack is made against the armour class the defender would have without the shield. A magic shield loses its enchantment along with the +1 every shield gives. A cursed shield that makes armour class worse still counts, because only a positive bonus is taken away.

Claude-Session: https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD
When a member's declared melee attack comes up and its group has no living monster the member can see, the attack now fizzles with a DeclarationFizzledEvent at battle.melee.fizzled, whose reason is battle.declaration.no_target. Before, the attack vanished with no event. _melee_fizzle checks for a target before it checks the member's position, so the member gets this reason whether or not they closed. A slow attack gets the same check at the end of the round, because it goes through the same function.

A member who stood within 5' of a group that ran out of the battle in one move also got no event. The engaged member passed the fizzle check, and then _resolve_party_attack returned nothing because the group had fled. That attack now fizzles with combat.attack.out_of_reach. A new guard test in test_melee_no_target.py covers it.

The fizzle message now says the member finds nothing left to strike when the reason is no_target.

The golden file phase4_delve.json changes, regenerated with tests/generate_phase4_goldens.py from the same seed. One of its battles has two members whose melee attacks are aimed at a group that is already dead when their turn comes. The only change is the two new fizzle events for those members, and their two message lines. No other golden changes.

Claude-Session: https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD
The tests for the ignored magic shield and the melee with nothing left to strike pass, so their expected-to-fail markers come off. The register's entry for a group that runs now also says that a group that runs out of the battle leaves nothing to strike, so even a member who stood within 5' of it fizzles with combat.attack.out_of_reach.

Claude-Session: https://claude.ai/code/session_01V7ErtnsmhaP7aLwcfmzohD
@mmacy
mmacy merged commit 5c9cb3e into main Oct 31, 2026
5 checks passed
@mmacy
mmacy deleted the chunk/playtest-fixes branch October 31, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant