-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(NDF) :: {collision_started, NDF, NDF} | {collision_ended, NDF, NDF} | {trigger_entered, NDF, NDF} | {trigger_exited, NDF, NDF}. -type collision_pair(NDG) :: {collision_pair, NDG, NDG}. -opaque world(NDH) :: {world, gleam@dict:dict(NDH, expresso@body:body(NDH)), vec@vec3:vec3(float()), integer(), float(), integer(), spatial_strategy(), gleam@option:option(expresso@collision:spatial_index(NDH)), list(collision_pair(NDH))}. -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(NDL), expresso@body:body(NDL)) -> world(NDL). 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(NDP), NDP) -> world(NDP). 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(NDS), NDS, fun((expresso@body:body(NDS)) -> expresso@body:body(NDS)) ) -> world(NDS). 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(NDX), NDX) -> {ok, expresso@body:body(NDX)} | {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(NEC)) -> gleam@dict:dict(NEC, expresso@body:body(NEC)). 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(NEH), integer()) -> world(NEH). 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(NEK), float()) -> world(NEK). 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(NEN), integer()) -> world(NEN). 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(NEQ), spatial_strategy()) -> world(NEQ). 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(NET), vec@vec3:vec3(float()), float()) -> list({NET, expresso@body:body(NET)}). 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(NEY), vec@vec3:vec3(float()), vec@vec3:vec3(float())) -> list({NEY, expresso@body:body(NEY)}). 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(NFE), vec@vec3:vec3(float())) -> gleam@option:option({NFE, expresso@body:body(NFE), 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(NFJ), vec@vec3:vec3(float()), vec@vec3:vec3(float()), float(), gleam@option:option(integer()) ) -> gleam@option:option(expresso@collision:raycast_hit(NFJ)). 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(NFX), float()) -> world(NFX). 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(NGA), float()) -> world(NGA). 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(NGM, expresso@body:body(NGM)), 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", 741). ?DOC(" Build a BVH spatial index from bodies (stores IDs for efficient updates)\n"). -spec build_bvh_index(gleam@dict:dict(NGQ, expresso@body:body(NGQ))) -> expresso@collision:spatial_index(NGQ). build_bvh_index(Bodies) -> Items = begin _pipe = maps:to_list(Bodies), gleam@list:map( _pipe, fun(Pair) -> {Id, Body} = Pair, Pos_3d = {vec3, erlang:element(2, erlang:element(3, Body)), erlang:element(3, erlang:element(3, Body)), +0.0}, {Pos_3d, Id} end ) 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 => 750, value => _assert_fail, start => 22713, 'end' => 22774, pattern_start => 22724, pattern_end => 22732}) end, {b_v_h_index, Tree@1}. -file("src/expresso/world.gleam", 757). ?DOC(" Build a Grid spatial index from bodies (stores IDs for efficient updates)\n"). -spec build_grid_index(gleam@dict:dict(NGV, expresso@body:body(NGV)), float()) -> expresso@collision:spatial_index(NGV). 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(NGJ)) -> world(NGJ). 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", 779). ?DOC(" Check if a collision pair exists in a list (order-independent)\n"). -spec contains_collision(list(collision_pair(NHA)), NHA, NHA) -> 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", 786). ?DOC(" Convert contacts to collision pairs\n"). -spec contacts_to_pairs( list(expresso@collision:contact(NHD)), gleam@dict:dict(NHD, expresso@body:body(NHD)) ) -> {list(collision_pair(NHD)), list(collision_pair(NHD))}. 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", 808). ?DOC(" Generate collision events by comparing current and previous collisions\n"). -spec generate_collision_events( list(collision_pair(NHN)), list(collision_pair(NHN)), list(collision_pair(NHN)), gleam@dict:dict(NHN, expresso@body:body(NHN)) ) -> list(collision_event(NHN)). 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(NGE), boolean()) -> {world(NGE), list(collision_event(NGE))}. 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(NFQ), float()) -> {world(NFQ), list(collision_event(NFQ))}. 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}.