tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 333dca1d5fb473825c9128d402d4cc4e2a5176fc
llimphi-voxel/src/critter.rs
History
Who touched what
raw
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
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}