Skip to main content

gravity_fields/
gravity-fields.rs

1use mittens_engine::{engine, utils};
2
3#[path = "example_util/mod.rs"]
4mod example_util;
5
6fn cube_renderable_sibling_of_collider(
7    world: &engine::ecs::World,
8    collider_cid: engine::ecs::ComponentId,
9) -> Option<engine::ecs::ComponentId> {
10    let t = world.parent_of(collider_cid)?;
11    for &sib in world.children_of(t).iter() {
12        if sib == collider_cid {
13            continue;
14        }
15        let Some(r) =
16            world.get_component_by_id_as::<engine::ecs::component::RenderableComponent>(sib)
17        else {
18            continue;
19        };
20        if r.renderable.base_mesh == engine::graphics::primitives::CpuMeshHandle::CUBE {
21            return Some(sib);
22        }
23    }
24    None
25}
26
27fn set_renderable_color_rgba(
28    world: &mut engine::ecs::World,
29    emit: &mut dyn engine::ecs::SignalEmitter,
30    renderable_cid: engine::ecs::ComponentId,
31    rgba: [f32; 4],
32) {
33    // Prefer existing ColorComponent.
34    let existing = world
35        .children_of(renderable_cid)
36        .iter()
37        .copied()
38        .find(|&ch| {
39            world
40                .get_component_by_id_as::<engine::ecs::component::ColorComponent>(ch)
41                .is_some()
42        });
43
44    if let Some(color_cid) = existing {
45        if let Some(c) =
46            world.get_component_by_id_as_mut::<engine::ecs::component::ColorComponent>(color_cid)
47        {
48            c.rgba = rgba;
49            emit.push_intent_now(
50                color_cid,
51                engine::ecs::IntentValue::RegisterColor {
52                    component_ids: vec![color_cid],
53                },
54            );
55        }
56        return;
57    }
58
59    // Fallback: add a ColorComponent child if missing.
60    let color_cid = world.register(engine::ecs::component::ColorComponent::rgba(
61        rgba[0], rgba[1], rgba[2], rgba[3],
62    ));
63    let _ = world.add_child(renderable_cid, color_cid);
64    emit.push_intent_now(
65        color_cid,
66        engine::ecs::IntentValue::RegisterColor {
67            component_ids: vec![color_cid],
68        },
69    );
70}
71
72fn kinetic_response_child_of_collider(
73    world: &engine::ecs::World,
74    collider_cid: engine::ecs::ComponentId,
75) -> Option<engine::ecs::ComponentId> {
76    for &ch in world.children_of(collider_cid).iter() {
77        if world
78            .get_component_by_id_as::<engine::ecs::component::KineticResponseComponent>(ch)
79            .is_some()
80        {
81            return Some(ch);
82        }
83    }
84    None
85}
86
87fn on_collision_turn_white(
88    world: &mut engine::ecs::World,
89    emit: &mut dyn engine::ecs::SignalEmitter,
90    signal: &engine::ecs::Signal,
91) {
92    let Some(engine::ecs::EventSignal::CollisionStarted { a, b, .. }) = signal.event.as_ref()
93    else {
94        return;
95    };
96
97    let self_collider = signal.scope;
98    let other = if self_collider == *a {
99        *b
100    } else if self_collider == *b {
101        *a
102    } else {
103        return;
104    };
105
106    // Only react to cube-vs-cube touches (ignore static walls/floor and non-renderable colliders like camera).
107    let Some(other_cn) =
108        world.get_component_by_id_as::<engine::ecs::component::CollisionComponent>(other)
109    else {
110        return;
111    };
112    if other_cn.mode == engine::ecs::component::CollisionMode::Static {
113        return;
114    }
115    if cube_renderable_sibling_of_collider(world, other).is_none() {
116        return;
117    }
118
119    let Some(self_renderable) = cube_renderable_sibling_of_collider(world, self_collider) else {
120        return;
121    };
122    set_renderable_color_rgba(world, emit, self_renderable, [1.0, 1.0, 1.0, 1.0]);
123}
124
125fn on_collision_freeze_gravity(
126    world: &mut engine::ecs::World,
127    _emit: &mut dyn engine::ecs::SignalEmitter,
128    signal: &engine::ecs::Signal,
129) {
130    let Some(engine::ecs::EventSignal::CollisionStarted { a, b, .. }) = signal.event.as_ref()
131    else {
132        return;
133    };
134
135    let self_collider = signal.scope;
136    let other = if self_collider == *a {
137        *b
138    } else if self_collider == *b {
139        *a
140    } else {
141        return;
142    };
143
144    // Only react to cube-vs-cube touches.
145    let Some(other_cn) =
146        world.get_component_by_id_as::<engine::ecs::component::CollisionComponent>(other)
147    else {
148        return;
149    };
150    if other_cn.mode == engine::ecs::component::CollisionMode::Static {
151        return;
152    }
153    if cube_renderable_sibling_of_collider(world, other).is_none() {
154        return;
155    }
156
157    let Some(response_cid) = kinetic_response_child_of_collider(world, self_collider) else {
158        return;
159    };
160    if let Some(r) = world
161        .get_component_by_id_as_mut::<engine::ecs::component::KineticResponseComponent>(
162            response_cid,
163        )
164    {
165        // Gravity is a cached per-responder coefficient; set it to 0 once touched.
166        r.gravity_coefficient = 0.0;
167    }
168}
169
170fn main() {
171    mittens_engine::example_support::ensure_model_assets();
172    utils::logger::init();
173
174    let world = engine::ecs::World::default();
175    let mut universe = engine::Universe::new(world);
176
177    let bg_color = universe
178        .world
179        .add_component(engine::ecs::component::BackgroundColorComponent::new());
180    let bg_color_c = universe
181        .world
182        .add_component(engine::ecs::component::ColorComponent::rgba(
183            0.06, 0.04, 0.07, 1.0,
184        ));
185    let _ = universe.world.add_child(bg_color, bg_color_c);
186    universe.add(bg_color);
187
188    // overhead directional light
189    let directional_light = universe.world.add_component(
190        engine::ecs::component::DirectionalLightComponent::new()
191            .with_color(0.16, 0.14, 0.12)
192            .with_intensity(0.7),
193    );
194    let directional_light_t = universe.world.add_component(
195        engine::ecs::component::TransformComponent::new().with_position(0.0, 1.0, 0.0),
196    );
197    let _ = universe.attach(directional_light_t, directional_light);
198    universe.add(directional_light_t);
199
200    // Simple input-driven camera.
201    let input = universe
202        .world
203        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
204
205    let cam_t = universe.world.add_component(
206        engine::ecs::component::TransformComponent::new().with_position(0.0, 4.0, 10.0),
207    );
208    let cam = universe.world.add_component(
209        engine::ecs::component::Camera3DComponent::new()
210            // The default (150) clips background dressing (city + clouds).
211            .with_far(600.0)
212            .with_fov(70.0),
213    );
214
215    // Make the camera affect the collision system (same pattern as collision-perimeter).
216    let cam_collision = universe
217        .world
218        .add_component(engine::ecs::component::CollisionComponent::RIGGED());
219    let cam_response = universe
220        .world
221        .add_component(engine::ecs::component::KineticResponseComponent::slide());
222    let cam_shape =
223        universe
224            .world
225            .add_component(engine::ecs::component::CollisionShapeComponent::new(
226                engine::ecs::component::CollisionShape::sphere_radius(0.25),
227            ));
228
229    let input_mode = universe.world.add_component(
230        engine::ecs::component::InputTransformModeComponent::forward_z()
231            .with_roll_axis_y()
232            .with_fps_rotation(),
233    );
234
235    let _ = universe.attach(input, input_mode);
236    let _ = universe.attach(input, cam_t);
237    let _ = universe.attach(cam_t, cam);
238    let _ = universe.attach(cam_t, cam_collision);
239    let _ = universe.attach(cam_collision, cam_response);
240    let _ = universe.attach(cam_collision, cam_shape);
241
242    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
243    example_util::spawn_desktop_camera_controls_hint(&mut universe, cam_t);
244    universe.add(input);
245
246    let arena_half = 30.0;
247
248    // --- Background world (occluded + lit) ---
249    // Buildings are scene dressing and should not occlude foreground cubes.
250    let bg_root = universe.world.add_component(
251        engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
252    );
253    universe.add(bg_root);
254
255    // Background ground plane (rendered in background pass) with top surface at y=0.
256    // This should not occlude foreground due to depth clear between passes.
257    let bg_ground_thickness = 0.20;
258    let bg_ground_root_t = universe
259        .world
260        .add_component(engine::ecs::component::TransformComponent::new());
261    let bg_ground_geom_t = universe.world.add_component(
262        engine::ecs::component::TransformComponent::new()
263            .with_position(0.0, -bg_ground_thickness, 0.0)
264            .with_scale(
265                arena_half * 10.0,
266                bg_ground_thickness * 2.0,
267                arena_half * 10.0,
268            ),
269    );
270    let bg_ground_r = universe
271        .world
272        .add_component(engine::ecs::component::RenderableComponent::cube());
273    let bg_ground_c = universe
274        .world
275        .add_component(engine::ecs::component::ColorComponent::rgba(
276            0.03, 0.03, 0.035, 1.0,
277        ));
278    let _ = universe.attach(bg_root, bg_ground_root_t);
279    let _ = universe.attach(bg_ground_root_t, bg_ground_geom_t);
280    let _ = universe.attach(bg_ground_geom_t, bg_ground_r);
281    let _ = universe.attach(bg_ground_r, bg_ground_c);
282
283    // Background clouds (occluded + lit) using the shared example helper.
284    // NOTE: `spawn_cloud_ring` places a ring centered at the parent transform's origin.
285    // If we attach directly to `bg_root`, the ring can end up mostly out of view.
286    // So we offset a cloud root forward along -Z and keep the radius within the camera far clip.
287    let bg_cloud_root = universe.world.add_component(
288        engine::ecs::component::TransformComponent::new().with_position(
289            0.0,
290            0.0,
291            -arena_half * 2.8,
292        ),
293    );
294    let _ = universe.attach(bg_root, bg_cloud_root);
295
296    let mut cloud_params = example_util::CloudRingParams::default();
297    cloud_params.cloud_count = 9;
298    cloud_params.radius = arena_half * 1.9;
299    cloud_params.center_y = 18.0;
300    cloud_params.puffs_per_cloud = 26;
301    cloud_params.angle_jitter = 0.35;
302    cloud_params.high_y_probability = 0.35;
303    cloud_params.high_y_multiplier = 1.35;
304    cloud_params.seed = 0xC10_71A5u32;
305    example_util::spawn_cloud_ring(&mut universe, bg_cloud_root, cloud_params);
306
307    // Foreground city: NOT background-layer.
308    let fg_city_root = universe
309        .world
310        .add_component(engine::ecs::component::TransformComponent::new());
311    universe.add(fg_city_root);
312
313    // Background city: nested under the BackgroundComponent.
314    let bg_city_root = universe
315        .world
316        .add_component(engine::ecs::component::TransformComponent::new());
317    let _ = universe.attach(bg_root, bg_city_root);
318
319    fn hash_u32(mut x: u32) -> u32 {
320        // Small, deterministic integer hash (no external RNG dependency).
321        x ^= x >> 16;
322        x = x.wrapping_mul(0x7FEB_352D);
323        x ^= x >> 15;
324        x = x.wrapping_mul(0x846C_A68B);
325        x ^= x >> 16;
326        x
327    }
328
329    fn rand01(seed: u32) -> f32 {
330        let x = hash_u32(seed);
331        (x as f32) / (u32::MAX as f32)
332    }
333
334    fn spawn_floor(
335        universe: &mut engine::Universe,
336        building_root: engine::ecs::ComponentId,
337        floor_center_y: f32,
338        footprint_x: f32,
339        footprint_z: f32,
340        floor_h: f32,
341        floor_rgb: [f32; 4],
342        building_scale: f32,
343        window_mask: &[bool],
344        window_height_coeff: f32,
345        seed: u32,
346    ) {
347        // Floor geometry.
348        // IMPORTANT: don't put non-uniform scale on the node that windows attach to,
349        // otherwise child window meshes inherit the scale and look stretched.
350        let floor_root_t = universe.world.add_component(
351            engine::ecs::component::TransformComponent::new().with_position(
352                0.0,
353                floor_center_y,
354                0.0,
355            ),
356        );
357        let floor_geom_t = universe.world.add_component(
358            engine::ecs::component::TransformComponent::new().with_scale(
359                footprint_x,
360                floor_h,
361                footprint_z,
362            ),
363        );
364        let floor_r = universe
365            .world
366            .add_component(engine::ecs::component::RenderableComponent::cube());
367        let floor_c = universe
368            .world
369            .add_component(engine::ecs::component::ColorComponent::rgba(
370                floor_rgb[0],
371                floor_rgb[1],
372                floor_rgb[2],
373                floor_rgb[3],
374            ));
375
376        let _ = universe.attach(building_root, floor_root_t);
377        let _ = universe.attach(floor_root_t, floor_geom_t);
378        let _ = universe.attach(floor_geom_t, floor_r);
379        let _ = universe.attach(floor_r, floor_c);
380
381        // Windows: a single clean row per floor.
382        // Each floor receives a Vec<bool> (window_mask) that indicates which window slots are present.
383        let slots = window_mask.len().max(1);
384        let building_scale = building_scale.max(0.01);
385        let window_height_coeff = window_height_coeff.clamp(0.05, 2.0);
386
387        // Keep window size independent from floor height; floor height is already controlled by height_scale.
388        let nominal_window = 0.32 * building_scale;
389        let window_d = 0.06 * building_scale;
390
391        // Compute per-side spacing and clamp window size to fit.
392        let margin_x = (0.65 * nominal_window).min(0.18 * footprint_x);
393        let margin_z = (0.65 * nominal_window).min(0.18 * footprint_z);
394        let available_x = (footprint_x - 2.0 * margin_x).max(nominal_window);
395        let available_z = (footprint_z - 2.0 * margin_z).max(nominal_window);
396        let step_x = available_x / (slots as f32);
397        let step_z = available_z / (slots as f32);
398
399        let window_wx = nominal_window.min(step_x * 0.85);
400        let window_wz = nominal_window.min(step_z * 0.85);
401        let window_h = (nominal_window * window_height_coeff).min(floor_h * 0.80);
402
403        let warm = 0.70 + 0.25 * rand01(seed ^ 0x1F12_6A77);
404        let window_rgb = [1.0, 0.78 * warm, 0.48 * warm, 1.0];
405
406        let mut spawn_window = |local_pos: (f32, f32, f32), local_scale: (f32, f32, f32)| {
407            let window_t = universe.world.add_component(
408                engine::ecs::component::TransformComponent::new()
409                    .with_position(local_pos.0, local_pos.1, local_pos.2)
410                    .with_scale(local_scale.0, local_scale.1, local_scale.2),
411            );
412            let window_r = universe
413                .world
414                .add_component(engine::ecs::component::RenderableComponent::cube());
415            let window_c =
416                universe
417                    .world
418                    .add_component(engine::ecs::component::ColorComponent::rgba(
419                        window_rgb[0],
420                        window_rgb[1],
421                        window_rgb[2],
422                        window_rgb[3],
423                    ));
424            let window_e = universe
425                .world
426                .add_component(engine::ecs::component::EmissiveComponent::on());
427
428            let _ = universe.attach(floor_root_t, window_t);
429            let _ = universe.attach(window_t, window_r);
430            let _ = universe.attach(window_r, window_c);
431            let _ = universe.attach(window_r, window_e);
432        };
433
434        // Place the row at the center of the floor segment.
435        let wy = 0.0;
436
437        for i in 0..slots {
438            if !window_mask[i] {
439                continue;
440            }
441            let tx = -0.5 * footprint_x + margin_x + (i as f32 + 0.5) * step_x;
442            let tz = -0.5 * footprint_z + margin_z + (i as f32 + 0.5) * step_z;
443
444            // +Z/-Z faces: windows laid out along X.
445            for z_sign in [1.0_f32, -1.0_f32] {
446                spawn_window(
447                    (tx, wy, z_sign * (footprint_z * 0.5 + window_d * 0.6)),
448                    (window_wx, window_h, window_d),
449                );
450            }
451
452            // +X/-X faces: windows laid out along Z.
453            for x_sign in [1.0_f32, -1.0_f32] {
454                spawn_window(
455                    (x_sign * (footprint_x * 0.5 + window_d * 0.6), wy, tz),
456                    (window_d, window_h, window_wz),
457                );
458            }
459        }
460    }
461
462    fn spawn_building(
463        universe: &mut engine::Universe,
464        parent: engine::ecs::ComponentId,
465        x: f32,
466        y_offset: f32,
467        z: f32,
468        floors: u32,
469        seed: u32,
470        building_scale: f32,
471        height_scale: f32,
472        total_windows_per_floor_per_building_side: u32,
473        window_height_coeff: f32,
474    ) {
475        let building_scale = building_scale.max(0.01);
476        let height_scale = height_scale.max(0.01);
477
478        let floors = floors.max(1);
479        let windows_per_side = total_windows_per_floor_per_building_side.max(1) as usize;
480
481        let footprint_x = 3.2 * building_scale;
482        let footprint_z = 3.2 * building_scale;
483        let floor_h = 1.15 * building_scale * height_scale;
484
485        let base_t = universe.world.add_component(
486            engine::ecs::component::TransformComponent::new().with_position(x, y_offset, z),
487        );
488        let _ = universe.attach(parent, base_t);
489
490        // Slight per-building tint.
491        let tint = 0.03 * (rand01(seed ^ 0xA511_E9B3) - 0.5);
492        let base_rgb = [0.08 + tint, 0.085 + tint, 0.095 + tint, 1.0];
493
494        // Probability that a given window slot exists.
495        // (False means no window cube at that slot.)
496        let window_fill_probability = 0.80_f32;
497
498        for floor_i in 0..floors {
499            let floor_seed = seed ^ floor_i.wrapping_mul(0x9E37_79B9) ^ 0xB17D_1E55;
500
501            let mut window_mask = Vec::with_capacity(windows_per_side);
502            for i in 0..windows_per_side {
503                let s = floor_seed ^ (i as u32).wrapping_mul(0x85EB_CA6B) ^ 0x243F_6A88;
504                window_mask.push(rand01(s) < window_fill_probability);
505            }
506
507            let floor_center_y = floor_h * (floor_i as f32) + floor_h * 0.5;
508            spawn_floor(
509                universe,
510                base_t,
511                floor_center_y,
512                footprint_x,
513                footprint_z,
514                floor_h,
515                base_rgb,
516                building_scale,
517                &window_mask,
518                window_height_coeff,
519                floor_seed,
520            );
521        }
522    }
523
524    fn spawn_city(
525        universe: &mut engine::Universe,
526        parent: engine::ecs::ComponentId,
527        columns: u32,
528        spacing: f32,
529        position_scale: f32,
530        building_scale: f32,
531        height_scale: f32,
532        seed_base: u32,
533        y_offset: f32,
534        total_windows_per_floor_per_building_side: u32,
535        window_height_coeff: f32,
536    ) {
537        let columns = columns.max(1);
538        let half = (columns as i32) / 2;
539
540        for gz in -half..=half {
541            for gx in -half..=half {
542                // Hole in the middle.
543                if gx == 0 && gz == 0 {
544                    continue;
545                }
546
547                let seed = seed_base
548                    ^ ((gx + half) as u32).wrapping_mul(0x9E37_79B9)
549                    ^ ((gz + half) as u32).wrapping_mul(0x85EB_CA6B)
550                    ^ 0xB17D_1E55;
551
552                let floors = 3 + (hash_u32(seed) % 10); // 3..=12 floors
553
554                // Centered around the origin.
555                // - position_scale controls how far out the city sits (positions only)
556                // - building_scale controls building footprint/window sizes
557                let x = (gx as f32) * spacing * position_scale;
558                let z = (gz as f32) * spacing * position_scale;
559
560                spawn_building(
561                    universe,
562                    parent,
563                    x,
564                    y_offset,
565                    z,
566                    floors,
567                    seed,
568                    building_scale,
569                    height_scale,
570                    total_windows_per_floor_per_building_side,
571                    window_height_coeff,
572                );
573            }
574        }
575    }
576
577    // // Foreground city: 5x5 (hole in center), scaled out to sit around the floor.
578    // spawn_city(
579    //     &mut universe,
580    //     fg_city_root,
581    //     5,
582    //     7.0,
583    //     2.0,
584    //     1.0,
585    //     1.0,
586    //     0xC170_0001u32,
587    //     3,
588    //     2,
589    //     -0.0
590    // );
591
592    // Background city: larger grid, farther out, taller buildings.
593    spawn_city(
594        &mut universe,
595        bg_city_root,
596        11,
597        3.0,
598        3.0,
599        0.45,
600        1.0,
601        0xC170_0002u32,
602        -2.0,
603        6,
604        0.75,
605    );
606
607    fn spawn_street_light(universe: &mut engine::Universe) -> engine::ecs::ComponentId {
608        // Dimensions (world units).
609        let shaft_height = 6.0;
610        let shaft_thickness = 0.22;
611        let arm_length = shaft_height / 3.0;
612        let arm_thickness = 0.18;
613
614        // Colors.
615        let pole_color = [0.35, 0.35, 0.38, 1.0];
616        let housing_color = [0.18, 0.18, 0.20, 1.0];
617        let diffuser_color = [1.0, 1.0, 1.0, 1.0];
618
619        // Model root at origin.
620        // The caller is expected to position + rotate this model using an external placement transform.
621        // Street light is authored facing +X (arm extends toward +X).
622        let root_t = universe
623            .world
624            .add_component(engine::ecs::component::TransformComponent::new());
625
626        // Vertical shaft (split root/geom so scaling doesn't affect the arm).
627        let shaft_root_t = universe
628            .world
629            .add_component(engine::ecs::component::TransformComponent::new());
630        let shaft_geom_t = universe.world.add_component(
631            engine::ecs::component::TransformComponent::new()
632                .with_position(0.0, shaft_height * 0.5, 0.0)
633                .with_scale(shaft_thickness, shaft_height, shaft_thickness),
634        );
635        let shaft_r = universe
636            .world
637            .add_component(engine::ecs::component::RenderableComponent::cube());
638        let shaft_c = universe
639            .world
640            .add_component(engine::ecs::component::ColorComponent::rgba(
641                pole_color[0],
642                pole_color[1],
643                pole_color[2],
644                pole_color[3],
645            ));
646
647        let _ = universe.attach(root_t, shaft_root_t);
648        let _ = universe.attach(shaft_root_t, shaft_geom_t);
649        let _ = universe.attach(shaft_geom_t, shaft_r);
650        let _ = universe.attach(shaft_r, shaft_c);
651
652        // Horizontal arm at the top; starts at the pole and extends toward +X.
653        let arm_root_t = universe.world.add_component(
654            engine::ecs::component::TransformComponent::new().with_position(0.0, shaft_height, 0.0),
655        );
656        let arm_geom_t = universe.world.add_component(
657            engine::ecs::component::TransformComponent::new()
658                .with_position(arm_length * 0.5, -arm_thickness * 0.5, 0.0)
659                .with_scale(arm_length, arm_thickness, arm_thickness),
660        );
661        let arm_r = universe
662            .world
663            .add_component(engine::ecs::component::RenderableComponent::cube());
664        let arm_c = universe
665            .world
666            .add_component(engine::ecs::component::ColorComponent::rgba(
667                pole_color[0],
668                pole_color[1],
669                pole_color[2],
670                pole_color[3],
671            ));
672
673        let _ = universe.attach(shaft_root_t, arm_root_t);
674        let _ = universe.attach(arm_root_t, arm_geom_t);
675        let _ = universe.attach(arm_geom_t, arm_r);
676        let _ = universe.attach(arm_r, arm_c);
677
678        // Light housing at the end of the arm (+X end).
679        let housing_root_t = universe.world.add_component(
680            engine::ecs::component::TransformComponent::new().with_position(arm_length, 0.0, 0.0),
681        );
682        let housing_geom_t = universe.world.add_component(
683            engine::ecs::component::TransformComponent::new()
684                .with_position(0.5, 0.0, 0.0)
685                // 2x1x1 box in (z, x, y) -> (x, y, z) = (1, 1, 2)
686                .with_scale(2.0, 1.0, 1.0),
687        );
688        let housing_r = universe
689            .world
690            .add_component(engine::ecs::component::RenderableComponent::cube());
691        let housing_c = universe
692            .world
693            .add_component(engine::ecs::component::ColorComponent::rgba(
694                housing_color[0],
695                housing_color[1],
696                housing_color[2],
697                housing_color[3],
698            ));
699
700        let _ = universe.attach(arm_root_t, housing_root_t);
701        let _ = universe.attach(housing_root_t, housing_geom_t);
702        let _ = universe.attach(housing_geom_t, housing_r);
703        let _ = universe.attach(housing_r, housing_c);
704
705        // White diffuser cube (slightly smaller, slightly lower).
706        let diffuser_t = universe.world.add_component(
707            engine::ecs::component::TransformComponent::new()
708                .with_position(0.5, -0.35, 0.0)
709                .with_scale(1.85, 0.55, 0.70),
710        );
711        let diffuser_r = universe
712            .world
713            .add_component(engine::ecs::component::RenderableComponent::cube());
714        let diffuser_c =
715            universe
716                .world
717                .add_component(engine::ecs::component::ColorComponent::rgba(
718                    diffuser_color[0],
719                    diffuser_color[1],
720                    diffuser_color[2],
721                    diffuser_color[3],
722                ));
723
724        let diffuser_emissive = universe
725            .world
726            .add_component(engine::ecs::component::EmissiveComponent::on());
727        let _ = universe.attach(diffuser_r, diffuser_emissive);
728
729        let _ = universe.attach(housing_root_t, diffuser_t);
730        let _ = universe.attach(diffuser_t, diffuser_r);
731        let _ = universe.attach(diffuser_r, diffuser_c);
732
733        // Yellow (slightly red) point light.
734        let light = universe.world.add_component(
735            engine::ecs::component::PointLightComponent::new()
736                .with_color(1.0, 0.82, 0.55)
737                .with_intensity(5.0)
738                .with_distance(40.0),
739        );
740        let _ = universe.attach(diffuser_t, light);
741
742        root_t
743    }
744
745    // Spawn 6 street lights around the arena.
746    // Arms point inward from each side.
747    let light_x = arena_half - 2.0;
748    for z in [-10.0, 0.0, 10.0] {
749        // Left side: street light model faces +X by default, so it points inward.
750        let left_place = universe.world.add_component(
751            engine::ecs::component::TransformComponent::new().with_position(-light_x, 0.0, z),
752        );
753        let left_model = spawn_street_light(&mut universe);
754        let _ = universe.attach(left_place, left_model);
755        universe.add(left_place);
756
757        // Right side: rotate 180° around Y so the arm points toward -X (inward).
758        let right_place = universe.world.add_component(
759            engine::ecs::component::TransformComponent::new()
760                .with_position(light_x, 0.0, z)
761                .with_rotation_euler(0.0, std::f32::consts::PI, 0.0),
762        );
763        let right_model = spawn_street_light(&mut universe);
764        let _ = universe.attach(right_place, right_model);
765        universe.add(right_place);
766    }
767
768    // Big floor.
769    {
770        let floor_half = arena_half;
771        let thickness = 0.20;
772
773        // Split transforms so scaling the floor does not scale any potential children.
774        // Top surface at y=0.
775        let floor_root_t = universe
776            .world
777            .add_component(engine::ecs::component::TransformComponent::new());
778        let floor_geom_t = universe.world.add_component(
779            engine::ecs::component::TransformComponent::new()
780                .with_position(0.0, -thickness, 0.0)
781                .with_scale(floor_half * 2.0, thickness * 2.0, floor_half * 2.0),
782        );
783        let floor_r = universe
784            .world
785            .add_component(engine::ecs::component::RenderableComponent::cube());
786        let floor_color =
787            universe
788                .world
789                .add_component(engine::ecs::component::ColorComponent::rgba(
790                    0.08, 0.08, 0.09, 1.0,
791                ));
792
793        let floor_cn = universe
794            .world
795            .add_component(engine::ecs::component::CollisionComponent::STATIC());
796        let floor_shape =
797            universe
798                .world
799                .add_component(engine::ecs::component::CollisionShapeComponent::new(
800                    engine::ecs::component::CollisionShape::cube_half_extents([
801                        floor_half, thickness, floor_half,
802                    ]),
803                ));
804
805        let _ = universe.attach(floor_root_t, floor_geom_t);
806        let _ = universe.attach(floor_geom_t, floor_r);
807        let _ = universe.attach(floor_r, floor_color);
808        let _ = universe.attach(floor_geom_t, floor_cn);
809        let _ = universe.attach(floor_cn, floor_shape);
810        universe.add(floor_root_t);
811    }
812
813    // Square boundary walls around the floor edges.
814    // Note: STATIC colliders don't need KineticResponse; they still collide with kinematic/rigged.
815    fn spawn_boundary_wall(
816        universe: &mut engine::Universe,
817        x: f32,
818        y: f32,
819        z: f32,
820        half_extents: [f32; 3],
821        color: [f32; 4],
822    ) {
823        let t = universe.world.add_component(
824            engine::ecs::component::TransformComponent::new()
825                .with_position(x, y, z)
826                .with_scale(
827                    half_extents[0] * 2.0,
828                    half_extents[1] * 2.0,
829                    half_extents[2] * 2.0,
830                ),
831        );
832        let r = universe
833            .world
834            .add_component(engine::ecs::component::RenderableComponent::cube());
835        let c = universe
836            .world
837            .add_component(engine::ecs::component::ColorComponent::rgba(
838                color[0], color[1], color[2], color[3],
839            ));
840
841        let cn = universe
842            .world
843            .add_component(engine::ecs::component::CollisionComponent::STATIC());
844        let shape =
845            universe
846                .world
847                .add_component(engine::ecs::component::CollisionShapeComponent::new(
848                    engine::ecs::component::CollisionShape::cube_half_extents(half_extents),
849                ));
850
851        let _ = universe.attach(t, r);
852        let _ = universe.attach(r, c);
853        let _ = universe.attach(t, cn);
854        let _ = universe.attach(cn, shape);
855        universe.add(t);
856    }
857
858    {
859        let wall_half_height = 2.5;
860        let wall_half_thickness = 0.30;
861        let wall_center_y = wall_half_height; // bottom at y=0
862        let wall_half_len = arena_half;
863
864        // Subtle visible walls.
865        let wall_color = [0.10, 0.10, 0.12, 0.30];
866
867        // +Z / -Z
868        spawn_boundary_wall(
869            &mut universe,
870            0.0,
871            wall_center_y,
872            arena_half,
873            [wall_half_len, wall_half_height, wall_half_thickness],
874            wall_color,
875        );
876        spawn_boundary_wall(
877            &mut universe,
878            0.0,
879            wall_center_y,
880            -arena_half,
881            [wall_half_len, wall_half_height, wall_half_thickness],
882            wall_color,
883        );
884
885        // +X / -X
886        spawn_boundary_wall(
887            &mut universe,
888            arena_half,
889            wall_center_y,
890            0.0,
891            [wall_half_thickness, wall_half_height, wall_half_len],
892            wall_color,
893        );
894        spawn_boundary_wall(
895            &mut universe,
896            -arena_half,
897            wall_center_y,
898            0.0,
899            [wall_half_thickness, wall_half_height, wall_half_len],
900            wall_color,
901        );
902    }
903
904    fn spawn_falling_cube(
905        universe: &mut engine::Universe,
906        parent: engine::ecs::ComponentId,
907        x: f32,
908        y: f32,
909        z: f32,
910        s: f32,
911        r: f32,
912        g: f32,
913        b: f32,
914    ) {
915        let t = universe.world.add_component(
916            engine::ecs::component::TransformComponent::new()
917                .with_position(x, y, z)
918                .with_scale(s, s, s),
919        );
920
921        let renderable = universe
922            .world
923            .add_component(engine::ecs::component::RenderableComponent::cube());
924        let color = universe
925            .world
926            .add_component(engine::ecs::component::ColorComponent::rgba(r, g, b, 1.0));
927
928        let cn = universe
929            .world
930            .add_component(engine::ecs::component::CollisionComponent::KINEMATIC());
931
932        let response = universe.world.add_component({
933            let mut r = engine::ecs::component::KineticResponseComponent::push()
934                .with_push_strength(3.0)
935                .with_friction_y(18.0);
936            // Allow higher fall speeds so gravity coefficients are visible.
937            r.max_speed = 80.0;
938            r
939        });
940
941        let shape =
942            universe
943                .world
944                .add_component(engine::ecs::component::CollisionShapeComponent::new(
945                    engine::ecs::component::CollisionShape::cube_half_extents([
946                        0.5 * s,
947                        0.5 * s,
948                        0.5 * s,
949                    ]),
950                ));
951
952        let _ = universe.attach(t, renderable);
953        let _ = universe.attach(renderable, color);
954
955        let _ = universe.attach(t, cn);
956        let _ = universe.attach(cn, response);
957        let _ = universe.attach(cn, shape);
958
959        let _ = universe.attach(parent, t);
960    }
961
962    // Three gravity fields, each with 20 cubes.
963    let field_low = universe
964        .world
965        .add_component(engine::ecs::component::GravityComponent::new().with_coefficient(0.125));
966    let field_mid = universe
967        .world
968        .add_component(engine::ecs::component::GravityComponent::new().with_coefficient(0.5));
969    let field_high = universe
970        .world
971        .add_component(engine::ecs::component::GravityComponent::new().with_coefficient(1.0));
972
973    universe.add(field_low);
974    universe.add(field_mid);
975    universe.add(field_high);
976
977    fn spawn_field(
978        universe: &mut engine::Universe,
979        field: engine::ecs::ComponentId,
980        base_x: f32,
981        base_z: f32,
982        cube_color: [f32; 3],
983    ) {
984        let spacing = 0.7;
985        let start_x = base_x - 2.0 * spacing;
986        let start_z = base_z - 1.5 * spacing;
987
988        for i in 0..20 {
989            let ix = (i % 5) as f32;
990            let iz = (i / 5) as f32;
991
992            let x = start_x + ix * spacing;
993            let z = start_z + iz * spacing;
994            let y = 2.0 + iz * 0.6 + ix * 0.1;
995
996            spawn_falling_cube(
997                universe,
998                field,
999                x,
1000                y,
1001                z,
1002                0.5,
1003                cube_color[0],
1004                cube_color[1],
1005                cube_color[2],
1006            );
1007        }
1008
1009        // Visual marker for the field center.
1010        let marker_t = universe.world.add_component(
1011            engine::ecs::component::TransformComponent::new()
1012                .with_position(base_x, 0.1, base_z)
1013                .with_scale(0.3, 0.02, 0.3),
1014        );
1015        let marker_r = universe
1016            .world
1017            .add_component(engine::ecs::component::RenderableComponent::cube());
1018        let marker_c = universe
1019            .world
1020            .add_component(engine::ecs::component::ColorComponent::rgba(
1021                cube_color[0],
1022                cube_color[1],
1023                cube_color[2],
1024                1.0,
1025            ));
1026        let _ = universe.attach(marker_t, marker_r);
1027        let _ = universe.attach(marker_r, marker_c);
1028        universe.add(marker_t);
1029    }
1030
1031    spawn_field(&mut universe, field_low, -8.0, 0.0, [0.3, 0.6, 1.0]);
1032    spawn_field(&mut universe, field_mid, 0.0, 0.0, [0.4, 1.0, 0.4]);
1033    spawn_field(&mut universe, field_high, 8.0, 0.0, [1.0, 0.5, 0.2]);
1034
1035    // Group-scoped collision behaviors.
1036    // - Low + High fields: cubes turn white after colliding with another cube.
1037    // - Mid field: cubes lose gravity after colliding with another cube.
1038    universe.add_signal_handler(
1039        engine::ecs::SignalKind::CollisionStarted,
1040        field_low,
1041        on_collision_turn_white,
1042    );
1043    universe.add_signal_handler(
1044        engine::ecs::SignalKind::CollisionStarted,
1045        field_high,
1046        on_collision_turn_white,
1047    );
1048    universe.add_signal_handler(
1049        engine::ecs::SignalKind::CollisionStarted,
1050        field_mid,
1051        on_collision_freeze_gravity,
1052    );
1053
1054    universe.systems.process_commands(
1055        &mut universe.world,
1056        &mut universe.visuals,
1057        &mut universe.render_assets,
1058        &mut universe.command_queue,
1059    );
1060
1061    universe.enable_repl();
1062    engine::Windowing::run_app(universe).expect("Windowing failed");
1063}