gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-voxel/src/critter.rs Rust · 171 líneas · 6.1 KB Historia Quién tocó qué crudo

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · Sergio ·

1//! `Critter` — un agente que **deambula** el mundo voxel por su cuenta: la
2//! misma física de cuerpo que el jugador ([`Player`]) más una voluntad mínima
3//! (cambia de rumbo cada tanto, salta a veces, gira al chocar). Sirve para
4//! poblar un mundo voxel de bichos vivos; el render los dibuja como cajas
5//! analíticas en el mismo pase de ray-march ([`llimphi_3d::Entity3d`]).
6//!
7//! Determinista (sin `rand`): cada bicho lleva su propia semilla y avanza un
8//! LCG, así un mundo se reproduce igual (y los tests/PNG son estables).
9
10use llimphi_3d::{Entity3d, VoxelGrid};
11
12use crate::{forward_h, Player};
13
14const TAU: f32 = std::f32::consts::TAU;
15/// Velocidad de pasto de un bicho (más lento que el jugador).
16const CRITTER_SPEED: f32 = 3.2;
17/// Semi-tamaño de la caja del bicho (ancho, alto, fondo) en voxels.
18const HALF: [f32; 3] = [0.5, 0.7, 0.5];
19
20/// Bicho que deambula: un [`Player`] (física) + rumbo y temporizadores de IA.
21#[derive(Debug, Clone, Copy)]
22pub struct Critter {
23    /// Cuerpo físico (gravedad + colisión), reusado del jugador.
24    pub body: Player,
25    /// Color de la caja al dibujarlo.
26    pub color: [u8; 3],
27    /// Rumbo actual de caminata (yaw, rad).
28    heading: f32,
29    /// Segundos hasta el próximo cambio de rumbo.
30    timer: f32,
31    /// Pide saltar en el próximo paso (one-shot).
32    jump: bool,
33    /// Estado del LCG (azar determinista por bicho).
34    rng: u32,
35}
36
37impl Critter {
38    /// Bicho posado sobre la columna `(x, z)` del grid, con `color` y `seed`
39    /// (distintas semillas → rumbos distintos).
40    pub fn spawn_on(grid: &VoxelGrid, x: u32, z: u32, color: [u8; 3], seed: u32) -> Self {
41        let mut body = Player::spawn_on(grid, x, z);
42        body.speed = CRITTER_SPEED;
43        let mut c = Self {
44            body,
45            color,
46            heading: 0.0,
47            timer: 0.0,
48            jump: false,
49            rng: seed | 1, // nunca 0 (un LCG con 0 se queda pegado en patrones pobres)
50        };
51        c.pick_new_goal();
52        c
53    }
54
55    /// Avanza un `dt`: actualiza la voluntad y mueve el cuerpo. Llamar por frame.
56    pub fn step(&mut self, grid: &VoxelGrid, dt: f32) {
57        self.timer -= dt;
58        if self.timer <= 0.0 {
59            self.pick_new_goal();
60        }
61
62        let before = self.body.pos;
63        let wish = forward_h(self.heading);
64        let jump = self.jump && self.body.on_ground;
65        self.body.step(grid, wish, jump, dt);
66        self.jump = false;
67
68        // ¿Chocó? (apenas se movió en horizontal estando en el piso) → media
69        // vuelta y nuevo rumbo, así no se queda empujando una pared.
70        let dx = self.body.pos.x - before.x;
71        let dz = self.body.pos.z - before.z;
72        let moved = (dx * dx + dz * dz).sqrt();
73        if self.body.on_ground && moved < 0.2 * CRITTER_SPEED * dt {
74            self.heading += TAU * 0.5 + (self.next_f32() - 0.5);
75            self.timer = 0.6 + self.next_f32();
76        }
77    }
78
79    /// Caja analítica para el renderer: centro = pies + medio-alto, así la caja
80    /// se apoya en el suelo (los pies del cuerpo).
81    pub fn entity(&self) -> Entity3d {
82        Entity3d {
83            pos: [self.body.pos.x, self.body.pos.y + HALF[1], self.body.pos.z],
84            half: HALF,
85            color: self.color,
86        }
87    }
88
89    /// Elige rumbo, duración y un eventual salto nuevos.
90    fn pick_new_goal(&mut self) {
91        self.heading = self.next_f32() * TAU;
92        self.timer = 0.8 + self.next_f32() * 2.5;
93        self.jump = self.next_f32() < 0.18;
94    }
95
96    /// Próximo `f32` en `[0, 1)` del LCG (Numerical Recipes).
97    fn next_f32(&mut self) -> f32 {
98        self.rng = self.rng.wrapping_mul(1664525).wrapping_add(1013904223);
99        // Bits altos (más aleatorios) → mantisa de 24 bits.
100        (self.rng >> 8) as f32 / (1u32 << 24) as f32
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use llimphi_3d::glam::Vec3;
108
109    /// Grid 32³ con piso sólido en `y=0`.
110    fn grid_con_piso() -> VoxelGrid {
111        let mut g = VoxelGrid::new([32, 8, 32]);
112        for z in 0..32 {
113            for x in 0..32 {
114                g.set(x, 0, z, [90, 140, 80]);
115            }
116        }
117        g
118    }
119
120    #[test]
121    fn deambula_y_no_atraviesa_el_piso() {
122        let g = grid_con_piso();
123        let start = Vec3::new(16.5, 1.0, 16.5);
124        let mut c = Critter::spawn_on(&g, 16, 16, [220, 220, 220], 7);
125        assert!((c.body.pos - start).length() < 0.1);
126        for _ in 0..600 {
127            c.step(&g, 1.0 / 60.0);
128            // Nunca cae por debajo del piso (pies en y≈1).
129            assert!(c.body.pos.y >= 1.0 - 0.05, "se hundió: y={}", c.body.pos.y);
130        }
131        // Tras 10 s deambulando, se movió de donde arrancó.
132        let dx = c.body.pos.x - start.x;
133        let dz = c.body.pos.z - start.z;
134        assert!((dx * dx + dz * dz).sqrt() > 1.0, "no deambuló");
135    }
136
137    #[test]
138    fn rebota_y_queda_dentro_del_corral() {
139        // Corral 8×8 con paredes altas; el bicho debe quedar adentro.
140        let mut g = VoxelGrid::new([8, 6, 8]);
141        for z in 0..8 {
142            for x in 0..8 {
143                g.set(x, 0, z, [90, 140, 80]);
144                if x == 0 || x == 7 || z == 0 || z == 7 {
145                    for y in 1..6 {
146                        g.set(x, y, z, [120, 120, 120]);
147                    }
148                }
149            }
150        }
151        let mut c = Critter::spawn_on(&g, 4, 4, [220, 180, 120], 3);
152        for _ in 0..1200 {
153            c.step(&g, 1.0 / 60.0);
154        }
155        // Dentro del corral interior (1..7) con su medio-ancho.
156        assert!(c.body.pos.x > 1.0 - HALF[0] && c.body.pos.x < 7.0 + HALF[0], "x fuera: {}", c.body.pos.x);
157        assert!(c.body.pos.z > 1.0 - HALF[0] && c.body.pos.z < 7.0 + HALF[0], "z fuera: {}", c.body.pos.z);
158    }
159
160    #[test]
161    fn dos_semillas_distintas_divergen() {
162        let g = grid_con_piso();
163        let mut a = Critter::spawn_on(&g, 16, 16, [255; 3], 1);
164        let mut b = Critter::spawn_on(&g, 16, 16, [255; 3], 999);
165        for _ in 0..300 {
166            a.step(&g, 1.0 / 60.0);
167            b.step(&g, 1.0 / 60.0);
168        }
169        assert!((a.body.pos - b.body.pos).length() > 0.5, "semillas no divergen");
170    }
171}