Make phase a block property rather than a distinct type

This commit is contained in:
James Campbell
2026-08-02 00:03:58 -04:00
parent a17a68a07e
commit 7b7b005823
5 changed files with 515 additions and 201 deletions

View File

@@ -109,3 +109,121 @@ traps:
armed = i
break
assert armed is not None and 13 <= armed <= 17 # ~0.25s = 15 frames
# --- per-target triggers/delays (a block can move AND phase) -----------------
def _block(make_level, body):
lvl = make_level(
"name: t\ntile_size: 32\nmap: |\n ######\n #P..G#\n ######\ntraps:\n" + body
)
return lvl.traps[0]
def _cond(block, target):
return type(block._trig[target]["cond"]).__name__
def test_trigger_single_condition_applies_to_all_targets(make_level):
# A bare condition (not a target map) drives both motion and phase.
b = _block(
make_level, " - type: block\n at: [2, 1]\n trigger: { within: 2 }\n"
)
assert _cond(b, "motion") == "_Within"
assert _cond(b, "phase") == "_Within"
def test_trigger_all_any_is_a_single_condition(make_level):
# `all`/`any` are condition keywords, not target names -> one condition.
b = _block(
make_level,
" - type: block\n at: [2, 1]\n"
" trigger: { any: [ { within: 2 }, { dir: left } ] }\n",
)
assert _cond(b, "motion") == "_Any" and _cond(b, "phase") == "_Any"
def test_trigger_per_target_map_with_default(make_level):
# `default` covers unnamed targets; a named target overrides it.
b = _block(
make_level,
" - type: block\n at: [2, 1]\n phase: true\n"
" trigger:\n default: always\n motion: { within: 5 }\n",
)
assert _cond(b, "motion") == "_Within" # named
assert _cond(b, "phase") == "_Always" # from default
def test_trigger_unnamed_target_off_without_default(make_level):
# A map naming only motion (no default) leaves phase with no trigger -> off.
b = _block(
make_level,
" - type: block\n at: [2, 1]\n phase: true\n"
" trigger: { motion: always }\n",
)
assert "phase" in b._trig_never
assert _cond(b, "phase") == "_Never" and _cond(b, "motion") == "_Always"
def test_trigger_unknown_target_raises(make_level):
import pytest
with pytest.raises(ValueError):
_block(
make_level,
" - type: block\n at: [2, 1]\n"
" trigger: { motion: always, bogus: always }\n",
)
def test_delay_scalar_applies_to_all_targets(make_level):
b = _block(make_level, " - type: block\n at: [2, 1]\n delay: 0.2\n")
assert b._trig["motion"]["delay"] == 0.2 and b._trig["phase"]["delay"] == 0.2
def test_delay_per_target_map(make_level):
b = _block(
make_level,
" - type: block\n at: [2, 1]\n delay: { motion: 0.1, phase: 0.3 }\n",
)
assert b._trig["motion"]["delay"] == 0.1 and b._trig["phase"]["delay"] == 0.3
# unnamed-without-default delay falls back to 0 (no arm delay)
b2 = _block(
make_level, " - type: block\n at: [2, 1]\n delay: { motion: 0.1 }\n"
)
assert b2._trig["phase"]["delay"] == 0.0
def test_motion_and_phase_independent_triggers(make_level):
# The user's case: a phase block that's *always* moving but only materialises
# when the player is near. Motion and phase read their own triggers.
lvl = make_level("""
name: t
tile_size: 32
map: |
############
#..........#
#P........G#
############
traps:
- type: block
at: [4, 1]
phase: true
move: [3, 0]
speed: 200
fade: 0.2
trigger:
motion: always
phase: { within: 2 }
""")
b = lvl.traps[0]
b.reset()
far = FakeGame(pygame.Rect(1 * 32, 1 * 32, 20, 28)) # player at the far end
x0 = b._rect().x
for _ in range(20):
b.tick(1 / 60, far)
assert b._rect().x != x0 # moved (motion: always) ...
assert b.alpha == 0.0 # ... but hasn't materialised (player far)
near = FakeGame(pygame.Rect(b._rect().x + 2, 1 * 32, 20, 28))
for _ in range(10):
b.tick(1 / 60, near)
assert b.alpha > 0.0 # phases in once the player is close