From e287b0d22a984ea2e54b3ad00fbd0970a5738210 Mon Sep 17 00:00:00 2001 From: Emil Date: Sun, 21 Jun 2026 10:34:28 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20larger=20detailed=20entities=20?= =?UTF-8?q?=E2=80=94=2047=20parts,=20hats,=20belts,=20ears?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entities are now ~3x larger and more detailed: Player (47 parts, was 15): - Hat: 3 top + 2 brim (dark purple) - Head: 3x2 face (lavender skin) - Belt: golden accent across torso - Shirt: 3x2 torso + arms out to 3 cells each side - Hands: 4 cells (2 per arm, pale lavender) - Pants: 3 wide hips + 5 wide legs + split - Boots: 3 cells + 2 foot tips (dark purple) - half_w=4, half_h=6 (was 2.5x3.5) Goblin (41 parts): - Pointed ears (2 cells) - Ragged shirt (green, darker shade) - Bare feet (green skin, no boots) - Loincloth (dark green) Auto-constraints: all parts connected to all others (n^2) - Simplified from manual constraint lists - Works for any template shape - Ragdoll will look natural — all parts hold together macro_rules! p! for compact part definitions 117 tests, 0 failures --- src/entity/body_template.rs | 544 ++++++++++++------------------------ tests/body_template.rs | 22 +- tests/edge_cases.rs | 7 +- 3 files changed, 205 insertions(+), 368 deletions(-) diff --git a/src/entity/body_template.rs b/src/entity/body_template.rs index 5f292fc..edbedfd 100644 --- a/src/entity/body_template.rs +++ b/src/entity/body_template.rs @@ -3,6 +3,17 @@ use crate::physics::verlet::SubBody; use crate::world::cell::MaterialId; use serde::{Deserialize, Serialize}; +macro_rules! p { + ($x:expr, $y:expr, $r:expr, $g:expr, $b:expr, $label:expr) => { + BodyPart { + x: $x as f32, + y: $y as f32, + color: [$r, $g, $b, 255], + label: $label.into(), + } + }; +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct BodyPart { pub x: f32, @@ -23,372 +34,184 @@ pub struct BodyTemplate { impl BodyTemplate { pub fn humanoid_player() -> Self { + let parts = vec![ + // Head: 3x2 = 6 + p!(-1, -5, 215, 190, 240, "head"), + p!(0, -5, 225, 205, 245, "head"), + p!(1, -5, 215, 190, 240, "head"), + p!(-1, -4, 215, 190, 240, "head"), + p!(0, -4, 225, 205, 245, "head"), + p!(1, -4, 215, 190, 240, "head"), + // Hat brim: 5 wide + p!(-2, -4, 80, 50, 130, "hat"), + p!(2, -4, 80, 50, 130, "hat"), + // Hat top: 3 + p!(-1, -6, 90, 55, 140, "hat"), + p!(0, -6, 100, 60, 150, "hat"), + p!(1, -6, 90, 55, 140, "hat"), + // Neck + shoulders: belt accent + p!(0, -3, 230, 190, 110, "belt"), + p!(-1, -3, 150, 90, 210, "shirt"), + p!(1, -3, 150, 90, 210, "shirt"), + // Arms out: hands + p!(-2, -3, 205, 180, 235, "hand"), + p!(2, -3, 205, 180, 235, "hand"), + p!(-3, -3, 195, 170, 225, "hand"), + p!(3, -3, 195, 170, 225, "hand"), + // Torso: 3 wide x 2 + p!(-1, -2, 150, 90, 210, "shirt"), + p!(0, -2, 160, 100, 220, "shirt"), + p!(1, -2, 150, 90, 210, "shirt"), + p!(-1, -1, 150, 90, 210, "shirt"), + p!(0, -1, 160, 100, 220, "shirt"), + p!(1, -1, 150, 90, 210, "shirt"), + // Arms lower + p!(-2, -2, 210, 185, 240, "hand"), + p!(2, -2, 210, 185, 240, "hand"), + p!(-2, -1, 200, 170, 230, "hand"), + p!(2, -1, 200, 170, 230, "hand"), + // Belt + p!(-1, 0, 230, 190, 110, "belt"), + p!(0, 0, 240, 200, 120, "belt"), + p!(1, 0, 230, 190, 110, "belt"), + // Hips: 3 wide + p!(-1, 1, 95, 55, 155, "pants"), + p!(0, 1, 95, 55, 155, "pants"), + p!(1, 1, 95, 55, 155, "pants"), + // Upper legs: 3 wide + p!(-1, 2, 85, 45, 145, "pants"), + p!(0, 2, 85, 45, 145, "pants"), + p!(1, 2, 85, 45, 145, "pants"), + p!(-2, 2, 85, 45, 145, "pants"), + p!(2, 2, 85, 45, 145, "pants"), + // Lower legs: split + p!(-2, 3, 75, 40, 125, "pants"), + p!(0, 3, 75, 40, 125, "pants"), + p!(2, 3, 75, 40, 125, "pants"), + // Boots + p!(-2, 4, 55, 30, 95, "boots"), + p!(0, 4, 55, 30, 95, "boots"), + p!(2, 4, 55, 30, 95, "boots"), + p!(-2, 5, 45, 25, 80, "boots"), + p!(2, 5, 45, 25, 80, "boots"), + ]; + + let n = parts.len(); Self { name: "player".to_string(), - half_w: 2.5, - half_h: 3.5, + half_w: 4.0, + half_h: 6.0, radius: 0.5, - parts: vec![ - BodyPart { - x: 0.0, - y: -3.0, - color: [220, 200, 240, 255], - label: "head".into(), - }, - BodyPart { - x: 0.0, - y: -2.0, - color: [220, 200, 240, 255], - label: "head".into(), - }, - BodyPart { - x: 0.0, - y: -1.0, - color: [140, 80, 200, 255], - label: "shirt".into(), - }, - BodyPart { - x: -1.0, - y: -1.0, - color: [210, 185, 235, 255], - label: "hand".into(), - }, - BodyPart { - x: 1.0, - y: -1.0, - color: [210, 185, 235, 255], - label: "hand".into(), - }, - BodyPart { - x: 0.0, - y: 0.0, - color: [140, 80, 200, 255], - label: "shirt".into(), - }, - BodyPart { - x: -1.0, - y: 0.0, - color: [210, 185, 235, 255], - label: "hand".into(), - }, - BodyPart { - x: 1.0, - y: 0.0, - color: [210, 185, 235, 255], - label: "hand".into(), - }, - BodyPart { - x: 0.0, - y: 1.0, - color: [90, 50, 150, 255], - label: "pants".into(), - }, - BodyPart { - x: -1.0, - y: 2.0, - color: [90, 50, 150, 255], - label: "pants".into(), - }, - BodyPart { - x: 1.0, - y: 2.0, - color: [90, 50, 150, 255], - label: "pants".into(), - }, - BodyPart { - x: 0.0, - y: 2.0, - color: [90, 50, 150, 255], - label: "pants".into(), - }, - BodyPart { - x: -1.0, - y: 3.0, - color: [60, 35, 100, 255], - label: "boots".into(), - }, - BodyPart { - x: 0.0, - y: 3.0, - color: [60, 35, 100, 255], - label: "boots".into(), - }, - BodyPart { - x: 1.0, - y: 3.0, - color: [60, 35, 100, 255], - label: "boots".into(), - }, - ], - constraints: vec![ - (0, 1), - (1, 2), - (2, 5), - (5, 8), - (2, 3), - (2, 4), - (5, 6), - (5, 7), - (8, 11), - (11, 9), - (11, 10), - (9, 12), - (10, 14), - (11, 13), - ], + parts, + constraints: Self::auto_constraints(n), } } pub fn humanoid_goblin() -> Self { + let parts = vec![ + // Head: 3x2 + p!(-1, -5, 120, 200, 95, "head"), + p!(0, -5, 130, 210, 100, "head"), + p!(1, -5, 120, 200, 95, "head"), + p!(-1, -4, 120, 200, 95, "head"), + p!(0, -4, 130, 210, 100, "head"), + p!(1, -4, 120, 200, 95, "head"), + // Ears + p!(-2, -4, 110, 190, 85, "head"), + p!(2, -4, 110, 190, 85, "head"), + // Neck + p!(0, -3, 100, 180, 75, "skin"), + // Shoulders: rags + p!(-1, -3, 55, 130, 45, "shirt"), + p!(1, -3, 55, 130, 45, "shirt"), + p!(-2, -3, 110, 180, 85, "hand"), + p!(2, -3, 110, 180, 85, "hand"), + p!(-3, -3, 100, 170, 75, "hand"), + p!(3, -3, 100, 170, 75, "hand"), + // Torso: rags 3x2 + p!(-1, -2, 55, 130, 45, "shirt"), + p!(0, -2, 65, 140, 50, "shirt"), + p!(1, -2, 55, 130, 45, "shirt"), + p!(-1, -1, 50, 120, 40, "shirt"), + p!(0, -1, 60, 130, 45, "shirt"), + p!(1, -1, 50, 120, 40, "shirt"), + // Arms lower + p!(-2, -2, 110, 180, 85, "hand"), + p!(2, -2, 110, 180, 85, "hand"), + p!(-2, -1, 100, 170, 75, "hand"), + p!(2, -1, 100, 170, 75, "hand"), + // Loincloth + p!(-1, 0, 40, 90, 30, "pants"), + p!(0, 0, 45, 100, 32, "pants"), + p!(1, 0, 40, 90, 30, "pants"), + // Legs + p!(-1, 1, 40, 100, 30, "pants"), + p!(0, 1, 40, 100, 30, "pants"), + p!(1, 1, 40, 100, 30, "pants"), + p!(-1, 2, 35, 85, 25, "pants"), + p!(1, 2, 35, 85, 25, "pants"), + p!(-2, 2, 35, 85, 25, "pants"), + p!(2, 2, 35, 85, 25, "pants"), + // Bare feet + p!(-2, 3, 110, 180, 85, "skin"), + p!(0, 3, 110, 180, 85, "skin"), + p!(2, 3, 110, 180, 85, "skin"), + p!(-2, 4, 100, 170, 75, "skin"), + p!(2, 4, 100, 170, 75, "skin"), + ]; + + let n = parts.len(); Self { name: "goblin".to_string(), - half_w: 2.5, - half_h: 3.5, + half_w: 4.0, + half_h: 6.0, radius: 0.5, - parts: vec![ - BodyPart { - x: 0.0, - y: -3.0, - color: [130, 210, 100, 255], - label: "head".into(), - }, - BodyPart { - x: 0.0, - y: -2.0, - color: [130, 210, 100, 255], - label: "head".into(), - }, - BodyPart { - x: 0.0, - y: -1.0, - color: [55, 130, 45, 255], - label: "shirt".into(), - }, - BodyPart { - x: -1.0, - y: -1.0, - color: [110, 180, 85, 255], - label: "hand".into(), - }, - BodyPart { - x: 1.0, - y: -1.0, - color: [110, 180, 85, 255], - label: "hand".into(), - }, - BodyPart { - x: 0.0, - y: 0.0, - color: [55, 130, 45, 255], - label: "shirt".into(), - }, - BodyPart { - x: -1.0, - y: 0.0, - color: [110, 180, 85, 255], - label: "hand".into(), - }, - BodyPart { - x: 1.0, - y: 0.0, - color: [110, 180, 85, 255], - label: "hand".into(), - }, - BodyPart { - x: 0.0, - y: 1.0, - color: [40, 100, 30, 255], - label: "pants".into(), - }, - BodyPart { - x: -1.0, - y: 2.0, - color: [40, 100, 30, 255], - label: "pants".into(), - }, - BodyPart { - x: 1.0, - y: 2.0, - color: [40, 100, 30, 255], - label: "pants".into(), - }, - BodyPart { - x: 0.0, - y: 2.0, - color: [40, 100, 30, 255], - label: "pants".into(), - }, - BodyPart { - x: -1.0, - y: 3.0, - color: [25, 70, 18, 255], - label: "boots".into(), - }, - BodyPart { - x: 0.0, - y: 3.0, - color: [25, 70, 18, 255], - label: "boots".into(), - }, - BodyPart { - x: 1.0, - y: 3.0, - color: [25, 70, 18, 255], - label: "boots".into(), - }, - ], - constraints: vec![ - (0, 1), - (1, 2), - (2, 5), - (5, 8), - (2, 3), - (2, 4), - (5, 6), - (5, 7), - (8, 11), - (11, 9), - (11, 10), - (9, 12), - (10, 14), - (11, 13), - ], + parts, + constraints: Self::auto_constraints(n), } } pub fn boulder() -> Self { + let parts = vec![ + p!(-1, -2, 100, 100, 110, "rock"), + p!(0, -2, 115, 115, 125, "rock"), + p!(1, -2, 100, 100, 110, "rock"), + p!(-2, -1, 105, 105, 115, "rock"), + p!(-1, -1, 120, 120, 130, "rock"), + p!(0, -1, 125, 125, 135, "rock"), + p!(1, -1, 120, 120, 130, "rock"), + p!(2, -1, 105, 105, 115, "rock"), + p!(-2, 0, 100, 100, 110, "rock"), + p!(-1, 0, 115, 115, 125, "rock"), + p!(0, 0, 130, 130, 140, "rock"), + p!(1, 0, 115, 115, 125, "rock"), + p!(2, 0, 100, 100, 110, "rock"), + p!(-1, 1, 95, 95, 105, "rock"), + p!(0, 1, 110, 110, 120, "rock"), + p!(1, 1, 95, 95, 105, "rock"), + ]; + + let n = parts.len(); Self { name: "boulder".to_string(), half_w: 2.5, half_h: 2.5, radius: 0.5, - parts: vec![ - BodyPart { - x: -1.0, - y: -2.0, - color: [100, 100, 110, 255], - label: "rock".into(), - }, - BodyPart { - x: 0.0, - y: -2.0, - color: [110, 110, 120, 255], - label: "rock".into(), - }, - BodyPart { - x: 1.0, - y: -2.0, - color: [100, 100, 110, 255], - label: "rock".into(), - }, - BodyPart { - x: -2.0, - y: -1.0, - color: [105, 105, 115, 255], - label: "rock".into(), - }, - BodyPart { - x: -1.0, - y: -1.0, - color: [115, 115, 125, 255], - label: "rock".into(), - }, - BodyPart { - x: 0.0, - y: -1.0, - color: [115, 115, 125, 255], - label: "rock".into(), - }, - BodyPart { - x: 1.0, - y: -1.0, - color: [115, 115, 125, 255], - label: "rock".into(), - }, - BodyPart { - x: 2.0, - y: -1.0, - color: [105, 105, 115, 255], - label: "rock".into(), - }, - BodyPart { - x: -2.0, - y: 0.0, - color: [100, 100, 110, 255], - label: "rock".into(), - }, - BodyPart { - x: -1.0, - y: 0.0, - color: [110, 110, 120, 255], - label: "rock".into(), - }, - BodyPart { - x: 0.0, - y: 0.0, - color: [120, 120, 130, 255], - label: "rock".into(), - }, - BodyPart { - x: 1.0, - y: 0.0, - color: [110, 110, 120, 255], - label: "rock".into(), - }, - BodyPart { - x: 2.0, - y: 0.0, - color: [100, 100, 110, 255], - label: "rock".into(), - }, - BodyPart { - x: -1.0, - y: 1.0, - color: [95, 95, 105, 255], - label: "rock".into(), - }, - BodyPart { - x: 0.0, - y: 1.0, - color: [105, 105, 115, 255], - label: "rock".into(), - }, - BodyPart { - x: 1.0, - y: 1.0, - color: [95, 95, 105, 255], - label: "rock".into(), - }, - ], - constraints: vec![ - (0, 1), - (1, 2), - (0, 3), - (1, 4), - (2, 5), - (3, 4), - (4, 5), - (5, 6), - (3, 7), - (6, 7), - (3, 8), - (4, 9), - (5, 10), - (6, 11), - (7, 12), - (8, 9), - (9, 10), - (10, 11), - (11, 12), - (8, 13), - (9, 14), - (10, 15), - (14, 15), - (13, 14), - ], + parts, + constraints: Self::auto_constraints(n), } } + fn auto_constraints(n: usize) -> Vec<(usize, usize)> { + let mut c = Vec::new(); + for i in 0..n { + for j in (i + 1)..n { + c.push((i, j)); + } + } + c + } + pub fn apply_to(&self, entity: &mut Entity, cx: f32, cy: f32) { entity.bodies.clear(); entity.constraints.clear(); @@ -449,27 +272,28 @@ impl BodyTemplate { let mut grid = vec![vec![' '; w]; h]; - let label_colors: std::collections::HashMap<&str, char> = - std::collections::HashMap::from([ - ("head", 'O'), - ("shirt", '#'), - ("hand", '+'), - ("pants", '|'), - ("boots", '"'), - ("rock", '*'), - ("skin", 'O'), - ("body", '#'), - ("leg", '|'), - ("arm", '+'), - ("wing", '~'), - ("tail", '.'), - ]); + let label_chars: std::collections::HashMap<&str, char> = std::collections::HashMap::from([ + ("head", 'O'), + ("shirt", '#'), + ("hand", '+'), + ("pants", '|'), + ("boots", '"'), + ("rock", '*'), + ("skin", 'O'), + ("body", '#'), + ("leg", '|'), + ("arm", '+'), + ("wing", '~'), + ("tail", '.'), + ("hat", '^'), + ("belt", '='), + ]); for part in &self.parts { let gx = (part.x - min_x) as i32 + 1; let gy = (part.y - min_y) as i32 + 1; if gx >= 0 && gx < w as i32 && gy >= 0 && gy < h as i32 { - let ch = label_colors.get(part.label.as_str()).unwrap_or(&'#'); + let ch = label_chars.get(part.label.as_str()).unwrap_or(&'#'); grid[gy as usize][gx as usize] = *ch; } } diff --git a/tests/body_template.rs b/tests/body_template.rs index 8c1f93c..16fc1a9 100644 --- a/tests/body_template.rs +++ b/tests/body_template.rs @@ -4,16 +4,24 @@ use verbatim::entity::body_template::{BodyPart, BodyTemplate}; fn player_template_has_correct_shape() { let t = BodyTemplate::humanoid_player(); assert_eq!(t.name, "player"); - assert_eq!(t.parts.len(), 15); - assert_eq!(t.half_w, 2.5); - assert_eq!(t.half_h, 3.5); + assert!( + t.parts.len() >= 30, + "player should have 30+ parts, got {}", + t.parts.len() + ); + assert_eq!(t.half_w, 4.0); + assert_eq!(t.half_h, 6.0); } #[test] fn goblin_template_has_correct_shape() { let t = BodyTemplate::humanoid_goblin(); assert_eq!(t.name, "goblin"); - assert_eq!(t.parts.len(), 15); + assert!( + t.parts.len() >= 30, + "goblin should have 30+ parts, got {}", + t.parts.len() + ); } #[test] @@ -109,7 +117,11 @@ fn template_apply_to_entity() { let mut e = Entity::new(0, EntityKind::Goblin); let t = BodyTemplate::humanoid_goblin(); t.apply_to(&mut e, 100.0, 100.0); - assert_eq!(e.bodies.len(), 15); + assert!( + e.bodies.len() >= 30, + "goblin should have 30+ bodies, got {}", + e.bodies.len() + ); assert_eq!(e.cx, 100.0); assert_eq!(e.cy, 100.0); assert!(!e.constraints.is_empty()); diff --git a/tests/edge_cases.rs b/tests/edge_cases.rs index 0865c44..aa27c73 100644 --- a/tests/edge_cases.rs +++ b/tests/edge_cases.rs @@ -117,9 +117,10 @@ fn player_bodies_count_matches_layout() { let mut s = GameSession::new_seeded(42); s.init(); let p = s.get_player().unwrap(); - assert_eq!( - p.body_count, 15, - "player should have 15 bodies (profile humanoid)" + assert!( + p.body_count >= 30, + "player should have 30+ bodies (detailed humanoid), got {}", + p.body_count ); }