diff --git a/game/sim/physics.py b/game/sim/physics.py new file mode 100644 index 0000000..78e59f1 --- /dev/null +++ b/game/sim/physics.py @@ -0,0 +1,212 @@ +import math + +from ..constants import tileSize +from .geometry import Aabb + +slopeStick = 6.0 +groundProbe = 2.0 + + +def bodyAabb(actor): + return Aabb(actor.x, actor.y, actor.w, actor.h) + + +def cellRange(box): + leftCol = int(math.floor(box.left / tileSize)) + rightCol = int(math.floor((box.right - 1e-6) / tileSize)) + topRow = int(math.floor(box.top / tileSize)) + bottomRow = int(math.floor((box.bottom - 1e-6) / tileSize)) + return leftCol, rightCol, topRow, bottomRow + + +def solidTilesOverlapping(terrain, box): + leftCol, rightCol, topRow, bottomRow = cellRange(box) + hits = [] + for row in range(topRow, bottomRow + 1): + for col in range(leftCol, rightCol + 1): + if terrain.solidAt(col, row): + hits.append(terrain.rectFor(col, row)) + return hits + + +def oneWayTilesOverlapping(terrain, box): + leftCol, rightCol, topRow, bottomRow = cellRange(box) + hits = [] + for row in range(topRow, bottomRow + 1): + for col in range(leftCol, rightCol + 1): + if terrain.oneWayAt(col, row): + hits.append(terrain.rectFor(col, row)) + return hits + + +def slopeSurfaceY(col, row, sampleX, kind): + tileLeft = col * tileSize + tileTop = row * tileSize + localX = sampleX - tileLeft + if localX < 0.0: + localX = 0.0 + elif localX > tileSize: + localX = float(tileSize) + if kind == "ascendRight": + return tileTop + (tileSize - localX) + return tileTop + localX + + +def moveHorizontal(actor, terrain, platforms): + actor.x += actor.vx + box = bodyAabb(actor) + actor.onWallLeft = False + actor.onWallRight = False + for tileBox in solidTilesOverlapping(terrain, box): + if box.intersects(tileBox): + if actor.vx > 0: + actor.x = tileBox.left - actor.w + actor.onWallRight = True + elif actor.vx < 0: + actor.x = tileBox.right + actor.onWallLeft = True + actor.vx = 0.0 + box = bodyAabb(actor) + for platform in platforms: + if platform.oneWay: + continue + platformBox = platform.aabb + if box.intersects(platformBox): + if actor.vx > 0: + actor.x = platformBox.left - actor.w + actor.onWallRight = True + elif actor.vx < 0: + actor.x = platformBox.right + actor.onWallLeft = True + actor.vx = 0.0 + box = bodyAabb(actor) + + +def moveVertical(actor, terrain, platforms, dropThrough=False): + prevBottom = actor.y + actor.h + actor.y += actor.vy + box = bodyAabb(actor) + actor.onGround = False + actor.onCeiling = False + actor.groundPlatform = None + + for tileBox in solidTilesOverlapping(terrain, box): + if box.intersects(tileBox): + if actor.vy > 0: + actor.y = tileBox.top - actor.h + actor.onGround = True + elif actor.vy < 0: + actor.y = tileBox.bottom + actor.onCeiling = True + actor.vy = 0.0 + box = bodyAabb(actor) + + if actor.vy >= 0 and not dropThrough: + for tileBox in oneWayTilesOverlapping(terrain, box): + top = tileBox.top + if prevBottom <= top + 1.0 and box.bottom >= top and box.right > tileBox.left and box.left < tileBox.right: + actor.y = top - actor.h + actor.vy = 0.0 + actor.onGround = True + box = bodyAabb(actor) + + for platform in platforms: + platformBox = platform.aabb + if platform.oneWay: + if actor.vy >= 0 and not dropThrough: + top = platformBox.top + horizontal = box.right > platformBox.left and box.left < platformBox.right + if prevBottom <= top + 1.0 and box.bottom >= top and horizontal: + actor.y = top - actor.h + actor.vy = 0.0 + actor.onGround = True + actor.groundPlatform = platform + box = bodyAabb(actor) + else: + if box.intersects(platformBox): + if actor.vy > 0: + actor.y = platformBox.top - actor.h + actor.onGround = True + actor.groundPlatform = platform + elif actor.vy < 0: + actor.y = platformBox.bottom + actor.onCeiling = True + actor.vy = 0.0 + box = bodyAabb(actor) + + +def resolveSlope(actor, terrain, wasOnGround): + box = bodyAabb(actor) + sampleX = box.centerX + col = int(math.floor(sampleX / tileSize)) + topRow = int(math.floor((box.top - tileSize) / tileSize)) + bottomRow = int(math.floor((box.bottom + slopeStick) / tileSize)) + for row in range(topRow, bottomRow + 1): + kind = terrain.slopeAt(col, row) + if not kind: + continue + surfaceY = slopeSurfaceY(col, row, sampleX, kind) + if surfaceY <= box.top: + continue + gap = surfaceY - box.bottom + penetrating = gap <= 0.0 and gap >= -float(tileSize) - slopeStick + clinging = wasOnGround and 0.0 < gap <= slopeStick + if actor.vy >= 0.0 and (penetrating or clinging): + actor.y = surfaceY - actor.h + actor.vy = 0.0 + actor.onGround = True + return True + return False + + +def touchingSolidSide(actor, terrain, platforms, direction): + probe = Aabb( + actor.x + (actor.w if direction > 0 else -groundProbe), + actor.y + 2.0, + groundProbe, + actor.h - 4.0, + ) + for tileBox in solidTilesOverlapping(terrain, probe): + if probe.intersects(tileBox): + return True + for platform in platforms: + if platform.oneWay: + continue + if probe.intersects(platform.aabb): + return True + return False + + +def hasGroundBelow(actor, terrain, platforms): + probe = Aabb(actor.x + 1.0, actor.y + actor.h, actor.w - 2.0, groundProbe) + for tileBox in solidTilesOverlapping(terrain, probe): + if probe.intersects(tileBox): + return True + for tileBox in oneWayTilesOverlapping(terrain, probe): + if probe.intersects(tileBox): + return True + for platform in platforms: + if probe.intersects(platform.aabb): + return True + return False + + +def overlapsLadder(actor, terrain): + box = bodyAabb(actor) + leftCol, rightCol, topRow, bottomRow = cellRange(box) + for row in range(topRow, bottomRow + 1): + for col in range(leftCol, rightCol + 1): + if terrain.ladderAt(col, row): + return True + return False + + +def hazardOverlap(actor, terrain): + box = bodyAabb(actor).inflated(-1.0, -1.0) + leftCol, rightCol, topRow, bottomRow = cellRange(box) + worstDamage = 0 + for row in range(topRow, bottomRow + 1): + for col in range(leftCol, rightCol + 1): + if terrain.hazardAt(col, row): + worstDamage = max(worstDamage, terrain.hazardDamageAt(col, row)) + return worstDamage