diff --git a/README.md b/README.md index 4c7b1c1..bdd4e7e 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,7 @@ position (so a moving trap's sensors follow it): | `{ dir: left\|right\|above\|below }` | player is on that side | | `{ dir: …, range: N }` | …and within N tiles that way | | `{ dir: …, aligned: true }` | …and overlapping on the perpendicular axis (i.e. *directly* left / *directly* above) | +| `{ dir: …, inclusive: true }` | …also count the trap's own tile as being on that side (default `false`: the player must be strictly past the near edge) | | `{ timer: { interval: A, up_time: B } }` | cyclic: off A seconds, on B seconds | | `{ all: [ … ] }` | every listed condition (AND) | | `{ any: [ … ] }` | any listed condition (OR) | @@ -186,8 +187,11 @@ countdown. Handy for "linger and it strikes" spikes. You also die if a moving (non-deadly) `block` **pinches** you: presses you against a solid surface — squished into the ground from above, carried up into a -ceiling, or ridden sideways into a wall. During the death pause the whole scene -freezes, so the offending block stops on the spot until you respawn. +ceiling, or driven sideways into a wall by a block hitting you from the side. +Riding a platform horizontally into a wall is *not* a crush, though: you simply +stop at the wall while the platform slides on under your feet. During the death +pause the whole scene freezes, so the offending block stops on the spot until +you respawn. ### Mounting traps on other traps diff --git a/SPEC.md b/SPEC.md index b3fcd09..af5310f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -152,9 +152,13 @@ resolution), and whether a moving carrier is closing in on the appropriate side: - Moving down onto a floored player (backed below + pinned) → crush. - Moving up into a player under a ceiling (backed above + pinned) → crush. - Moving horizontally into a player backed by a wall → crush. -- Being carried sideways into a wall (via the `carry` direction) → crush. +- Being carried *up* into a ceiling while riding a platform → crush (the rising + carrier under the feet is caught by the "moving up" case above). A block that stops with the player fitting underneath does **not** crush -(no residual overlap → not pinned). +(no residual overlap → not pinned). Riding a platform *horizontally* into a wall +is **not** a crush: the carrier is under the feet, perpendicular to the wall, so +it can't pin the player against it — the X pass just stops them at the wall edge +while the platform slides on underneath. ### 5.7 Death conditions (in `playing`) - Battery ≤ 0. @@ -326,6 +330,7 @@ evaluated against the player each frame relative to the trap's `sensor_rect()`. | `{ dir: left\|right\|above\|below }` | player is on that side | | `{ dir: …, range: N }` | …and within N tiles in that direction | | `{ dir: …, aligned: true }` | …and overlapping on the perpendicular axis (directly left / directly above, etc.) | +| `{ dir: …, inclusive: true }` | …count the trap's own tile as being on that side (default false: the player must be strictly past the near edge) | | `{ timer: { interval: A, up_time: B } }` | cyclic: off for A s, then on for B s, repeating | | `{ all: [ … ] }` | AND of sub-conditions | | `{ any: [ … ] }` | OR of sub-conditions | diff --git a/game/player.py b/game/player.py index 6a9e748..114a0fc 100644 --- a/game/player.py +++ b/game/player.py @@ -127,14 +127,12 @@ class Player: if dx < 0 and bl and r.colliderect(right): # pushed left into a wall return True - # Riding a platform that carries us *sideways* into a wall: the pushing - # block is under our feet, so the side-probes above miss it — use the - # carry direction instead. (Vertical carry crushes are caught above.) - cdx, _ = self.carry - if cdx > 0 and br: - return True - if cdx < 0 and bl: - return True + # A platform that carries us *sideways* into a wall is NOT a crush: the + # carrier is under our feet, perpendicular to the wall, so it can't pinch + # us against it. The X pass simply stops us at the wall edge while the + # platform keeps sliding underneath. (A genuine horizontal crush — a + # mover closing on our side against a backing wall — is the dx cases + # above. Vertical carry crushes are the dy cases above.) return False def _ride_platforms(self): diff --git a/game/traps.py b/game/traps.py index 55fdcba..088746d 100644 --- a/game/traps.py +++ b/game/traps.py @@ -83,7 +83,7 @@ class _Directional: if self.aligned and not (p.bottom > r.top and p.top < r.bottom): return False if d == "left": - ref = r.right if self.inclusive else r.right + ref = r.right if self.inclusive else r.left if p.centerx >= ref: return False dist = ref - p.centerx diff --git a/tests/test_crush.py b/tests/test_crush.py index 6e11d99..e948b8f 100644 --- a/tests/test_crush.py +++ b/tests/test_crush.py @@ -113,6 +113,46 @@ traps: assert g.player.rect.x < start_x # got shoved along +def test_ride_into_wall_stops_no_crush(make_game): + # Riding a horizontally-moving platform into a stationary wall must NOT + # crush the player: they stop at the wall while the block slides underneath. + # The wall sits one row *above* the platform's track (col 8, row 2), so it + # catches the player's body while the platform passes below it. + g = make_game(""" +name: t +tile_size: 32 +map: | + ########### + #.........# + #.......#.# + #.........# + #........G# + ########### +traps: + - type: block + at: [1, 3] + move: [8, 0] + speed: 200 + trigger: always +""") + # stand the player on top of the platform at its left end (body in row 2) + place(g, 1, 2) + g.player.fy = float(3 * 32 - g.player.h) # feet on the block's top + g.player._sync_rect() + wall_left = 8 * 32 + hit_wall = False + for _ in range(180): + step(g) + if g.state != "playing": + break + assert not g.player.crushed # never crushed + assert g.player.rect.right <= wall_left + 1 # stopped at the wall + if g.player.rect.right >= wall_left - 1: + hit_wall = True + assert g.state == "playing" # survived the whole time + assert hit_wall # actually reached the wall + + def test_corner_clip_does_not_warp(make_game): # Walking into a moving block near its corner must not fling the player # across it (the old velocity-sign resolver bug). diff --git a/tests/test_triggers.py b/tests/test_triggers.py index c1868e9..8634bcb 100644 --- a/tests/test_triggers.py +++ b/tests/test_triggers.py @@ -41,6 +41,21 @@ def test_dir_aligned(): assert ev({"dir": "above"}, 400, 60) is True +def test_dir_inclusive(): + # trap column spans x 100..132 (centre 116). A player standing *inside* the + # column (centre 116) is neither strictly left nor strictly right of it. + assert ev({"dir": "left"}, 106, 105) is False # centre 116, inside -> not left + assert ev({"dir": "right"}, 106, 105) is False # inside -> not right + # inclusive counts the trap's own tile as being on that side + assert ev({"dir": "left", "inclusive": True}, 106, 105) is True + assert ev({"dir": "right", "inclusive": True}, 106, 105) is True + # and vertically, too (column spans y 100..132) + assert ev({"dir": "above"}, 108, 106) is False + assert ev({"dir": "above", "inclusive": True}, 108, 106) is True + assert ev({"dir": "below"}, 108, 106) is False + assert ev({"dir": "below", "inclusive": True}, 108, 106) is True + + def test_all_and_any(): c = {"all": [{"within": 3}, {"dir": "above", "aligned": True}]} assert ev(c, 108, 80) is True