Allow player to be pushed off moving blocks
This commit is contained in:
@@ -166,6 +166,7 @@ position (so a moving trap's sensors follow it):
|
|||||||
| `{ dir: left\|right\|above\|below }` | player is on that side |
|
| `{ dir: left\|right\|above\|below }` | player is on that side |
|
||||||
| `{ dir: …, range: N }` | …and within N tiles that way |
|
| `{ 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: …, 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 |
|
| `{ timer: { interval: A, up_time: B } }` | cyclic: off A seconds, on B seconds |
|
||||||
| `{ all: [ … ] }` | every listed condition (AND) |
|
| `{ all: [ … ] }` | every listed condition (AND) |
|
||||||
| `{ any: [ … ] }` | any listed condition (OR) |
|
| `{ 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
|
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
|
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
|
ceiling, or driven sideways into a wall by a block hitting you from the side.
|
||||||
freezes, so the offending block stops on the spot until you respawn.
|
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
|
### Mounting traps on other traps
|
||||||
|
|
||||||
|
|||||||
9
SPEC.md
9
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 down onto a floored player (backed below + pinned) → crush.
|
||||||
- Moving up into a player under a ceiling (backed above + pinned) → crush.
|
- Moving up into a player under a ceiling (backed above + pinned) → crush.
|
||||||
- Moving horizontally into a player backed by a wall → 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
|
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`)
|
### 5.7 Death conditions (in `playing`)
|
||||||
- Battery ≤ 0.
|
- 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: left\|right\|above\|below }` | player is on that side |
|
||||||
| `{ dir: …, range: N }` | …and within N tiles in that direction |
|
| `{ 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: …, 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 |
|
| `{ timer: { interval: A, up_time: B } }` | cyclic: off for A s, then on for B s, repeating |
|
||||||
| `{ all: [ … ] }` | AND of sub-conditions |
|
| `{ all: [ … ] }` | AND of sub-conditions |
|
||||||
| `{ any: [ … ] }` | OR of sub-conditions |
|
| `{ any: [ … ] }` | OR of sub-conditions |
|
||||||
|
|||||||
@@ -127,14 +127,12 @@ class Player:
|
|||||||
if dx < 0 and bl and r.colliderect(right): # pushed left into a wall
|
if dx < 0 and bl and r.colliderect(right): # pushed left into a wall
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Riding a platform that carries us *sideways* into a wall: the pushing
|
# A platform that carries us *sideways* into a wall is NOT a crush: the
|
||||||
# block is under our feet, so the side-probes above miss it — use the
|
# carrier is under our feet, perpendicular to the wall, so it can't pinch
|
||||||
# carry direction instead. (Vertical carry crushes are caught above.)
|
# us against it. The X pass simply stops us at the wall edge while the
|
||||||
cdx, _ = self.carry
|
# platform keeps sliding underneath. (A genuine horizontal crush — a
|
||||||
if cdx > 0 and br:
|
# mover closing on our side against a backing wall — is the dx cases
|
||||||
return True
|
# above. Vertical carry crushes are the dy cases above.)
|
||||||
if cdx < 0 and bl:
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _ride_platforms(self):
|
def _ride_platforms(self):
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class _Directional:
|
|||||||
if self.aligned and not (p.bottom > r.top and p.top < r.bottom):
|
if self.aligned and not (p.bottom > r.top and p.top < r.bottom):
|
||||||
return False
|
return False
|
||||||
if d == "left":
|
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:
|
if p.centerx >= ref:
|
||||||
return False
|
return False
|
||||||
dist = ref - p.centerx
|
dist = ref - p.centerx
|
||||||
|
|||||||
@@ -113,6 +113,46 @@ traps:
|
|||||||
assert g.player.rect.x < start_x # got shoved along
|
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):
|
def test_corner_clip_does_not_warp(make_game):
|
||||||
# Walking into a moving block near its corner must not fling the player
|
# Walking into a moving block near its corner must not fling the player
|
||||||
# across it (the old velocity-sign resolver bug).
|
# across it (the old velocity-sign resolver bug).
|
||||||
|
|||||||
@@ -41,6 +41,21 @@ def test_dir_aligned():
|
|||||||
assert ev({"dir": "above"}, 400, 60) is True
|
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():
|
def test_all_and_any():
|
||||||
c = {"all": [{"within": 3}, {"dir": "above", "aligned": True}]}
|
c = {"all": [{"within": 3}, {"dir": "above", "aligned": True}]}
|
||||||
assert ev(c, 108, 80) is True
|
assert ev(c, 108, 80) is True
|
||||||
|
|||||||
Reference in New Issue
Block a user