~/bend-docscommunity

physics.bend source

physics.bend on the hub · documented module

import Base# physics.bend — pure player physics for the Minecraft clone.# No world import here (avoids a circular import). Collision sampling# against the voxel grid lives in world/game modules; this file provides# pure math + per-axis resolve helpers that game.bend loops with lookups.type Vec3 is Data:  V{x: F32, y: F32, z: F32}def Vec3.zero() -> Vec3:  V{0.0, 0.0, 0.0}def Vec3.add(a: Vec3, b: Vec3) -> Vec3:  match a b:    case V{ax, ay, az} V{bx, by, bz}:      V{(ax + bx : F32), (ay + by : F32), (az + bz : F32)}def Vec3.scale(+s: F32, v: Vec3) -> Vec3:  match v:    case V{x, y, z}:      V{(s * x : F32), (s * y : F32), (s * z : F32)}def Vec3.is_eq(a: Vec3, b: Vec3) -> Bool:  match a b:    case V{ax, ay, az} V{bx, by, bz}:      Bool.and(Bool.and(F32.is_eq(ax, bx), F32.is_eq(ay, by)), F32.is_eq(az, bz))type Player is Data:  P{pos: Vec3, vel: Vec3, yaw: F32, pitch: F32, on_ground: Bool, fly: Bool}def Player.spawn() -> Player:  P{V{32.5, 20.0, 32.5}, V{0.0, 0.0, 0.0}, 0.0, 0.0, False{}, False{}}# Movement constants (defs returning F32, single source of truth).def Physics.gravity() -> F32:  28.0def Physics.jump_vel() -> F32:  9.0def Physics.walk_speed() -> F32:  5.0def Physics.fly_speed() -> F32:  8.0def Physics.terminal() -> F32:  F32.neg(30.0)def Physics.epsilon() -> F32:  0.001# Clamp falling speed to terminal velocity.def Physics.clamp_fall(vy: F32) -> F32:  F32.max(vy, Physics.terminal())# Clamp one axis into [lo, hi].def Physics.clamp_axis(v: F32, lo: F32, hi: F32) -> F32:  F32.clamp(v, lo, hi)# Solid check (duplicated small on purpose, no world import):# blocked = not (b == air(0) or b == water(7)).def Physics.blocked(+block: U32) -> Bool:  Bool.not(Bool.or(U32.is_eq(block, 0), U32.is_eq(block, 7)))# True if any block id in the nearby list is solid.# Only recursive def over List here; solid is first and shrinks via tail.def Physics.any_blocked(solid: List<U32>) -> Bool:  match solid:    case Nil{}:      False{}    case h <> t:      Bool.or(Physics.blocked(h), Physics.any_blocked(t))# Vertical velocity after gravity over dt. Falls only when airborne and# not flying; otherwise unchanged. Clamps to terminal.def Physics.fall_vy(vy: F32, dt: F32, fly: Bool, on_ground: Bool) -> F32:  match fly on_ground:    case False{} False{}:      Physics.clamp_fall((vy - (Physics.gravity() * dt : F32) : F32))    case False{} True{}:      vy    case True{} False{}:      vy    case True{} True{}:      vy# Jump: grounded + pressed sets upward velocity, else keeps vy.def Physics.jump_vy(vy: F32, on_ground: Bool, jump: Bool) -> F32:  match on_ground jump:    case True{} True{}:      Physics.jump_vel()    case True{} False{}:      vy    case False{} True{}:      vy    case False{} False{}:      vy# Friction/drag factor over dt: strong on ground, mild in air.def Physics.fric_factor(dt: F32, on_ground: Bool) -> F32:  match on_ground:    case False{}:      (1.0 - (0.5 * dt : F32) : F32)    case True{}:      F32.max((1.0 - (10.0 * dt : F32) : F32), 0.0)# Grounded when there is support below and we are not moving up.def Physics.grounded_go(below: Bool, slow: Bool) -> Bool:  match below slow:    case True{} True{}:      True{}    case True{} False{}:      False{}    case False{} True{}:      False{}    case False{} False{}:      False{}# Single-axis pure shifts.def Physics.shift_x(pos: Vec3, delta: F32) -> Vec3:  match pos:    case V{x, y, z}:      V{(x + delta : F32), y, z}def Physics.shift_y(pos: Vec3, delta: F32) -> Vec3:  match pos:    case V{x, y, z}:      V{x, (y + delta : F32), z}def Physics.shift_z(pos: Vec3, delta: F32) -> Vec3:  match pos:    case V{x, y, z}:      V{x, y, (z + delta : F32)}# Dispatch a pure shift from precomputed axis flags (avoids matching on U32).def Physics.shift_go(pos: Vec3, delta: F32, is_x: Bool, is_y: Bool) -> Vec3:  match is_x is_y:    case True{} _:      Physics.shift_x(pos, delta)    case False{} True{}:      Physics.shift_y(pos, delta)    case False{} False{}:      Physics.shift_z(pos, delta)# Pure AABB overlap test on min/max corners.def Physics.aabb_overlap(a_min: Vec3, a_max: Vec3, b_min: Vec3, b_max: Vec3) -> Bool:  match a_min a_max b_min b_max:    case V{+aminx, +aminy, +aminz} V{+amaxx, +amaxy, +amaxz} V{+bminx, +bminy, +bminz} V{+bmaxx, +bmaxy, +bmaxz}:      Bool.and(Bool.and(F32.is_lt(aminx, bmaxx), F32.is_lt(bminx, amaxx)), Bool.and(Bool.and(F32.is_lt(aminy, bmaxy), F32.is_lt(bminy, amaxy)), Bool.and(F32.is_lt(aminz, bmaxz), F32.is_lt(bminz, amaxz))))# Look helpers (defined before their Player wrappers).def Player.turn_go(pos: Vec3, vel: Vec3, yaw: F32, pitch: F32, og: Bool, fly: Bool, dx: F32, dy: F32) -> Player:  P{pos, vel, (yaw + (dx * 0.003 : F32) : F32), F32.clamp((pitch + (dy * 0.003 : F32) : F32), F32.neg(1.55), 1.55), og, fly}def Player.dir_go(+yaw: F32, +pitch: F32) -> Vec3:  V{(F32.neg(F32.sin(yaw)) * F32.cos(pitch) : F32), F32.sin(pitch), (F32.neg(F32.cos(yaw)) * F32.cos(pitch) : F32)}def Player.eye_go(pos: Vec3) -> Vec3:  match pos:    case V{x, y, z}:      V{x, (y + 1.6 : F32), z}# Pure physics velocity step: gravity when airborne and not flying.def Physics.step_vel(v: Vec3, on_ground: Bool, fly: Bool, dt: F32) -> Vec3:  match v:    case V{vx, vy, vz}:      V{vx, Physics.fall_vy(vy, dt, fly, on_ground), vz}# Gravity only (no ground check here; game.bend supplies fly mode).def Physics.apply_gravity(vel: Vec3, dt: F32, fly: Bool) -> Vec3:  match vel:    case V{vx, vy, vz}:      V{vx, Physics.fall_vy(vy, dt, fly, False{}), vz}# Jump impulse when grounded and pressed.def Physics.apply_jump(vel: Vec3, on_ground: Bool, jump: Bool) -> Vec3:  match vel:    case V{vx, vy, vz}:      V{vx, Physics.jump_vy(vy, on_ground, jump), vz}# Horizontal friction/drag; vertical untouched.def Physics.apply_friction(vel: Vec3, on_ground: Bool, dt: F32) -> Vec3:  match vel on_ground:    case V{vx, vy, vz} False{}:      +k = Physics.fric_factor(dt, False{})      V{(vx * k : F32), vy, (vz * k : F32)}    case V{vx, vy, vz} True{}:      +k = Physics.fric_factor(dt, True{})      V{(vx * k : F32), vy, (vz * k : F32)}# Semi-implicit Euler position step: pos + vel * dt.def Physics.step_pos(pos: Vec3, vel: Vec3, +dt: F32) -> Vec3:  match pos vel:    case V{px, py, pz} V{vx, vy, vz}:      V{(px + (vx * dt : F32) : F32), (py + (vy * dt : F32) : F32), (pz + (vz * dt : F32) : F32)}# Pure add of a delta triple (game.bend does collision around this).def Physics.try_move(pos: Vec3, dx: F32, dy: F32, dz: F32) -> Vec3:  match pos:    case V{x, y, z}:      V{(x + dx : F32), (y + dy : F32), (z + dz : F32)}# Grounded from vertical speed + support below (no world lookup here).def Physics.is_on_ground(vy: F32, below: Bool) -> Bool:  Physics.grounded_go(below, F32.is_le(vy, 0.0))# Pure single-axis shift selected by axis id (0 = x, 1 = y, else z).def Physics.shift_axis(pos: Vec3, delta: F32, +axis: U32) -> Vec3:  Physics.shift_go(pos, delta, U32.is_eq(axis, 0), U32.is_eq(axis, 1))# Per-axis resolve from a precomputed hit flag: stay + True on hit,# else shift + False. game.bend loops this with world lookups.def Physics.collide_axis(pos: Vec3, delta: F32, +axis: U32, hit: Bool) -> Vec3 & Bool:  match hit:    case True{}:      (pos, True{})    case False{}:      (Physics.shift_go(pos, delta, U32.is_eq(axis, 0), U32.is_eq(axis, 1)), False{})# Per-axis resolve from a small list of nearby block ids.def Physics.move_axis(pos: Vec3, delta: F32, +axis: U32, solid: List<U32>) -> Vec3 & Bool:  Physics.collide_axis(pos, delta, axis, Physics.any_blocked(solid))# Pure collide between current and candidate positions.def Physics.collide(pos: Vec3, new_pos: Vec3, hit: Bool) -> Vec3 & Bool:  match hit:    case True{}:      (pos, True{})    case False{}:      (new_pos, False{})# 8 AABB corners for a player at feet pos (half width 0.3, height 1.8).# game.bend maps these through world lookups for collision.def Physics.player_aabb_corners(+pos: Vec3) -> List<Vec3>:  match pos:    case V{+x, +y, +z}:      V{(x - 0.3 : F32), y, (z - 0.3 : F32)} <> V{(x + 0.3 : F32), y, (z - 0.3 : F32)} <> V{(x - 0.3 : F32), y, (z + 0.3 : F32)} <> V{(x + 0.3 : F32), y, (z + 0.3 : F32)} <> V{(x - 0.3 : F32), (y + 1.8 : F32), (z - 0.3 : F32)} <> V{(x + 0.3 : F32), (y + 1.8 : F32), (z - 0.3 : F32)} <> V{(x - 0.3 : F32), (y + 1.8 : F32), (z + 0.3 : F32)} <> V{(x + 0.3 : F32), (y + 1.8 : F32), (z + 0.3 : F32)} <> Nil{}# Yaw/pitch look; dx/dy are mouse deltas scaled by sensitivity.def Player.turn(p: Player, +dx: F32, +dy: F32) -> Player:  match p:    case P{pos, vel, yaw, pitch, og, fly}:      Player.turn_go(pos, vel, yaw, pitch, og, fly, dx, dy)# Forward vector from yaw/pitch.def Player.dir(p: Player) -> Vec3:  match p:    case P{pos, vel, yaw, pitch, og, fly}:      Player.dir_go(yaw, pitch)# Eye position (feet + 1.6 height).def Player.eye(p: Player) -> Vec3:  match p:    case P{pos, vel, yaw, pitch, og, fly}:      Player.eye_go(pos)# Pure physics integrate (no input, no collision): gravity then move.# Player.apply_input (wish dirs/jump) lives in the player module and runs# before this; world/game collision runs after via try_move/collide_axis.def Physics.integrate_go(pos: Vec3, vel: Vec3, yaw: F32, pitch: F32, +og: Bool, +fly: Bool, +dt: F32) -> Player:  +new_vel = Physics.step_vel(vel, og, fly, dt)  new_pos = Physics.step_pos(pos, new_vel, dt)  P{new_pos, new_vel, yaw, pitch, og, fly}def Physics.integrate(p: Player, +dt: F32) -> Player:  match p:    case P{pos, vel, yaw, pitch, og, fly}:      Physics.integrate_go(pos, vel, yaw, pitch, og, fly, dt)# LAW: gravity never increases fall velocity (airborne, not flying).def Physics.law_gravity_holds(+vy: F32, dt: F32) -> Bool:  F32.is_le(Physics.fall_vy(vy, dt, False{}, False{}), vy)# LAW: jumping while grounded sets upward velocity to jump_vel.def Physics.law_jump_holds() -> Bool:  F32.is_eq(Physics.jump_vy(0.0, True{}, True{}), Physics.jump_vel())# LAW: zero dt does not move the player.def Physics.law_zero_dt_holds(+pos: Vec3, vel: Vec3) -> Bool:  Vec3.is_eq(Physics.step_pos(pos, vel, 0.0), pos)# --- Sprint / sneak / swim / step / glide constants (additive) ---# Sprint = Ctrl hold (walk 5.0 stays). Double-W not tracked; sprint is explicit.def Physics.sprint_speed() -> F32:  7.5def Physics.sneak_speed() -> F32:  2.0def Physics.swim_speed() -> F32:  3.0def Physics.step_height() -> F32:  0.6def Physics.glide_rate() -> F32:  0.15def Physics.glide_max() -> F32:  2.0def Physics.air_drag() -> F32:  0.98def Physics.fly_drag() -> F32:  0.95def Physics.water_factor() -> F32:  0.8def Physics.ice_friction() -> F32:  0.98def Physics.ground_friction() -> F32:  0.7def Physics.drown_max() -> U32:  300def Physics.drown_ticks() -> U32:  300# Sprint selector: True -> sprint_speed, else walk_speed.def Physics.speed_of(sprint: Bool) -> F32:  match sprint:    case True{}:      Physics.sprint_speed()    case False{}:      Physics.walk_speed()# Water drag on one axis: scale by water factor.def Physics.water_drag(v: F32) -> F32:  (v * Physics.water_factor() : F32)# Swim lift: upward velocity when swimming in water, else 0.def Physics.swim_lift(swim: Bool, in_water: Bool) -> F32:  match swim in_water:    case True{} True{}:      Physics.swim_speed()    case True{} False{}:      0.0    case False{} True{}:      0.0    case False{} False{}:      0.0# Step-up helper + main: 0.0 < dy <= step_height.def Physics.can_step_go(le: Bool, gt: Bool) -> Bool:  Bool.and(le, gt)def Physics.can_step(+dy: F32) -> Bool:  Physics.can_step_go(F32.is_le(dy, Physics.step_height()), F32.is_gt(dy, 0.0))# Knockback: add horizontal impulse (vx+kx, vz+kz).def Physics.knockback(vx: F32, vz: F32, kx: F32, kz: F32) -> F32 & F32:  nx = (vx + kx : F32)  nz = (vz + kz : F32)  (nx, nz)# Fall damage helper + main: 0 under 3.5, else (fall-3.5) rounded.def Physics.fall_damage_go(small: Bool, fall: F32) -> U32:  match small:    case True{}:      0    case False{}:      F32.to_u32(F32.round((fall - 3.5 : F32)))def Physics.fall_damage(+fall: F32) -> U32:  Physics.fall_damage_go(F32.is_lt(fall, 3.5), fall)# Fluid drag: uniform scale helper, then air/fly/water selectors.def Physics.scale_vel(vel: Vec3, +k: F32) -> Vec3:  match vel:    case V{vx, vy, vz}:      V{(vx * k : F32), (vy * k : F32), (vz * k : F32)}def Physics.apply_air(vel: Vec3) -> Vec3:  Physics.scale_vel(vel, Physics.air_drag())def Physics.apply_fly_drag(vel: Vec3) -> Vec3:  Physics.scale_vel(vel, Physics.fly_drag())def Physics.apply_water_vel(vel: Vec3) -> Vec3:  Physics.scale_vel(vel, Physics.water_factor())def Physics.apply_fluid_air(vel: Vec3, fly: Bool) -> Vec3:  match fly:    case True{}:      Physics.apply_fly_drag(vel)    case False{}:      Physics.apply_air(vel)def Physics.apply_fluid_water(vel: Vec3, in_water: Bool, fly: Bool) -> Vec3:  match in_water:    case True{}:      Physics.apply_water_vel(vel)    case False{}:      Physics.apply_fluid_air(vel, fly)def Physics.apply_fluid(vel: Vec3, in_water: Bool, fly: Bool) -> Vec3:  Physics.apply_fluid_water(vel, in_water, fly)# Head bump helper + main: zero upward vy on ceiling.def Physics.bump_go(blocked: Bool, up: Bool, vy: F32) -> F32:  match blocked up:    case True{} True{}:      0.0    case True{} False{}:      vy    case False{} True{}:      vy    case False{} False{}:      vydef Physics.bump(+vy: F32, blocked_above: Bool) -> F32:  Physics.bump_go(blocked_above, F32.is_gt(vy, 0.0), vy)# Elytra-lite glide: sink rate from forward speed, clamped to max.def Physics.glide_drop(fly_speed: F32) -> F32:  F32.min((fly_speed * Physics.glide_rate() : F32), Physics.glide_max())# Slippery ice: block 12 (reserved ice, blocks.bend has 0-11) -> 0.98 else 0.7.def Physics.friction_pick(is_ice: Bool) -> F32:  match is_ice:    case True{}:      Physics.ice_friction()    case False{}:      Physics.ground_friction()def Physics.friction_of(block: U32) -> F32:  Physics.friction_pick(U32.is_eq(block, 12))