proof_engine/entity/mod.rs
1//! Amorphous entity rendering.
2//!
3//! Entities are not rigid sprites. They are clusters of Glyphs bound together
4//! by internal force fields. Their visual form is emergent from the binding forces.
5//! As HP decreases, cohesion drops and the entity visibly falls apart.
6
7pub mod formation;
8pub mod cohesion;
9pub mod ai;
10
11use crate::glyph::GlyphId;
12use crate::math::ForceField;
13use glam::{Vec3, Vec4};
14
15/// Opaque handle to an entity in the scene.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct EntityId(pub u32);
18
19/// An amorphous visual entity held together by binding forces.
20#[derive(Clone)]
21pub struct AmorphousEntity {
22 // ── Identity ─────────────────────────────────────────────────────────────
23 pub name: String,
24 pub position: Vec3,
25
26 // ── Binding ──────────────────────────────────────────────────────────────
27 /// The core force holding the entity together. Usually Gravity.
28 pub binding_field: ForceField,
29 /// IDs of glyphs that compose this entity.
30 pub glyph_ids: Vec<GlyphId>,
31 /// Target positions (relative to entity center) for each glyph.
32 pub formation: Vec<Vec3>,
33 /// Character assigned to each formation slot.
34 pub formation_chars: Vec<char>,
35 /// Color assigned to each formation slot.
36 pub formation_colors: Vec<Vec4>,
37
38 // ── Stats that drive visuals ──────────────────────────────────────────────
39 pub hp: f32,
40 pub max_hp: f32,
41 pub entity_mass: f32,
42 pub entity_temperature: f32,
43 pub entity_entropy: f32,
44
45 // ── Visual state ─────────────────────────────────────────────────────────
46 /// 0.0 = fully dispersed, 1.0 = tight formation.
47 /// Driven by hp/max_hp: cohesion = (hp / max_hp).sqrt()
48 pub cohesion: f32,
49
50 /// The entity's pulse function (breathing/heartbeat rhythm).
51 pub pulse_rate: f32, // Hz
52 pub pulse_depth: f32, // amplitude of pulse oscillation
53
54 // ── Internal animation time ───────────────────────────────────────────────
55 pub age: f32,
56
57 // ── Lifecycle ─────────────────────────────────────────────────────────────
58 /// Arbitrary string tags for filtering/grouping.
59 pub tags: Vec<String>,
60 /// Set to true to remove the entity on the next GC pass.
61 pub despawn_requested: bool,
62}
63
64impl AmorphousEntity {
65 pub fn new(name: impl Into<String>, position: Vec3) -> Self {
66 Self {
67 name: name.into(),
68 position,
69 binding_field: ForceField::Gravity {
70 center: position,
71 strength: 5.0,
72 falloff: crate::math::Falloff::InverseSquare,
73 },
74 glyph_ids: Vec::new(),
75 formation: Vec::new(),
76 formation_chars: Vec::new(),
77 formation_colors: Vec::new(),
78 hp: 100.0,
79 max_hp: 100.0,
80 entity_mass: 10.0,
81 entity_temperature: 0.5,
82 entity_entropy: 0.1,
83 cohesion: 1.0,
84 pulse_rate: 1.0,
85 pulse_depth: 0.05,
86 age: 0.0,
87 tags: Vec::new(),
88 despawn_requested: false,
89 }
90 }
91
92 /// HP fraction [0, 1].
93 pub fn hp_frac(&self) -> f32 {
94 (self.hp / self.max_hp.max(0.001)).clamp(0.0, 1.0)
95 }
96
97 /// Update cohesion based on current HP.
98 pub fn update_cohesion(&mut self) {
99 // Low HP = lower cohesion (entity falls apart)
100 self.cohesion = self.hp_frac().sqrt();
101 }
102
103 /// Advance entity time. Returns true if the entity should be removed (hp <= 0).
104 pub fn tick(&mut self, dt: f32, _time: f32) -> bool {
105 self.age += dt;
106 self.update_cohesion();
107 self.hp <= 0.0
108 }
109
110 /// Apply damage to this entity.
111 pub fn take_damage(&mut self, amount: f32) {
112 self.hp = (self.hp - amount).max(0.0);
113 }
114
115 /// Return true if the entity is dead.
116 pub fn is_dead(&self) -> bool { self.hp <= 0.0 }
117}
118
119impl Default for AmorphousEntity {
120 fn default() -> Self { Self::new("", Vec3::ZERO) }
121}