feat: BodyTemplate system — data-driven entity editor for AI
BodyTemplate: JSON-serializable entity body definition. - parts: Vec<BodyPart> with x, y, color, label - constraints: Vec<(usize, usize)> connecting parts - half_w/half_h/radius: collider and physics params Built-in templates: - humanoid_player: purple wizard (15 parts) - humanoid_goblin: green (15 parts) - boulder: 4x4 rock (16 parts) API for AI agents: - BodyTemplate::to_json() / from_json() — serialize/deserialize - BodyTemplate::preview_ascii() — text preview of silhouette - BodyTemplate::apply_to(&mut entity, cx, cy) — build entity from template - template_for_kind(EntityKind) — get template by entity type - Custom templates: create any shape (snake, dragon, boss) from code or JSON Entity::build_humanoid() now delegates to template_for_kind().apply_to() — single source of truth for body layout. 7 new tests for template system (116 total, 0 failures)
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
use crate::entity::entity::{Entity, EntityKind};
|
||||
use crate::physics::verlet::SubBody;
|
||||
use crate::world::cell::MaterialId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct BodyPart {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub color: [u8; 4],
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct BodyTemplate {
|
||||
pub name: String,
|
||||
pub half_w: f32,
|
||||
pub half_h: f32,
|
||||
pub radius: f32,
|
||||
pub parts: Vec<BodyPart>,
|
||||
pub constraints: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl BodyTemplate {
|
||||
pub fn humanoid_player() -> Self {
|
||||
Self {
|
||||
name: "player".to_string(),
|
||||
half_w: 2.5,
|
||||
half_h: 3.5,
|
||||
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),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn humanoid_goblin() -> Self {
|
||||
Self {
|
||||
name: "goblin".to_string(),
|
||||
half_w: 2.5,
|
||||
half_h: 3.5,
|
||||
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),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn boulder() -> Self {
|
||||
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),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, entity: &mut Entity, cx: f32, cy: f32) {
|
||||
entity.bodies.clear();
|
||||
entity.constraints.clear();
|
||||
entity.rest_offsets.clear();
|
||||
entity.cx = cx;
|
||||
entity.cy = cy;
|
||||
entity.cvx = 0.0;
|
||||
entity.cvy = 0.0;
|
||||
entity.half_w = self.half_w;
|
||||
entity.half_h = self.half_h;
|
||||
|
||||
let mat = MaterialId::Flesh;
|
||||
for part in &self.parts {
|
||||
entity.rest_offsets.push((part.x, part.y));
|
||||
let mut b = SubBody::new(cx + part.x, cy + part.y, self.radius, mat);
|
||||
b.color = part.color;
|
||||
entity.bodies.push(b);
|
||||
}
|
||||
|
||||
for &(a, b) in &self.constraints {
|
||||
let dx = entity.bodies[a].x - entity.bodies[b].x;
|
||||
let dy = entity.bodies[a].y - entity.bodies[b].y;
|
||||
let len = (dx * dx + dy * dy).sqrt().max(0.5);
|
||||
entity
|
||||
.constraints
|
||||
.push(crate::physics::verlet::Constraint::new(a, b, len, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string_pretty(self).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn from_json(s: &str) -> Result<Self, String> {
|
||||
serde_json::from_str(s).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn preview_ascii(&self) -> String {
|
||||
if self.parts.is_empty() {
|
||||
return "(empty template)".to_string();
|
||||
}
|
||||
|
||||
let min_x = self.parts.iter().map(|p| p.x).fold(f32::INFINITY, f32::min);
|
||||
let max_x = self
|
||||
.parts
|
||||
.iter()
|
||||
.map(|p| p.x)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
let min_y = self.parts.iter().map(|p| p.y).fold(f32::INFINITY, f32::min);
|
||||
let max_y = self
|
||||
.parts
|
||||
.iter()
|
||||
.map(|p| p.y)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
|
||||
let w = ((max_x - min_x) as i32 + 3) as usize;
|
||||
let h = ((max_y - min_y) as i32 + 3) as usize;
|
||||
|
||||
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", '.'),
|
||||
]);
|
||||
|
||||
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(&'#');
|
||||
grid[gy as usize][gx as usize] = *ch;
|
||||
}
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
for row in grid {
|
||||
out.push_str(&row.iter().collect::<String>());
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template_for_kind(kind: EntityKind) -> BodyTemplate {
|
||||
match kind {
|
||||
EntityKind::Player => BodyTemplate::humanoid_player(),
|
||||
EntityKind::Goblin => BodyTemplate::humanoid_goblin(),
|
||||
EntityKind::Corpse => BodyTemplate::humanoid_player(),
|
||||
}
|
||||
}
|
||||
+2
-78
@@ -103,84 +103,8 @@ impl Entity {
|
||||
}
|
||||
|
||||
pub fn build_humanoid(&mut self, cx: f32, cy: f32) {
|
||||
self.bodies.clear();
|
||||
self.constraints.clear();
|
||||
self.rest_offsets.clear();
|
||||
self.cx = cx;
|
||||
self.cy = cy;
|
||||
self.cvx = 0.0;
|
||||
self.cvy = 0.0;
|
||||
self.half_w = 2.5;
|
||||
self.half_h = 3.5;
|
||||
|
||||
let r = 0.5;
|
||||
let mat = MaterialId::Flesh;
|
||||
|
||||
let (skin, shirt, pants, boots, hand) = match self.kind {
|
||||
EntityKind::Player => (
|
||||
[220, 200, 240, 255],
|
||||
[140, 80, 200, 255],
|
||||
[90, 50, 150, 255],
|
||||
[60, 35, 100, 255],
|
||||
[210, 185, 235, 255],
|
||||
),
|
||||
EntityKind::Goblin => (
|
||||
[130, 210, 100, 255],
|
||||
[55, 130, 45, 255],
|
||||
[40, 100, 30, 255],
|
||||
[25, 70, 18, 255],
|
||||
[110, 180, 85, 255],
|
||||
),
|
||||
EntityKind::Corpse => (
|
||||
[150, 130, 120, 255],
|
||||
[120, 100, 90, 255],
|
||||
[100, 85, 75, 255],
|
||||
[80, 65, 55, 255],
|
||||
[135, 115, 105, 255],
|
||||
),
|
||||
};
|
||||
|
||||
let layout: [(f32, f32, [u8; 4]); 15] = [
|
||||
(0.0, -3.0, skin),
|
||||
(0.0, -2.0, skin),
|
||||
(0.0, -1.0, shirt),
|
||||
(-1.0, -1.0, hand),
|
||||
(1.0, -1.0, hand),
|
||||
(0.0, 0.0, shirt),
|
||||
(-1.0, 0.0, hand),
|
||||
(1.0, 0.0, hand),
|
||||
(0.0, 1.0, pants),
|
||||
(-1.0, 2.0, pants),
|
||||
(1.0, 2.0, pants),
|
||||
(0.0, 2.0, pants),
|
||||
(-1.0, 3.0, boots),
|
||||
(0.0, 3.0, boots),
|
||||
(1.0, 3.0, boots),
|
||||
];
|
||||
|
||||
for &(ox, oy, color) in &layout {
|
||||
self.rest_offsets.push((ox, oy));
|
||||
let mut b = SubBody::new(cx + ox, cy + oy, r, mat);
|
||||
b.color = color;
|
||||
self.bodies.push(b);
|
||||
}
|
||||
|
||||
let mk = |a: usize, b: usize, len: f32| Constraint::new(a, b, len, 1.0);
|
||||
|
||||
self.constraints.push(mk(0, 1, 1.0));
|
||||
self.constraints.push(mk(1, 2, 1.0));
|
||||
self.constraints.push(mk(2, 5, 1.0));
|
||||
self.constraints.push(mk(5, 8, 1.0));
|
||||
self.constraints.push(mk(2, 3, 1.0));
|
||||
self.constraints.push(mk(2, 4, 1.0));
|
||||
self.constraints.push(mk(5, 6, 1.0));
|
||||
self.constraints.push(mk(5, 7, 1.0));
|
||||
self.constraints.push(mk(8, 11, 1.0));
|
||||
self.constraints.push(mk(11, 9, 1.0));
|
||||
self.constraints.push(mk(11, 10, 1.0));
|
||||
self.constraints.push(mk(9, 12, 1.0));
|
||||
self.constraints.push(mk(10, 14, 1.0));
|
||||
self.constraints.push(mk(11, 13, 1.0));
|
||||
let template = crate::entity::body_template::template_for_kind(self.kind);
|
||||
template.apply_to(self, cx, cy);
|
||||
}
|
||||
|
||||
pub fn sync_bodies_to_center(&mut self) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod entity;
|
||||
pub mod player;
|
||||
pub mod body_template;
|
||||
|
||||
pub use entity::{EntityManager, EntityKind};
|
||||
pub use player::Player;
|
||||
pub use body_template::{BodyTemplate, BodyPart, template_for_kind};
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
use verbatim::entity::body_template::{BodyPart, BodyTemplate};
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goblin_template_has_correct_shape() {
|
||||
let t = BodyTemplate::humanoid_goblin();
|
||||
assert_eq!(t.name, "goblin");
|
||||
assert_eq!(t.parts.len(), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boulder_template_has_correct_shape() {
|
||||
let t = BodyTemplate::boulder();
|
||||
assert_eq!(t.name, "boulder");
|
||||
assert_eq!(t.parts.len(), 16);
|
||||
assert!(t.parts.iter().all(|p| p.label == "rock"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_json_roundtrip() {
|
||||
let t = BodyTemplate::humanoid_player();
|
||||
let json = t.to_json();
|
||||
let t2 = BodyTemplate::from_json(&json).expect("parse");
|
||||
assert_eq!(t2.name, t.name);
|
||||
assert_eq!(t2.parts.len(), t.parts.len());
|
||||
assert_eq!(t2.half_w, t.half_w);
|
||||
assert_eq!(t2.half_h, t.half_h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_ascii_not_empty() {
|
||||
let t = BodyTemplate::humanoid_player();
|
||||
let preview = t.preview_ascii();
|
||||
assert!(!preview.is_empty());
|
||||
assert!(preview.contains("O"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_template_works() {
|
||||
let t = BodyTemplate {
|
||||
name: "snake".to_string(),
|
||||
half_w: 3.0,
|
||||
half_h: 0.5,
|
||||
radius: 0.5,
|
||||
parts: vec![
|
||||
BodyPart {
|
||||
x: -3.0,
|
||||
y: 0.0,
|
||||
color: [100, 200, 50, 255],
|
||||
label: "body".into(),
|
||||
},
|
||||
BodyPart {
|
||||
x: -2.0,
|
||||
y: 0.0,
|
||||
color: [100, 200, 50, 255],
|
||||
label: "body".into(),
|
||||
},
|
||||
BodyPart {
|
||||
x: -1.0,
|
||||
y: 0.0,
|
||||
color: [100, 200, 50, 255],
|
||||
label: "body".into(),
|
||||
},
|
||||
BodyPart {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
color: [100, 200, 50, 255],
|
||||
label: "body".into(),
|
||||
},
|
||||
BodyPart {
|
||||
x: 1.0,
|
||||
y: 0.0,
|
||||
color: [100, 200, 50, 255],
|
||||
label: "body".into(),
|
||||
},
|
||||
BodyPart {
|
||||
x: 2.0,
|
||||
y: 0.0,
|
||||
color: [100, 200, 50, 255],
|
||||
label: "body".into(),
|
||||
},
|
||||
BodyPart {
|
||||
x: 3.0,
|
||||
y: 0.0,
|
||||
color: [80, 180, 40, 255],
|
||||
label: "head".into(),
|
||||
},
|
||||
],
|
||||
constraints: vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)],
|
||||
};
|
||||
assert_eq!(t.parts.len(), 7);
|
||||
let json = t.to_json();
|
||||
let t2 = BodyTemplate::from_json(&json).expect("parse");
|
||||
assert_eq!(t2.parts.len(), 7);
|
||||
assert_eq!(t2.name, "snake");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_apply_to_entity() {
|
||||
use verbatim::entity::entity::{Entity, EntityKind};
|
||||
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_eq!(e.cx, 100.0);
|
||||
assert_eq!(e.cy, 100.0);
|
||||
assert!(!e.constraints.is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user