tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
1//! **Objeto potencial**: una cosa colocada en el mundo en su forma *latente* que,
2//! ante un disparador (aquí: el aterrizaje/nacimiento), **eclosiona** en la entidad
3//! que será. El primero es el [`Egg`] — un *huevo* que lleva adentro su potencial
4//! ([`Hatchling`]: qué nace, con qué edad y colores) y al abrirse **da a luz** un
5//! [`Actor`] recién nacido.
6//!
7//! Es la pieza del corto: la cámara cae, ve el huevo, y al tocar suelo el huevo se
8//! abre y nace el niño. La forma generaliza (otros objetos potenciales podrían nacer
9//! plantas, animales, estructuras); hoy el caso es el *huevito humano*.
10
11use llimphi_3d::glam::{Mat4, Vec3};
12use llimphi_3d::{push_cube, Vertex3d};
13
14use crate::actor::{Actor, Age};
15
16/// `T(center) · S(size)` — caja centrada en `center` escalada a `size`.
17fn trs(center: Vec3, size: Vec3) -> Mat4 {
18 Mat4::from_translation(center) * Mat4::from_scale(size)
19}
20
21/// **Lo que un huevo va a ser**: el potencial latente. Hoy un personaje (edad +
22/// colores); el motor lo materializa como [`Actor`] al nacer.
23#[derive(Debug, Clone, Copy)]
24pub struct Hatchling {
25 /// Edad con la que nace (el corto: [`Age::Baby`]).
26 pub age: Age,
27 pub skin: [f32; 3],
28 pub shirt: [f32; 3],
29 pub pants: [f32; 3],
30}
31
32impl Hatchling {
33 /// Un **huevito humano**: nace bebé, con una paleta cálida por defecto.
34 pub fn human(age: Age) -> Self {
35 Self {
36 age,
37 skin: [0.90, 0.74, 0.60],
38 shirt: [0.86, 0.46, 0.44],
39 pants: [0.22, 0.24, 0.32],
40 }
41 }
42}
43
44/// **Huevo** — un objeto potencial con forma de ovoide voxel. `hatch` va de `0`
45/// (intacto) a `1` (abierto: la tapa superior se levanta y se inclina, dejando
46/// salir al recién nacido). `pos` es la **base** del huevo en mundo.
47#[derive(Debug, Clone, Copy)]
48pub struct Egg {
49 /// Base del huevo, en mundo (mismas coords que el terreno/actor).
50 pub pos: Vec3,
51 /// Rumbo (yaw) — orienta al recién nacido y la apertura.
52 pub facing: f32,
53 /// Altura del huevo.
54 pub size: f32,
55 /// Color de la cáscara.
56 pub shell: [f32; 3],
57 /// Progreso de eclosión `[0,1]` (0 = intacto, 1 = abierto).
58 pub hatch: f32,
59 /// Qué nace de él.
60 pub becomes: Hatchling,
61}
62
63/// Fracción de la altura donde el huevo se "raja" (la tapa de arriba es la que se
64/// abre).
65const CRACK: f32 = 0.55;
66/// Slabs verticales con los que se aproxima el ovoide.
67const SLABS: usize = 9;
68
69impl Egg {
70 /// Huevo intacto en `pos` (base) de altura `size`, con el potencial `becomes`.
71 pub fn new(pos: Vec3, size: f32, becomes: Hatchling) -> Self {
72 Self { pos, facing: 0.0, size, shell: [0.94, 0.92, 0.86], hatch: 0.0, becomes }
73 }
74
75 /// Avanza la eclosión `dt` segundos a `rate` (`1/seg` ⇒ se abre en ~1 s).
76 pub fn advance(&mut self, dt: f32, rate: f32) {
77 self.hatch = (self.hatch + dt * rate).clamp(0.0, 1.0);
78 }
79
80 /// `true` cuando el huevo terminó de abrirse.
81 pub fn is_open(&self) -> bool {
82 self.hatch >= 1.0
83 }
84
85 /// Matriz de ubicación en mundo (igual rol que [`Actor::model`]).
86 pub fn model(&self) -> Mat4 {
87 Mat4::from_translation(self.pos) * Mat4::from_rotation_y(self.facing)
88 }
89
90 /// **Da a luz**: materializa el potencial como un [`Actor`] recién nacido, en la
91 /// posición y rumbo del huevo, con la edad/colores de [`Hatchling`].
92 pub fn newborn(&self) -> Actor {
93 Actor::new(self.pos, self.facing)
94 .with_age(self.becomes.age)
95 .with_colors(self.becomes.skin, self.becomes.shirt, self.becomes.pants)
96 }
97
98 /// Malla del cascarón en espacio local (base en el origen, ubicar con
99 /// [`model`](Self::model)). Ovoide por slabs horizontales; la tapa por encima de
100 /// [`CRACK`] **se separa** (se levanta y se inclina) según `hatch`, bisagra en la
101 /// línea de la rajadura.
102 pub fn mesh(&self) -> (Vec<Vertex3d>, Vec<u16>) {
103 let mut v = Vec::with_capacity(8 * SLABS);
104 let mut i = Vec::with_capacity(36 * SLABS);
105 let half_w = self.size * 0.42; // medio-ancho máximo (en la panza)
106 let thick = self.size / SLABS as f32;
107 let pivot = Vec3::new(0.0, CRACK * self.size, 0.0);
108
109 // Transform de la tapa: bisagra en la rajadura, se levanta y se inclina.
110 let lift = self.hatch * self.size * 0.75;
111 let tilt = self.hatch * 0.9;
112 let cap = Mat4::from_translation(pivot + Vec3::new(0.0, lift, 0.0))
113 * Mat4::from_rotation_z(tilt)
114 * Mat4::from_translation(-pivot);
115
116 for k in 0..SLABS {
117 let tc = (k as f32 + 0.5) / SLABS as f32; // centro del slab en [0,1]
118 // Medio-ancho del ovoide (elipse): 0 en los polos, máx en la panza.
119 let r = (half_w * (4.0 * tc * (1.0 - tc)).sqrt()).max(self.size * 0.04);
120 let y = tc * self.size;
121 let base = trs(Vec3::new(0.0, y, 0.0), Vec3::new(r * 2.0, thick * 1.04, r * 2.0));
122 let m = if tc > CRACK { cap * base } else { base };
123 push_cube(&mut v, &mut i, m, self.shell);
124 }
125 (v, i)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn el_recien_nacido_hereda_edad_y_lugar() {
135 let pos = Vec3::new(5.0, -2.0, 3.0);
136 let egg = Egg::new(pos, 1.0, Hatchling::human(Age::Baby));
137 let baby = egg.newborn();
138 assert_eq!(baby.age, Age::Baby);
139 assert_eq!(baby.pos, pos);
140 }
141
142 #[test]
143 fn la_eclosion_avanza_y_se_satura() {
144 let mut egg = Egg::new(Vec3::ZERO, 1.0, Hatchling::human(Age::Baby));
145 assert!(!egg.is_open());
146 egg.advance(0.5, 1.0);
147 assert!((egg.hatch - 0.5).abs() < 1e-5);
148 egg.advance(10.0, 1.0); // se pasa → se clampa
149 assert!(egg.is_open() && egg.hatch == 1.0);
150 }
151
152 #[test]
153 fn al_abrirse_la_tapa_se_levanta() {
154 let intact = Egg::new(Vec3::ZERO, 2.0, Hatchling::human(Age::Baby));
155 let mut open = intact;
156 open.hatch = 1.0;
157 let top = |e: &Egg| e.mesh().0.iter().map(|v| v.pos[1]).fold(f32::MIN, f32::max);
158 assert!(top(&open) > top(&intact) + 0.5, "la tapa abierta queda más alta: {} vs {}", top(&open), top(&intact));
159 }
160}