-module(expresso@world). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/expresso/world.gleam"). -export([new/1, add_body/2, remove_body/2, update_body/3, get_body/2, bodies/1, with_iterations/2, with_restitution/2, with_substeps/2, with_spatial_strategy/2, query_radius/3, query_region/3, query_nearest/2, raycast/5, step/2]). -export_type([spatial_strategy/0, collision_event/1, collision_pair/1, world/1]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " 2D physics world management with intelligent spatial acceleration.\n" "\n" " This module provides the `World` type that manages a collection of rigid bodies\n" " and simulates physics each step using position-based dynamics.\n" "\n" " ## Features\n" "\n" " - **Dual spatial acceleration**: BVH for dynamic scenes, Grid for particles\n" " - **Spatial queries**: Find bodies by radius, region, or nearest neighbor\n" " - **Auto-selection**: Intelligently chooses BVH or Grid based on body distribution\n" " - **Deterministic**: Same inputs always produce same outputs\n" "\n" " ## Quick Start\n" "\n" " ```gleam\n" " import expresso/world\n" " import expresso/body\n" " import vec/vec2\n" "\n" " // Create a world with gravity\n" " let world = world.new(gravity: vec2.Vec2(0.0, -9.8))\n" "\n" " // Add bodies\n" " let world = world\n" " |> world.add_body(body.new_circle(\"ball\", vec2.Vec2(0.0, 5.0), radius: 0.5))\n" " |> world.add_body(body.new_box(\"platform\", vec2.Vec2(0.0, 0.0), 5.0, 0.5))\n" "\n" " // Configure solver\n" " let world = world\n" " |> world.with_iterations(10)\n" " |> world.with_restitution(0.5)\n" "\n" " // Step physics each frame\n" " let world = world.step(world, delta_time: 0.016)\n" " ```\n" "\n" " ## Spatial Acceleration\n" "\n" " Choose the best strategy for your use case:\n" "\n" " - **UseBVH** - Best for dynamic scenes with moving objects (default)\n" " - **UseGrid** - Best for uniform particle systems (1000s of particles)\n" " - **AutoSelect** - Automatically picks BVH or Grid based on distribution\n" "\n" " ```gleam\n" " // Force BVH for dynamic game\n" " let world = world.with_spatial_strategy(world, world.UseBVH)\n" "\n" " // Force Grid for particle system\n" " let world = world.with_spatial_strategy(world, world.UseGrid(cell_size: 2.0))\n" " ```\n" "\n" " ## Spatial Queries\n" "\n" " Find bodies efficiently using spatial queries:\n" "\n" " ```gleam\n" " // Find all bodies near a point (e.g., explosion radius)\n" " let nearby = world.query_radius(world, center: pos, radius: 5.0)\n" "\n" " // Find all bodies in a rectangle (e.g., screen culling)\n" " let visible = world.query_region(world, min: vec2.Vec2(-10.0, -10.0), \n" " max: vec2.Vec2(10.0, 10.0))\n" "\n" " // Find nearest body (e.g., target acquisition)\n" " let nearest = world.query_nearest(world, point: player_pos)\n" " ```\n" ). -type spatial_strategy() :: use_b_v_h | {use_grid, float()} | auto_select. -type collision_event(NDE) :: {collision_started, NDE, NDE} | {collision_ended, NDE, NDE} | {trigger_entered, NDE, NDE} | {trigger_exited, NDE, NDE}. -type collision_pair(NDF) :: {collision_pair, NDF, NDF}. -opaque world(NDG) :: {world, gleam@dict:dict(NDG, expresso@body:body(NDG)), vec@vec3:vec3(float()), integer(), float(), integer(), spatial_strategy(), gleam@option:option(expresso@collision:spatial_index(NDG)), list(collision_pair(NDG))}. -file("src/expresso/world.gleam", 141). ?DOC( " Create a new empty physics world\n" "\n" " ## Example\n" " ```gleam\n" " let world = world.new(\n" " gravity: vec3.Vec3(0.0, -9.8, 0.0), // Earth gravity pointing down\n" " )\n" " ```\n" ). -spec new(vec@vec3:vec3(float())) -> world(any()). new(Gravity) -> {world, maps:new(), Gravity, 4, +0.0, 2, auto_select, none, []}. -file("src/expresso/world.gleam", 159). ?DOC(" Add a body to the world\n"). -spec add_body(world(NDK), expresso@body:body(NDK)) -> world(NDK). add_body(World, Body) -> New_bodies = gleam@dict:insert( erlang:element(2, World), erlang:element(2, Body), Body ), New_spatial_index = case erlang:element(8, World) of none -> none; {some, {b_v_h_index, Tree}} -> Pos_3d = {vec3, erlang:element(2, erlang:element(3, Body)), erlang:element(3, erlang:element(3, Body)), +0.0}, Updated_tree = spatial@bvh:insert( Tree, Pos_3d, erlang:element(2, Body), 8 ), {some, {b_v_h_index, Updated_tree}}; {some, {grid_index, Grid}} -> Pos_3d@1 = {vec3, erlang:element(2, erlang:element(3, Body)), erlang:element(3, erlang:element(3, Body)), +0.0}, Updated_grid = spatial@grid:insert( Grid, Pos_3d@1, erlang:element(2, Body) ), {some, {grid_index, Updated_grid}} end, {world, New_bodies, erlang:element(3, World), erlang:element(4, World), erlang:element(5, World), erlang:element(6, World), erlang:element(7, World), New_spatial_index, erlang:element(9, World)}. -file("src/expresso/world.gleam", 182). ?DOC(" Remove a body from the world\n"). -spec remove_body(world(NDO), NDO) -> world(NDO). remove_body(World, Id) -> New_bodies = gleam@dict:delete(erlang:element(2, World), Id), New_spatial_index = case erlang:element(8, World) of none -> none; {some, {b_v_h_index, Tree}} -> case spatial@bvh:remove(Tree, fun(Body_id) -> Body_id =:= Id end) of {ok, Updated_tree} -> {some, {b_v_h_index, Updated_tree}}; {error, _} -> none end; {some, {grid_index, _}} -> none end, {world, New_bodies, erlang:element(3, World), erlang:element(4, World), erlang:element(5, World), erlang:element(6, World), erlang:element(7, World), New_spatial_index, erlang:element(9, World)}. -file("src/expresso/world.gleam", 207). ?DOC(" Update a body in the world\n"). -spec update_body( world(NDR), NDR, fun((expresso@body:body(NDR)) -> expresso@body:body(NDR)) ) -> world(NDR). update_body(World, Id, Update) -> case gleam_stdlib:map_get(erlang:element(2, World), Id) of {ok, Body} -> Updated_body = Update(Body), New_bodies = gleam@dict:insert( erlang:element(2, World), Id, Updated_body ), New_spatial_index = case erlang:element(8, World) of none -> none; {some, {b_v_h_index, Tree}} -> New_pos = {vec3, erlang:element(2, erlang:element(3, Updated_body)), erlang:element(3, erlang:element(3, Updated_body)), +0.0}, case spatial@bvh:update( Tree, fun(Body_id) -> Body_id =:= Id end, New_pos, Id, 8 ) of {ok, Updated_tree} -> {some, {b_v_h_index, Updated_tree}}; {error, _} -> none end; {some, {grid_index, _}} -> none end, {world, New_bodies, erlang:element(3, World), erlang:element(4, World), erlang:element(5, World), erlang:element(6, World), erlang:element(7, World), New_spatial_index, erlang:element(9, World)}; {error, _} -> World end. -file("src/expresso/world.gleam", 249). ?DOC(" Get a body from the world\n"). -spec get_body(world(NDW), NDW) -> {ok, expresso@body:body(NDW)} | {error, nil}. get_body(World, Id) -> gleam_stdlib:map_get(erlang:element(2, World), Id). -file("src/expresso/world.gleam", 254). ?DOC(" Get all bodies in the world\n"). -spec bodies(world(NEB)) -> gleam@dict:dict(NEB, expresso@body:body(NEB)). bodies(World) -> erlang:element(2, World). -file("src/expresso/world.gleam", 262). ?DOC( " Set the number of solver iterations\n" "\n" " More iterations = more stable but slower.\n" " 3-5 is typical, 10+ for very stable stacks.\n" ). -spec with_iterations(world(NEG), integer()) -> world(NEG). with_iterations(World, Iterations) -> {world, erlang:element(2, World), erlang:element(3, World), Iterations, erlang:element(5, World), erlang:element(6, World), erlang:element(7, World), erlang:element(8, World), erlang:element(9, World)}. -file("src/expresso/world.gleam", 270). ?DOC( " Set the coefficient of restitution (bounciness)\n" "\n" " 0.0 = perfectly inelastic (no bounce)\n" " 1.0 = perfectly elastic (full bounce)\n" ). -spec with_restitution(world(NEJ), float()) -> world(NEJ). with_restitution(World, Restitution) -> {world, erlang:element(2, World), erlang:element(3, World), erlang:element(4, World), Restitution, erlang:element(6, World), erlang:element(7, World), erlang:element(8, World), erlang:element(9, World)}. -file("src/expresso/world.gleam", 289). ?DOC( " Set the number of substeps per physics step\n" "\n" " Substeps divide each physics step into smaller timesteps,\n" " re-detecting collisions between each substep. This fixes chain\n" " collisions where forces need to propagate through multiple bodies.\n" "\n" " More substeps = more accurate but slower.\n" " Typical values: 2-3 for games, 4+ for complex chains.\n" "\n" " ## Example\n" " ```gleam\n" " // Handle chains of 10+ enemies pushing each other\n" " let world = world.new(gravity: vec2.Vec2(0.0, 0.0))\n" " |> world.with_substeps(3)\n" " ```\n" ). -spec with_substeps(world(NEM), integer()) -> world(NEM). with_substeps(World, Substeps) -> {world, erlang:element(2, World), erlang:element(3, World), erlang:element(4, World), erlang:element(5, World), Substeps, erlang:element(7, World), erlang:element(8, World), erlang:element(9, World)}. -file("src/expresso/world.gleam", 307). ?DOC( " Set the spatial partitioning strategy\n" "\n" " - `UseBVH`: Best for dynamic scenes with moving objects (default)\n" " - `UseGrid(cell_size)`: Best for uniform distributions (particles, crowds)\n" " - `AutoSelect`: Automatically choose based on body count\n" "\n" " ## Example\n" " ```gleam\n" " // Force BVH for dynamic game\n" " let world = world.with_spatial_strategy(world, UseBVH)\n" " \n" " // Use grid for particle system\n" " let world = world.with_spatial_strategy(world, UseGrid(cell_size: 2.0))\n" " ```\n" ). -spec with_spatial_strategy(world(NEP), spatial_strategy()) -> world(NEP). with_spatial_strategy(World, Strategy) -> {world, erlang:element(2, World), erlang:element(3, World), erlang:element(4, World), erlang:element(5, World), erlang:element(6, World), Strategy, none, erlang:element(9, World)}. -file("src/expresso/world.gleam", 322). ?DOC( " Query bodies within a radius of a point\n" "\n" " Uses spatial acceleration when available.\n" "\n" " ## Example\n" " ```gleam\n" " let nearby = world.query_radius(world, center: vec3.Vec3(0.0, 0.0, 0.0), radius: 5.0)\n" " ```\n" ). -spec query_radius(world(NES), vec@vec3:vec3(float()), float()) -> list({NES, expresso@body:body(NES)}). query_radius(World, Center, Radius) -> case erlang:element(8, World) of none -> _pipe = maps:to_list(erlang:element(2, World)), expresso@collision:filter_by_radius(_pipe, Center, Radius); {some, {b_v_h_index, Tree}} -> _pipe@1 = spatial@bvh:query_radius(Tree, Center, Radius), expresso@collision:extract_bodies_from_results( _pipe@1, erlang:element(2, World) ); {some, {grid_index, Grid}} -> _pipe@2 = spatial@grid:query_radius(Grid, Center, Radius), expresso@collision:extract_bodies_from_results( _pipe@2, erlang:element(2, World) ) end. -file("src/expresso/world.gleam", 354). ?DOC( " Query bodies within a rectangular region\n" "\n" " ## Example\n" " ```gleam\n" " let bodies_in_region = world.query_region(\n" " world,\n" " min: vec3.Vec3(-10.0, -10.0, -10.0),\n" " max: vec3.Vec3(10.0, 10.0, 10.0),\n" " )\n" " ```\n" ). -spec query_region(world(NEX), vec@vec3:vec3(float()), vec@vec3:vec3(float())) -> list({NEX, expresso@body:body(NEX)}). query_region(World, Min, Max) -> Region = spatial@collider:box(Min, Max), case erlang:element(8, World) of none -> _pipe = maps:to_list(erlang:element(2, World)), expresso@collision:filter_by_region(_pipe, Min, Max); {some, {b_v_h_index, Tree}} -> _pipe@1 = spatial@bvh:'query'(Tree, Region), expresso@collision:extract_bodies_from_results( _pipe@1, erlang:element(2, World) ); {some, {grid_index, Grid}} -> _pipe@2 = spatial@grid:'query'(Grid, Region), expresso@collision:extract_bodies_from_results( _pipe@2, erlang:element(2, World) ) end. -file("src/expresso/world.gleam", 387). ?DOC( " Find the nearest body to a point\n" "\n" " ## Example\n" " ```gleam\n" " case world.query_nearest(world, point: vec3.Vec3(5.0, 5.0, 5.0)) {\n" " Some(#(id, body, distance)) -> // Use nearest body\n" " None -> // No bodies in world\n" " }\n" " ```\n" ). -spec query_nearest(world(NFD), vec@vec3:vec3(float())) -> gleam@option:option({NFD, expresso@body:body(NFD), float()}). query_nearest(World, Point) -> Max_radius = 1000.0, _pipe = query_radius(World, Point, Max_radius), expresso@collision:find_nearest(_pipe, Point). -file("src/expresso/world.gleam", 438). ?DOC( " Cast a ray and find the first body it hits\n" "\n" " Returns detailed information about the hit including:\n" " - The body that was hit\n" " - The exact hit point\n" " - The surface normal at the hit point\n" " - The distance from the ray origin\n" "\n" " Optionally filter by collision layers using the layer_mask parameter.\n" "\n" " ## Example\n" " ```gleam\n" " // Shoot a laser forward\n" " case world.raycast(\n" " world,\n" " origin: player_pos,\n" " direction: vec3.Vec3(1.0, 0.0, 0.0),\n" " max_distance: 100.0,\n" " layer_mask: option.None, // Hit all layers\n" " ) {\n" " Some(hit) -> {\n" " // Hit something!\n" " io.println(\"Hit \" <> hit.body.id <> \" at distance \" <> float.to_string(hit.distance))\n" " }\n" " None -> {\n" " // No hit\n" " }\n" " }\n" "\n" " // Only hit enemies\n" " case world.raycast(\n" " world,\n" " origin: player_pos,\n" " direction: aim_direction,\n" " max_distance: 50.0,\n" " layer_mask: option.Some(body.layer_enemy),\n" " ) {\n" " Some(hit) -> damage_enemy(hit.body_id)\n" " None -> {}\n" " }\n" " ```\n" ). -spec raycast( world(NFI), vec@vec3:vec3(float()), vec@vec3:vec3(float()), float(), gleam@option:option(integer()) ) -> gleam@option:option(expresso@collision:raycast_hit(NFI)). raycast(World, Origin, Direction, Max_distance, Layer_mask) -> expresso@collision:raycast( erlang:element(2, World), erlang:element(8, World), Origin, Direction, Max_distance, Layer_mask ). -file("src/expresso/world.gleam", 514). ?DOC(" Check if world has fast-moving CCD bodies that need extra substeps\n"). -spec has_fast_ccd_bodies(world(any()), float()) -> boolean(). has_fast_ccd_bodies(World, Delta_time) -> _pipe = maps:to_list(erlang:element(2, World)), gleam@list:any( _pipe, fun(Pair) -> {_, Body_val} = Pair, case erlang:element(13, Body_val) of false -> false; true -> Speed = vec@vec3f:length(erlang:element(4, Body_val)), Distance_per_frame = Speed * Delta_time, Bounding_radius = expresso@body:bounding_radius(Body_val), Distance_per_frame > (Bounding_radius * 0.5) end end ). -file("src/expresso/world.gleam", 534). ?DOC(" Apply gravity to all dynamic bodies\n"). -spec apply_gravity(world(NFW), float()) -> world(NFW). apply_gravity(World, Delta_time) -> New_bodies = gleam@dict:map_values( erlang:element(2, World), fun(_, Body) -> case erlang:element(9, Body) of true -> Body; false -> Gravity_velocity = vec@vec3f:scale( erlang:element(3, World), Delta_time ), {body, erlang:element(2, Body), erlang:element(3, Body), vec@vec3f:add(erlang:element(4, Body), Gravity_velocity), erlang:element(5, Body), erlang:element(6, Body), erlang:element(7, Body), erlang:element(8, Body), erlang:element(9, Body), erlang:element(10, Body), erlang:element(11, Body), erlang:element(12, Body), erlang:element(13, Body), erlang:element(14, Body), erlang:element(15, Body), erlang:element(16, Body)} end end ), {world, New_bodies, erlang:element(3, World), erlang:element(4, World), erlang:element(5, World), erlang:element(6, World), erlang:element(7, World), erlang:element(8, World), erlang:element(9, World)}. -file("src/expresso/world.gleam", 591). ?DOC(" Integrate angular velocity into rotation using quaternions\n"). -spec integrate_angular_velocity( quaternion:quaternion(), vec@vec3:vec3(float()), float() ) -> quaternion:quaternion(). integrate_angular_velocity(Rotation, Angular_velocity, Delta_time) -> Speed_squared = ((erlang:element(2, Angular_velocity) * erlang:element( 2, Angular_velocity )) + (erlang:element(3, Angular_velocity) * erlang:element(3, Angular_velocity))) + (erlang:element(4, Angular_velocity) * erlang:element(4, Angular_velocity)), case Speed_squared < 0.000001 of true -> Rotation; false -> Speed = case gleam@float:square_root(Speed_squared) of {ok, S} -> S; {error, _} -> +0.0 end, Angle = Speed * Delta_time, Axis = {vec3, case Speed of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> erlang:element(2, Angular_velocity) / Gleam@denominator end, case Speed of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator@1 -> erlang:element(3, Angular_velocity) / Gleam@denominator@1 end, case Speed of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator@2 -> erlang:element(4, Angular_velocity) / Gleam@denominator@2 end}, Delta_rotation = quaternion:from_axis_angle(Axis, Angle), _pipe = quaternion:multiply(Rotation, Delta_rotation), quaternion:normalize(_pipe) end. -file("src/expresso/world.gleam", 553). ?DOC(" Integrate velocities to update positions and rotations\n"). -spec integrate_velocities(world(NFZ), float()) -> world(NFZ). integrate_velocities(World, Delta_time) -> New_bodies = gleam@dict:map_values( erlang:element(2, World), fun(_, Body) -> Displacement = vec@vec3f:scale(erlang:element(4, Body), Delta_time), New_position = vec@vec3f:add(erlang:element(3, Body), Displacement), Friction_factor = 1.0 - (erlang:element(10, Body) * Delta_time), New_velocity = vec@vec3f:scale( erlang:element(4, Body), Friction_factor ), New_rotation = integrate_angular_velocity( erlang:element(5, Body), erlang:element(6, Body), Delta_time ), Angular_damping = 0.98, New_angular_velocity = vec@vec3f:scale( erlang:element(6, Body), Angular_damping ), {body, erlang:element(2, Body), New_position, New_velocity, New_rotation, New_angular_velocity, erlang:element(7, Body), erlang:element(8, Body), erlang:element(9, Body), erlang:element(10, Body), erlang:element(11, Body), erlang:element(12, Body), erlang:element(13, Body), erlang:element(14, Body), erlang:element(15, Body), erlang:element(16, Body)} end ), {world, New_bodies, erlang:element(3, World), erlang:element(4, World), erlang:element(5, World), erlang:element(6, World), erlang:element(7, World), erlang:element(8, World), erlang:element(9, World)}. -file("src/expresso/world.gleam", 724). ?DOC(" Auto-select the best spatial strategy based on body count and distribution\n"). -spec auto_select_strategy( gleam@dict:dict(NGL, expresso@body:body(NGL)), integer() ) -> spatial_strategy(). auto_select_strategy(Bodies, Body_count) -> case Body_count of N when N < 100 -> use_b_v_h; _ -> {Is_uniform, Avg_radius} = expresso@collision:analyze_distribution( Bodies ), case Is_uniform of true -> {use_grid, Avg_radius * 4.0}; false -> use_b_v_h end end. -file("src/expresso/world.gleam", 742). ?DOC( " Build a BVH spatial index from bodies (stores IDs for efficient updates)\n" " Optimized: Single pass with dict.fold instead of dict.to_list + list.map\n" ). -spec build_bvh_index(gleam@dict:dict(NGP, expresso@body:body(NGP))) -> expresso@collision:spatial_index(NGP). build_bvh_index(Bodies) -> Items = gleam@dict:fold( Bodies, [], fun(Acc, Id, Body) -> Pos_3d = {vec3, erlang:element(2, erlang:element(3, Body)), erlang:element(3, erlang:element(3, Body)), +0.0}, [{Pos_3d, Id} | Acc] end ), Tree@1 = case spatial@bvh:from_items(Items, 8) of {ok, Tree} -> Tree; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Expected body count to be always nonzero"/utf8>>, file => <>, module => <<"expresso/world"/utf8>>, function => <<"build_bvh_index"/utf8>>, line => 749, value => _assert_fail, start => 22764, 'end' => 22825, pattern_start => 22775, pattern_end => 22783}) end, {b_v_h_index, Tree@1}. -file("src/expresso/world.gleam", 756). ?DOC(" Build a Grid spatial index from bodies (stores IDs for efficient updates)\n"). -spec build_grid_index(gleam@dict:dict(NGU, expresso@body:body(NGU)), float()) -> expresso@collision:spatial_index(NGU). build_grid_index(Bodies, Cell_size) -> Bounds = expresso@collision:calculate_world_bounds(Bodies), Grid_empty = spatial@grid:new(Cell_size, Bounds), Grid_filled = gleam@dict:fold( Bodies, Grid_empty, fun(G, Id, Body) -> Pos_3d = {vec3, erlang:element(2, erlang:element(3, Body)), erlang:element(3, erlang:element(3, Body)), +0.0}, spatial@grid:insert(G, Pos_3d, Id) end ), {grid_index, Grid_filled}. -file("src/expresso/world.gleam", 700). ?DOC(" Build or rebuild the spatial index based on current bodies\n"). -spec rebuild_spatial_index(world(NGI)) -> world(NGI). rebuild_spatial_index(World) -> Body_count = maps:size(erlang:element(2, World)), case Body_count of 0 -> {world, erlang:element(2, World), erlang:element(3, World), erlang:element(4, World), erlang:element(5, World), erlang:element(6, World), erlang:element(7, World), none, erlang:element(9, World)}; _ -> Strategy = case erlang:element(7, World) of use_b_v_h -> use_b_v_h; {use_grid, Cell_size} -> {use_grid, Cell_size}; auto_select -> auto_select_strategy(erlang:element(2, World), Body_count) end, Spatial_index = case Strategy of use_b_v_h -> build_bvh_index(erlang:element(2, World)); {use_grid, Cell_size@1} -> build_grid_index(erlang:element(2, World), Cell_size@1); auto_select -> build_bvh_index(erlang:element(2, World)) end, {world, erlang:element(2, World), erlang:element(3, World), erlang:element(4, World), erlang:element(5, World), erlang:element(6, World), erlang:element(7, World), {some, Spatial_index}, erlang:element(9, World)} end. -file("src/expresso/world.gleam", 778). ?DOC(" Check if a collision pair exists in a list (order-independent)\n"). -spec contains_collision(list(collision_pair(NGZ)), NGZ, NGZ) -> boolean(). contains_collision(Collisions, A, B) -> gleam@list:any( Collisions, fun(Pair) -> ((erlang:element(2, Pair) =:= A) andalso (erlang:element(3, Pair) =:= B)) orelse ((erlang:element(2, Pair) =:= B) andalso (erlang:element( 3, Pair ) =:= A)) end ). -file("src/expresso/world.gleam", 785). ?DOC(" Convert contacts to collision pairs\n"). -spec contacts_to_pairs( list(expresso@collision:contact(NHC)), gleam@dict:dict(NHC, expresso@body:body(NHC)) ) -> {list(collision_pair(NHC)), list(collision_pair(NHC))}. contacts_to_pairs(Contacts, Bodies) -> gleam@list:fold( Contacts, {[], []}, fun(Acc, Contact) -> {Regular, Triggers} = Acc, case {gleam_stdlib:map_get(Bodies, erlang:element(2, Contact)), gleam_stdlib:map_get(Bodies, erlang:element(3, Contact))} of {{ok, Body_a}, {ok, Body_b}} -> Pair = {collision_pair, erlang:element(2, Contact), erlang:element(3, Contact)}, case erlang:element(16, Body_a) orelse erlang:element( 16, Body_b ) of true -> {Regular, [Pair | Triggers]}; false -> {[Pair | Regular], Triggers} end; {_, _} -> Acc end end ). -file("src/expresso/world.gleam", 807). ?DOC(" Generate collision events by comparing current and previous collisions\n"). -spec generate_collision_events( list(collision_pair(NHM)), list(collision_pair(NHM)), list(collision_pair(NHM)), gleam@dict:dict(NHM, expresso@body:body(NHM)) ) -> list(collision_event(NHM)). generate_collision_events(Current_regular, Current_triggers, Previous, Bodies) -> Started_events = gleam@list:filter_map( Current_regular, fun(Pair) -> case contains_collision( Previous, erlang:element(2, Pair), erlang:element(3, Pair) ) of true -> {error, nil}; false -> {ok, {collision_started, erlang:element(2, Pair), erlang:element(3, Pair)}} end end ), Ended_events = gleam@list:filter_map( Previous, fun(Pair@1) -> case contains_collision( Current_regular, erlang:element(2, Pair@1), erlang:element(3, Pair@1) ) of true -> {error, nil}; false -> {ok, {collision_ended, erlang:element(2, Pair@1), erlang:element(3, Pair@1)}} end end ), Trigger_events = gleam@list:filter_map( Current_triggers, fun(Pair@2) -> case {gleam_stdlib:map_get(Bodies, erlang:element(2, Pair@2)), gleam_stdlib:map_get(Bodies, erlang:element(3, Pair@2))} of {{ok, Body_a}, {ok, Body_b}} -> case {erlang:element(16, Body_a), erlang:element(16, Body_b)} of {true, false} -> case contains_collision( Previous, erlang:element(2, Pair@2), erlang:element(3, Pair@2) ) of true -> {error, nil}; false -> {ok, {trigger_entered, erlang:element(3, Pair@2), erlang:element(2, Pair@2)}} end; {false, true} -> case contains_collision( Previous, erlang:element(2, Pair@2), erlang:element(3, Pair@2) ) of true -> {error, nil}; false -> {ok, {trigger_entered, erlang:element(2, Pair@2), erlang:element(3, Pair@2)}} end; {_, _} -> {error, nil} end; {_, _} -> {error, nil} end end ), Trigger_exit_events = gleam@list:filter_map( Previous, fun(Pair@3) -> case {gleam_stdlib:map_get(Bodies, erlang:element(2, Pair@3)), gleam_stdlib:map_get(Bodies, erlang:element(3, Pair@3)), contains_collision( Current_triggers, erlang:element(2, Pair@3), erlang:element(3, Pair@3) )} of {{ok, Body_a@1}, {ok, Body_b@1}, false} -> case {erlang:element(16, Body_a@1), erlang:element(16, Body_b@1)} of {true, false} -> {ok, {trigger_exited, erlang:element(3, Pair@3), erlang:element(2, Pair@3)}}; {false, true} -> {ok, {trigger_exited, erlang:element(2, Pair@3), erlang:element(3, Pair@3)}}; {_, _} -> {error, nil} end; {_, _, _} -> {error, nil} end end ), lists:append( [Started_events, Ended_events, Trigger_events, Trigger_exit_events] ). -file("src/expresso/world.gleam", 636). ?DOC( " Detect collisions and resolve them\n" " Returns updated world and collision events (only generated if generate_events is True)\n" ). -spec detect_and_resolve_collisions(world(NGD), boolean()) -> {world(NGD), list(collision_event(NGD))}. detect_and_resolve_collisions(World, Generate_events) -> World@1 = rebuild_spatial_index(World), Contacts = case erlang:element(8, World@1) of none -> expresso@collision:detect_collisions(erlang:element(2, World@1)); {some, Spatial_index} -> expresso@collision:detect_collisions_with_index( erlang:element(2, World@1), Spatial_index ) end, Bodies_after_position = expresso@internal@solver:solve_position_constraints( erlang:element(2, World@1), Contacts, erlang:element(4, World@1) ), Bodies_after_velocity = expresso@internal@solver:solve_velocity_constraints( Bodies_after_position, Contacts, erlang:element(5, World@1) ), {Current_regular, Current_triggers} = contacts_to_pairs( Contacts, erlang:element(2, World@1) ), All_current = lists:append(Current_regular, Current_triggers), Events = case Generate_events of false -> []; true -> generate_collision_events( Current_regular, Current_triggers, erlang:element(9, World@1), erlang:element(2, World@1) ) end, New_previous = case Generate_events of true -> All_current; false -> erlang:element(9, World@1) end, Updated_world = {world, Bodies_after_velocity, erlang:element(3, World@1), erlang:element(4, World@1), erlang:element(5, World@1), erlang:element(6, World@1), erlang:element(7, World@1), erlang:element(8, World@1), New_previous}, {Updated_world, Events}. -file("src/expresso/world.gleam", 477). ?DOC( " Simulate one physics timestep using substeps\n" "\n" " This is the main physics update function. Call it each game tick with delta_time.\n" " Returns the updated world and a list of collision events that occurred.\n" "\n" " The timestep is divided into `world.substeps` smaller steps, with collision\n" " detection happening between each substep. This fixes chain collisions where\n" " forces need to propagate through multiple bodies (e.g., Enemy1 → Enemy2 → Enemy3).\n" "\n" " ## Example\n" " ```gleam\n" " let #(world, events) = world.step(world, delta_time: 1.0 /. 60.0) // 60 FPS\n" "\n" " // Handle collision events\n" " list.each(events, fn(event) {\n" " case event {\n" " CollisionStarted(a, b) -> io.println(\"Collision started!\")\n" " TriggerEntered(body, trigger) -> io.println(\"Entered trigger zone!\")\n" " _ -> Nil\n" " }\n" " })\n" " ```\n" ). -spec step(world(NFP), float()) -> {world(NFP), list(collision_event(NFP))}. step(World, Delta_time) -> Needs_ccd = has_fast_ccd_bodies(World, Delta_time), Substeps = case Needs_ccd of true -> erlang:element(6, World) * 4; false -> erlang:element(6, World) end, Substep_dt = case erlang:float(Substeps) of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> Delta_time / Gleam@denominator end, {Final_world, Events} = begin _pipe = gleam@list:range(0, Substeps - 1), gleam@list:fold( _pipe, {World, []}, fun(Acc, Substep_index) -> {Current_world, _} = Acc, Is_final_substep = Substep_index =:= (Substeps - 1), Updated_world = begin _pipe@1 = Current_world, _pipe@2 = apply_gravity(_pipe@1, Substep_dt), integrate_velocities(_pipe@2, Substep_dt) end, detect_and_resolve_collisions(Updated_world, Is_final_substep) end ) end, {Final_world, Events}.