Make phase a block property rather than a distinct type
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
"""Spike, arrow_shooter, warp, phase_block + the invisible flag & block arrays."""
|
||||
"""Spike, arrow_shooter, warp, phase blocks + the invisible flag & block arrays."""
|
||||
|
||||
import hashlib
|
||||
import pygame
|
||||
from conftest import FakeGame, step, run, place, hold, DT
|
||||
from game.assets import AssetStore
|
||||
from game.traps import Spike, ArrowShooter, Warp, PhaseBlock, Block
|
||||
from game.traps import Spike, ArrowShooter, Warp, Block
|
||||
|
||||
|
||||
# --- spike -------------------------------------------------------------------
|
||||
@@ -211,17 +211,18 @@ map: |
|
||||
#P......G#
|
||||
##########
|
||||
traps:
|
||||
- type: phase_block
|
||||
- type: block
|
||||
at: [4, 3]
|
||||
phase: true
|
||||
fade: 0.2
|
||||
trigger: { within: 2 }
|
||||
""")
|
||||
pb = g.level.traps[0]
|
||||
place(g, 1, 3)
|
||||
step(g)
|
||||
assert not pb.solid and pb.alpha == 0 # dormant far away
|
||||
assert not pb.materialized and pb.alpha == 0 # dormant far away
|
||||
run(g, 40, lambda i: hold(right=True))
|
||||
assert pb.solid and pb.alpha > 0 # phased in solid on approach
|
||||
assert pb.materialized and pb.alpha > 0 # phased in solid on approach
|
||||
|
||||
|
||||
def test_phase_block_kills_if_inside_when_it_forms(make_game):
|
||||
@@ -234,8 +235,9 @@ map: |
|
||||
#.....G#
|
||||
########
|
||||
traps:
|
||||
- type: phase_block
|
||||
- type: block
|
||||
at: [3, 2]
|
||||
phase: true
|
||||
fade: 0.2
|
||||
trigger: { within: 3 }
|
||||
""")
|
||||
@@ -261,8 +263,9 @@ map: |
|
||||
#......G#
|
||||
#########
|
||||
traps:
|
||||
- type: phase_block
|
||||
- type: block
|
||||
at: [4, 1]
|
||||
phase: true
|
||||
fade: 0.2
|
||||
trigger: { within: 5 }
|
||||
""")
|
||||
@@ -278,7 +281,7 @@ traps:
|
||||
# survived, and pushed out to the left so it no longer overlaps the cell
|
||||
assert g.deaths == d0
|
||||
assert g.player.rect.right <= b.left
|
||||
assert pb.solid and not pb.emerge_kill
|
||||
assert pb.materialized and not pb.emerge_kill
|
||||
|
||||
|
||||
def test_phase_block_kills_when_shove_would_squish(make_game):
|
||||
@@ -292,8 +295,9 @@ map: |
|
||||
#.....G#
|
||||
########
|
||||
traps:
|
||||
- type: phase_block
|
||||
- type: block
|
||||
at: [1, 1]
|
||||
phase: true
|
||||
fade: 0.2
|
||||
trigger: { within: 5 }
|
||||
""")
|
||||
@@ -326,8 +330,9 @@ map: |
|
||||
#.....G#
|
||||
########
|
||||
traps:
|
||||
- type: phase_block
|
||||
- type: block
|
||||
at: [3, 2]
|
||||
phase: true
|
||||
fade: 0.5
|
||||
trigger: { within: 3 }
|
||||
""")
|
||||
@@ -337,7 +342,44 @@ traps:
|
||||
step(g)
|
||||
assert 0 < pb.alpha < 1
|
||||
g._start_death() # die from something
|
||||
assert pb.alpha == 1.0 and pb.solid # snapped fully visible for the freeze
|
||||
assert pb.alpha == 1.0 and pb.materialized # snapped fully visible for the freeze
|
||||
|
||||
|
||||
def test_deadly_phase_block_harmless_until_materialized(make_game):
|
||||
# A deadly phase block is neither solid nor a hazard while dormant, then
|
||||
# becomes a (never-solid) hazard once it materialises.
|
||||
g = make_game("""
|
||||
name: t
|
||||
tile_size: 32
|
||||
map: |
|
||||
##########
|
||||
#........#
|
||||
#........#
|
||||
#P......G#
|
||||
##########
|
||||
traps:
|
||||
- type: block
|
||||
at: [4, 2]
|
||||
phase: true
|
||||
deadly: true
|
||||
fade: 0.15
|
||||
trigger: { within: 3 }
|
||||
""")
|
||||
b = g.level.traps[0]
|
||||
place(g, 1, 3) # far away -> dormant
|
||||
step(g)
|
||||
assert not b.solid_rects() and not b.hazard_rects()
|
||||
# Materialise it by standing on the cell; a deadly block never becomes solid.
|
||||
place(g, 4, 2)
|
||||
d0 = g.deaths
|
||||
killed = False
|
||||
for _ in range(12):
|
||||
step(g)
|
||||
if g.deaths > d0:
|
||||
killed = True
|
||||
break
|
||||
assert killed # the materialised hazard kills
|
||||
assert not b.solid_rects() # deadly -> never solid, even materialised
|
||||
|
||||
|
||||
def test_debug_overscan_reveals_offmap_trap(make_level):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user