use bevy::prelude::*;
use bevy_map_core::{ComponentOverrides, EntityInstance, Value};
use std::collections::HashMap;
use std::marker::PhantomData;
use uuid::Uuid;
pub trait MapEntityType: Component + Sized + Send + Sync + 'static {
fn type_name() -> &'static str;
fn from_instance(instance: &EntityInstance) -> Self;
fn sprite_properties() -> &'static [&'static str] {
&[]
}
fn inject_sprite_handle(
&mut self,
_property_name: &str,
_handle: bevy::prelude::Handle<bevy::prelude::Image>,
) {
}
}
#[derive(Component)]
pub struct MapEntityMarker {
pub instance_id: Uuid,
pub type_name: String,
}
#[derive(Component, Debug, Clone)]
pub struct EntityProperties {
pub properties: HashMap<String, Value>,
pub component_overrides: ComponentOverrides,
}
impl EntityProperties {
pub fn get_string(&self, key: &str) -> Option<&str> {
self.properties.get(key).and_then(|v| v.as_string())
}
pub fn get_int(&self, key: &str) -> Option<i64> {
self.properties.get(key).and_then(|v| v.as_int())
}
pub fn get_float(&self, key: &str) -> Option<f64> {
self.properties.get(key).and_then(|v| v.as_float())
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.properties.get(key).and_then(|v| v.as_bool())
}
pub fn get_array(&self, key: &str) -> Option<&Vec<Value>> {
self.properties.get(key).and_then(|v| v.as_array())
}
pub fn get_object(&self, key: &str) -> Option<&HashMap<String, Value>> {
self.properties.get(key).and_then(|v| v.as_object())
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.properties.get(key)
}
pub fn has(&self, key: &str) -> bool {
self.properties.contains_key(key)
}
pub fn get_vec2(&self, key: &str) -> Option<Vec2> {
let arr = self.properties.get(key).and_then(|v| v.as_array())?;
if arr.len() >= 2 {
let x = arr[0].as_float()? as f32;
let y = arr[1].as_float()? as f32;
Some(Vec2::new(x, y))
} else {
None
}
}
}
#[derive(Component, Debug, Clone)]
pub struct Dialogue {
pub dialogue_id: String,
}
trait EntitySpawner: Send + Sync {
fn spawn(&self, commands: &mut Commands, instance: &EntityInstance, transform: Transform);
}
struct TypedSpawner<T: MapEntityType> {
_marker: PhantomData<T>,
}
impl<T: MapEntityType> EntitySpawner for TypedSpawner<T> {
fn spawn(&self, commands: &mut Commands, instance: &EntityInstance, transform: Transform) {
let component = T::from_instance(instance);
let color = instance
.get_string("_editor_color")
.and_then(parse_hex_color)
.unwrap_or(Color::srgba(0.2, 0.6, 1.0, 0.8));
let marker_size = instance.get_float("_editor_marker_size").unwrap_or(16.0) as f32;
commands.spawn((
component,
transform,
Visibility::default(),
Sprite {
color,
custom_size: Some(Vec2::splat(marker_size)),
..default()
},
MapEntityMarker {
instance_id: instance.id,
type_name: instance.type_name.clone(),
},
EntityProperties {
properties: instance.properties.clone(),
component_overrides: instance.component_overrides.clone(),
},
));
}
}
fn parse_hex_color(hex: &str) -> Option<Color> {
let hex = hex.trim_start_matches('#');
if hex.len() == 6 {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(Color::srgba(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
0.8,
))
} else if hex.len() == 8 {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
Some(Color::srgba(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a as f32 / 255.0,
))
} else {
None
}
}
#[derive(Resource, Default)]
pub struct EntityRegistry {
spawners: HashMap<String, Box<dyn EntitySpawner>>,
}
impl EntityRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register<T: MapEntityType>(&mut self) {
self.spawners.insert(
T::type_name().to_string(),
Box::new(TypedSpawner::<T> {
_marker: PhantomData,
}),
);
}
pub fn is_registered(&self, type_name: &str) -> bool {
self.spawners.contains_key(type_name)
}
pub fn len(&self) -> usize {
self.spawners.len()
}
pub fn is_empty(&self) -> bool {
self.spawners.is_empty()
}
pub fn spawn(
&self,
commands: &mut Commands,
instance: &EntityInstance,
base_transform: Transform,
) -> bool {
let entity_transform =
base_transform * Transform::from_xyz(instance.position[0], instance.position[1], 0.0);
if let Some(spawner) = self.spawners.get(&instance.type_name) {
spawner.spawn(commands, instance, entity_transform);
true
} else {
commands.spawn((
entity_transform,
Visibility::default(),
Sprite {
color: Color::srgba(1.0, 0.2, 0.2, 0.8), custom_size: Some(Vec2::splat(16.0)),
..default()
},
MapEntityMarker {
instance_id: instance.id,
type_name: instance.type_name.clone(),
},
EntityProperties {
properties: instance.properties.clone(),
component_overrides: instance.component_overrides.clone(),
},
));
false
}
}
pub fn spawn_all(
&self,
commands: &mut Commands,
instances: &[EntityInstance],
base_transform: Transform,
) -> usize {
let mut unregistered = 0;
for instance in instances {
if !self.spawn(commands, instance, base_transform) {
warn!(
"Entity type '{}' not registered - spawned with red placeholder (use .register_map_entity::<YourType>() to register)",
instance.type_name
);
unregistered += 1;
}
}
unregistered
}
}
pub trait MapEntityExt {
fn register_map_entity<T: MapEntityType>(&mut self) -> &mut Self;
}
impl MapEntityExt for App {
fn register_map_entity<T: MapEntityType>(&mut self) -> &mut Self {
if !self.world().contains_resource::<EntityRegistry>() {
self.insert_resource(EntityRegistry::new());
}
self.world_mut()
.resource_mut::<EntityRegistry>()
.register::<T>();
self
}
}
pub fn attach_dialogues(
mut commands: Commands,
query: Query<(Entity, &EntityProperties), Without<Dialogue>>,
) {
for (entity, props) in query.iter() {
for (key, value) in &props.properties {
let is_dialogue_prop = key == "dialogue" || key.ends_with("_dialogue");
if !is_dialogue_prop {
continue;
}
let dialogue_id = match value {
Value::String(id) => Some(id.clone()),
Value::Object(obj) => obj.get("id").and_then(|v| {
if let Value::String(id) = v {
Some(id.clone())
} else {
None
}
}),
_ => None,
};
if let Some(id) = dialogue_id {
if !id.is_empty() {
commands.entity(entity).insert(Dialogue { dialogue_id: id });
break; }
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Component)]
#[allow(dead_code)]
struct TestEntity {
name: String,
health: i32,
}
impl MapEntityType for TestEntity {
fn type_name() -> &'static str {
"TestEntity"
}
fn from_instance(instance: &EntityInstance) -> Self {
Self {
name: instance.get_string("name").unwrap_or("Unknown").to_string(),
health: instance.get_int("health").unwrap_or(100) as i32,
}
}
}
#[test]
fn test_registry_register() {
let mut registry = EntityRegistry::new();
assert!(registry.is_empty());
registry.register::<TestEntity>();
assert!(!registry.is_empty());
assert_eq!(registry.len(), 1);
assert!(registry.is_registered("TestEntity"));
assert!(!registry.is_registered("OtherEntity"));
}
}