Skip to main content

eldiron_shared/
project.rs

1use crate::prelude::*;
2use buildergraph::BuilderGraph;
3use codegridfx::Module;
4use indexmap::IndexMap;
5pub use rusterix::map::*;
6use theframework::prelude::*;
7use tilegraph::TileGraphPaletteSource;
8
9/// The default target fps for the game.
10fn default_target_fps() -> u32 {
11    30
12}
13
14/// The default ms per tick for the game.
15fn default_tick_ms() -> u32 {
16    250
17}
18
19fn default_rules() -> String {
20    String::new()
21}
22
23fn default_locales() -> String {
24    String::new()
25}
26
27fn default_audio_fx() -> String {
28    String::new()
29}
30
31fn default_authoring() -> String {
32    String::new()
33}
34
35fn default_world_module() -> Module {
36    Module::as_type(codegridfx::ModuleType::World)
37}
38
39fn default_tile_board_cols() -> i32 {
40    13
41}
42
43fn default_tile_board_rows() -> i32 {
44    9
45}
46
47fn default_tile_collection_name() -> String {
48    "New Collection".to_string()
49}
50
51fn default_tile_collection_version() -> String {
52    "0.1".to_string()
53}
54
55fn default_palette_material_slots() -> Vec<PaletteMaterial> {
56    vec![PaletteMaterial::default(); 256]
57}
58
59#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
60pub struct PaletteMaterial {
61    #[serde(default = "default_palette_roughness")]
62    pub roughness: f32,
63    #[serde(default = "default_palette_metallic")]
64    pub metallic: f32,
65    #[serde(default = "default_palette_opacity")]
66    pub opacity: f32,
67    #[serde(default = "default_palette_emissive")]
68    pub emissive: f32,
69}
70
71fn default_palette_roughness() -> f32 {
72    0.5
73}
74
75fn default_palette_metallic() -> f32 {
76    0.0
77}
78
79fn default_palette_opacity() -> f32 {
80    1.0
81}
82
83fn default_palette_emissive() -> f32 {
84    0.0
85}
86
87impl Default for PaletteMaterial {
88    fn default() -> Self {
89        Self {
90            roughness: default_palette_roughness(),
91            metallic: default_palette_metallic(),
92            opacity: default_palette_opacity(),
93            emissive: default_palette_emissive(),
94        }
95    }
96}
97
98#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
99pub struct BuilderGraphAsset {
100    pub id: Uuid,
101    pub graph_id: Uuid,
102    #[serde(default)]
103    pub graph_name: String,
104    #[serde(default)]
105    pub graph_data: String,
106}
107
108impl BuilderGraphAsset {
109    pub fn new_table(name: String) -> Self {
110        let graph_data = BuilderGraph::preset_table_script_named(name.clone());
111        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
112        {
113            document.name().to_string()
114        } else if name.is_empty() {
115            "Table".to_string()
116        } else {
117            name.clone()
118        };
119        Self {
120            id: Uuid::new_v4(),
121            graph_id: Uuid::new_v4(),
122            graph_name,
123            graph_data,
124        }
125    }
126
127    pub fn new_empty(name: String) -> Self {
128        let graph_data = BuilderGraph::empty_script_named(name.clone());
129        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
130        {
131            document.name().to_string()
132        } else if name.is_empty() {
133            "Empty".to_string()
134        } else {
135            name.clone()
136        };
137        Self {
138            id: Uuid::new_v4(),
139            graph_id: Uuid::new_v4(),
140            graph_name,
141            graph_data,
142        }
143    }
144
145    pub fn new_wall_torch(name: String) -> Self {
146        let graph_data = BuilderGraph::preset_wall_torch_script_named(name.clone());
147        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
148        {
149            document.name().to_string()
150        } else if name.is_empty() {
151            "Wall Torch".to_string()
152        } else {
153            name.clone()
154        };
155        Self {
156            id: Uuid::new_v4(),
157            graph_id: Uuid::new_v4(),
158            graph_name,
159            graph_data,
160        }
161    }
162
163    pub fn new_wall_lantern(name: String) -> Self {
164        let graph_data = BuilderGraph::preset_wall_lantern_script_named(name.clone());
165        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
166        {
167            document.name().to_string()
168        } else if name.is_empty() {
169            "Wall Lantern".to_string()
170        } else {
171            name.clone()
172        };
173        Self {
174            id: Uuid::new_v4(),
175            graph_id: Uuid::new_v4(),
176            graph_name,
177            graph_data,
178        }
179    }
180
181    pub fn new_campfire(name: String) -> Self {
182        let graph_data = BuilderGraph::preset_campfire_script_named(name.clone());
183        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
184        {
185            document.name().to_string()
186        } else if name.is_empty() {
187            "Campfire".to_string()
188        } else {
189            name.clone()
190        };
191        Self {
192            id: Uuid::new_v4(),
193            graph_id: Uuid::new_v4(),
194            graph_name,
195            graph_data,
196        }
197    }
198}
199
200#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
201pub struct NodeGroupAsset {
202    pub group_id: Uuid,
203    pub graph_id: Uuid,
204    #[serde(default)]
205    pub graph_name: String,
206    pub output_grid_width: u16,
207    pub output_grid_height: u16,
208    pub tile_pixel_width: u16,
209    pub tile_pixel_height: u16,
210    #[serde(default)]
211    pub palette_source: TileGraphPaletteSource,
212    #[serde(default)]
213    pub palette_colors: Vec<TheColor>,
214    #[serde(default)]
215    pub graph_data: String,
216}
217
218impl NodeGroupAsset {
219    pub fn new(
220        group_id: Uuid,
221        output_grid_width: u16,
222        output_grid_height: u16,
223        palette_colors: Vec<TheColor>,
224    ) -> Self {
225        Self {
226            group_id,
227            graph_id: Uuid::new_v4(),
228            graph_name: "New Node Graph".to_string(),
229            output_grid_width,
230            output_grid_height,
231            tile_pixel_width: 32,
232            tile_pixel_height: 32,
233            palette_source: TileGraphPaletteSource::Local,
234            palette_colors,
235            graph_data: String::new(),
236        }
237    }
238}
239
240#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
241pub enum TileCollectionEntry {
242    SingleTile(Uuid),
243    TileGroup(Uuid),
244}
245
246impl TileCollectionEntry {
247    pub fn matches_source(&self, source: rusterix::TileSource) -> bool {
248        match (self, source) {
249            (Self::SingleTile(a), rusterix::TileSource::SingleTile(b)) => *a == b,
250            (Self::TileGroup(a), rusterix::TileSource::TileGroup(b)) => *a == b,
251            _ => false,
252        }
253    }
254}
255
256#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
257pub struct TileCollectionAsset {
258    pub id: Uuid,
259    #[serde(default = "default_tile_collection_name")]
260    pub name: String,
261    #[serde(default)]
262    pub author: String,
263    #[serde(default = "default_tile_collection_version")]
264    pub version: String,
265    #[serde(default)]
266    pub description: String,
267    #[serde(default)]
268    pub entries: Vec<TileCollectionEntry>,
269    #[serde(default)]
270    pub tile_board_tiles: IndexMap<Uuid, Vec2<i32>>,
271    #[serde(default)]
272    pub tile_board_groups: IndexMap<Uuid, Vec2<i32>>,
273    #[serde(default)]
274    pub tile_board_empty_slots: Vec<Vec2<i32>>,
275}
276
277impl TileCollectionAsset {
278    pub fn new(name: String) -> Self {
279        Self {
280            id: Uuid::new_v4(),
281            name,
282            author: String::new(),
283            version: default_tile_collection_version(),
284            description: String::new(),
285            entries: Vec::new(),
286            tile_board_tiles: IndexMap::default(),
287            tile_board_groups: IndexMap::default(),
288            tile_board_empty_slots: Vec::new(),
289        }
290    }
291}
292
293#[derive(Serialize, Deserialize, Clone, Debug)]
294pub struct Project {
295    pub name: String,
296    pub regions: Vec<Region>,
297    pub tilemaps: Vec<Tilemap>,
298
299    /// Tiles in the project
300    #[serde(default)]
301    pub tiles: IndexMap<Uuid, rusterix::Tile>,
302
303    /// Spatial tile groups in the project.
304    #[serde(default)]
305    pub tile_groups: IndexMap<Uuid, rusterix::TileGroup>,
306
307    /// Node-backed tile groups keyed by tile-group id.
308    #[serde(default)]
309    pub tile_node_groups: IndexMap<Uuid, NodeGroupAsset>,
310
311    /// Standalone builder graphs for props and assemblies.
312    #[serde(default)]
313    pub builder_graphs: IndexMap<Uuid, BuilderGraphAsset>,
314
315    /// Custom top-level tile collections shown as tabs in the tile picker.
316    #[serde(default)]
317    pub tile_collections: IndexMap<Uuid, TileCollectionAsset>,
318
319    /// Persisted board positions for top-level single tiles in the tile picker.
320    #[serde(default)]
321    pub tile_board_tiles: IndexMap<Uuid, Vec2<i32>>,
322
323    /// Persisted board positions for top-level tile groups in the tile picker.
324    #[serde(default)]
325    pub tile_board_groups: IndexMap<Uuid, Vec2<i32>>,
326
327    /// Persisted empty board cells left behind by deletions in the tile picker.
328    #[serde(default)]
329    pub tile_board_empty_slots: Vec<Vec2<i32>>,
330
331    /// Total board width in cells, including the trailing empty strip.
332    #[serde(default = "default_tile_board_cols")]
333    pub tile_board_cols: i32,
334
335    /// Total board height in cells, including the trailing empty strip.
336    #[serde(default = "default_tile_board_rows")]
337    pub tile_board_rows: i32,
338
339    #[serde(default)]
340    pub time: TheTime,
341
342    #[serde(default)]
343    pub characters: IndexMap<Uuid, Character>,
344    #[serde(default)]
345    pub items: IndexMap<Uuid, Item>,
346
347    #[serde(default)]
348    pub screens: IndexMap<Uuid, Screen>,
349
350    #[serde(default)]
351    pub assets: IndexMap<Uuid, Asset>,
352
353    #[serde(default)]
354    pub palette: ThePalette,
355
356    #[serde(default = "default_target_fps")]
357    pub target_fps: u32,
358
359    #[serde(default = "default_tick_ms")]
360    pub tick_ms: u32,
361
362    #[serde(default)]
363    pub config: String,
364
365    #[serde(default = "default_world_module")]
366    pub world_module: Module,
367
368    #[serde(default)]
369    pub world_source: String,
370
371    #[serde(default)]
372    pub world_source_debug: String,
373
374    #[serde(default = "default_rules")]
375    pub rules: String,
376
377    #[serde(default = "default_locales")]
378    pub locales: String,
379
380    #[serde(default = "default_audio_fx")]
381    pub audio_fx: String,
382
383    #[serde(default = "default_authoring")]
384    pub authoring: String,
385
386    #[serde(default)]
387    pub avatars: IndexMap<Uuid, Avatar>,
388
389    #[serde(default = "default_palette_material_slots")]
390    pub palette_materials: Vec<PaletteMaterial>,
391}
392
393impl Default for Project {
394    fn default() -> Self {
395        Self::new()
396    }
397}
398
399impl Project {
400    pub fn new() -> Self {
401        let region = Region::default();
402
403        Self {
404            name: String::new(),
405
406            regions: vec![region],
407            tilemaps: vec![],
408
409            tiles: IndexMap::default(),
410            tile_groups: IndexMap::default(),
411            tile_node_groups: IndexMap::default(),
412            builder_graphs: IndexMap::default(),
413            tile_collections: IndexMap::default(),
414            tile_board_tiles: IndexMap::default(),
415            tile_board_groups: IndexMap::default(),
416            tile_board_empty_slots: Vec::new(),
417            tile_board_cols: default_tile_board_cols(),
418            tile_board_rows: default_tile_board_rows(),
419
420            time: TheTime::default(),
421
422            characters: IndexMap::default(),
423            items: IndexMap::default(),
424
425            screens: IndexMap::default(),
426            assets: IndexMap::default(),
427
428            palette: ThePalette::default(),
429
430            target_fps: default_target_fps(),
431            tick_ms: default_tick_ms(),
432
433            avatars: IndexMap::default(),
434            palette_materials: default_palette_material_slots(),
435
436            config: String::new(),
437            world_module: default_world_module(),
438            world_source: String::new(),
439            world_source_debug: String::new(),
440            rules: default_rules(),
441            locales: default_locales(),
442            audio_fx: default_audio_fx(),
443            authoring: default_authoring(),
444        }
445    }
446
447    /// Add Character
448    pub fn add_character(&mut self, character: Character) {
449        self.characters.insert(character.id, character);
450    }
451
452    pub fn ensure_palette_materials_len(&mut self) {
453        if self.palette_materials.len() < self.palette.colors.len() {
454            self.palette_materials
455                .resize(self.palette.colors.len(), PaletteMaterial::default());
456        } else if self.palette_materials.len() > self.palette.colors.len() {
457            self.palette_materials.truncate(self.palette.colors.len());
458        }
459    }
460
461    pub fn reset_palette_material(&mut self, index: usize) {
462        self.ensure_palette_materials_len();
463        if let Some(material) = self.palette_materials.get_mut(index) {
464            *material = PaletteMaterial::default();
465        }
466    }
467
468    pub fn reset_all_palette_materials(&mut self) {
469        self.palette_materials = default_palette_material_slots();
470        self.ensure_palette_materials_len();
471    }
472
473    /// Removes the given character from the project.
474    pub fn remove_character(&mut self, id: &Uuid) {
475        self.characters.shift_remove(id);
476    }
477
478    /// Returns a list of all characters sorted by name.
479    pub fn sorted_character_list(&self) -> Vec<(Uuid, String)> {
480        let mut entries: Vec<(Uuid, String)> = self
481            .characters
482            .iter()
483            .map(|(uuid, data)| (*uuid, data.name.clone()))
484            .collect();
485
486        entries.sort_by(|a, b| a.1.cmp(&b.1));
487        entries
488    }
489
490    /// Returns a list of all items sorted by name.
491    pub fn sorted_item_list(&self) -> Vec<(Uuid, String)> {
492        let mut entries: Vec<(Uuid, String)> = self
493            .items
494            .iter()
495            .map(|(uuid, data)| (*uuid, data.name.clone()))
496            .collect();
497
498        entries.sort_by(|a, b| a.1.cmp(&b.1));
499        entries
500    }
501
502    /// Add Avatar
503    pub fn add_avatar(&mut self, avatar: Avatar) {
504        self.avatars.insert(avatar.id, avatar);
505    }
506
507    pub fn add_tile_group(&mut self, tile_group: rusterix::TileGroup) {
508        self.tile_groups.insert(tile_group.id, tile_group);
509    }
510
511    pub fn add_tile_node_group(&mut self, node_group: NodeGroupAsset) {
512        self.tile_node_groups
513            .insert(node_group.group_id, node_group);
514    }
515
516    pub fn add_builder_graph(&mut self, builder_graph: BuilderGraphAsset) {
517        self.builder_graphs.insert(builder_graph.id, builder_graph);
518    }
519
520    pub fn add_tile_collection(&mut self, collection: TileCollectionAsset) {
521        self.tile_collections.insert(collection.id, collection);
522    }
523
524    pub fn is_tile_node_group(&self, id: &Uuid) -> bool {
525        self.tile_node_groups.contains_key(id)
526    }
527
528    pub fn collection_contains_source(
529        &self,
530        collection_id: &Uuid,
531        source: rusterix::TileSource,
532    ) -> bool {
533        self.tile_collections
534            .get(collection_id)
535            .map(|collection| {
536                collection
537                    .entries
538                    .iter()
539                    .any(|entry| entry.matches_source(source))
540            })
541            .unwrap_or(false)
542    }
543
544    pub fn add_source_to_collection(&mut self, collection_id: &Uuid, source: rusterix::TileSource) {
545        let Some(collection) = self.tile_collections.get_mut(collection_id) else {
546            return;
547        };
548        let entry = match source {
549            rusterix::TileSource::SingleTile(id) => TileCollectionEntry::SingleTile(id),
550            rusterix::TileSource::TileGroup(id) => TileCollectionEntry::TileGroup(id),
551            _ => return,
552        };
553        if !collection.entries.contains(&entry) {
554            collection.entries.push(entry);
555        }
556    }
557
558    pub fn remove_source_from_collections(&mut self, source: rusterix::TileSource) {
559        for collection in self.tile_collections.values_mut() {
560            collection
561                .entries
562                .retain(|entry| !entry.matches_source(source));
563            match source {
564                rusterix::TileSource::SingleTile(id) => {
565                    collection.tile_board_tiles.shift_remove(&id);
566                }
567                rusterix::TileSource::TileGroup(id) => {
568                    collection.tile_board_groups.shift_remove(&id);
569                }
570                _ => {}
571            }
572        }
573    }
574
575    pub fn remove_tile_group(&mut self, id: &Uuid) {
576        self.tile_groups.shift_remove(id);
577        self.tile_node_groups.shift_remove(id);
578        self.tile_board_groups.shift_remove(id);
579        self.remove_source_from_collections(rusterix::TileSource::TileGroup(*id));
580    }
581
582    pub fn tile_board_position(&self, source: rusterix::TileSource) -> Option<Vec2<i32>> {
583        match source {
584            rusterix::TileSource::SingleTile(id) => self.tile_board_tiles.get(&id).copied(),
585            rusterix::TileSource::TileGroup(id) => self.tile_board_groups.get(&id).copied(),
586            _ => None,
587        }
588    }
589
590    pub fn collection_tile_board_position(
591        &self,
592        collection_id: &Uuid,
593        source: rusterix::TileSource,
594    ) -> Option<Vec2<i32>> {
595        let collection = self.tile_collections.get(collection_id)?;
596        match source {
597            rusterix::TileSource::SingleTile(id) => collection.tile_board_tiles.get(&id).copied(),
598            rusterix::TileSource::TileGroup(id) => collection.tile_board_groups.get(&id).copied(),
599            _ => None,
600        }
601    }
602
603    pub fn tile_board_empty_slots(&self) -> &[Vec2<i32>] {
604        &self.tile_board_empty_slots
605    }
606
607    pub fn collection_tile_board_empty_slots(&self, collection_id: &Uuid) -> Option<&[Vec2<i32>]> {
608        self.tile_collections
609            .get(collection_id)
610            .map(|collection| collection.tile_board_empty_slots.as_slice())
611    }
612
613    pub fn set_tile_board_position(&mut self, source: rusterix::TileSource, pos: Vec2<i32>) {
614        self.clear_tile_board_empty_slot(pos);
615        match source {
616            rusterix::TileSource::SingleTile(id) => {
617                self.tile_board_tiles.insert(id, pos);
618            }
619            rusterix::TileSource::TileGroup(id) => {
620                self.tile_board_groups.insert(id, pos);
621            }
622            _ => {}
623        }
624    }
625
626    pub fn set_collection_tile_board_position(
627        &mut self,
628        collection_id: &Uuid,
629        source: rusterix::TileSource,
630        pos: Vec2<i32>,
631    ) {
632        let Some(collection) = self.tile_collections.get_mut(collection_id) else {
633            return;
634        };
635        if let Some(index) = collection
636            .tile_board_empty_slots
637            .iter()
638            .position(|p| *p == pos)
639        {
640            collection.tile_board_empty_slots.swap_remove(index);
641        }
642        match source {
643            rusterix::TileSource::SingleTile(id) => {
644                collection.tile_board_tiles.insert(id, pos);
645            }
646            rusterix::TileSource::TileGroup(id) => {
647                collection.tile_board_groups.insert(id, pos);
648            }
649            _ => {}
650        }
651    }
652
653    pub fn reserve_tile_board_empty_slot(&mut self, pos: Vec2<i32>) {
654        if !self.tile_board_empty_slots.contains(&pos) {
655            self.tile_board_empty_slots.push(pos);
656        }
657    }
658
659    pub fn reserve_collection_tile_board_empty_slot(
660        &mut self,
661        collection_id: &Uuid,
662        pos: Vec2<i32>,
663    ) {
664        let Some(collection) = self.tile_collections.get_mut(collection_id) else {
665            return;
666        };
667        if !collection.tile_board_empty_slots.contains(&pos) {
668            collection.tile_board_empty_slots.push(pos);
669        }
670    }
671
672    pub fn clear_tile_board_empty_slot(&mut self, pos: Vec2<i32>) {
673        if let Some(index) = self.tile_board_empty_slots.iter().position(|p| *p == pos) {
674            self.tile_board_empty_slots.swap_remove(index);
675        }
676    }
677
678    pub fn ensure_tile_board_space(&mut self, pos: Vec2<i32>) {
679        if pos.x >= self.tile_board_cols - 1 {
680            self.tile_board_cols = pos.x + 2;
681        }
682        if pos.y >= self.tile_board_rows - 1 {
683            self.tile_board_rows = pos.y + 2;
684        }
685    }
686
687    /// Removes the given avatar from the project.
688    pub fn remove_avatar(&mut self, id: &Uuid) {
689        self.avatars.shift_remove(id);
690    }
691
692    /// Finds the avatar that contains the given animation id.
693    pub fn find_avatar_for_animation(&self, animation_id: &Uuid) -> Option<&Avatar> {
694        self.avatars
695            .values()
696            .find(|a| a.animations.iter().any(|anim| anim.id == *animation_id))
697    }
698
699    /// Returns an immutable reference to the texture identified by the editing context.
700    pub fn get_editing_texture(
701        &self,
702        editing_ctx: &PixelEditingContext,
703    ) -> Option<&rusterix::Texture> {
704        match editing_ctx {
705            PixelEditingContext::None => None,
706            PixelEditingContext::Tile(tile_id, frame_index) => {
707                let tile = self.tiles.get(tile_id)?;
708                tile.textures.get(*frame_index)
709            }
710            PixelEditingContext::AvatarFrame(
711                avatar_id,
712                anim_id,
713                perspective_index,
714                frame_index,
715            ) => {
716                let avatar = self.avatars.get(avatar_id)?;
717                let anim = avatar.animations.iter().find(|a| a.id == *anim_id)?;
718                let perspective = anim.perspectives.get(*perspective_index)?;
719                perspective.frames.get(*frame_index).map(|f| &f.texture)
720            }
721        }
722    }
723
724    /// Returns a mutable reference to the texture identified by the editing context.
725    pub fn get_editing_texture_mut(
726        &mut self,
727        editing_ctx: &PixelEditingContext,
728    ) -> Option<&mut rusterix::Texture> {
729        match editing_ctx {
730            PixelEditingContext::None => None,
731            PixelEditingContext::Tile(tile_id, frame_index) => {
732                let tile = self.tiles.get_mut(tile_id)?;
733                tile.textures.get_mut(*frame_index)
734            }
735            PixelEditingContext::AvatarFrame(
736                avatar_id,
737                anim_id,
738                perspective_index,
739                frame_index,
740            ) => {
741                let avatar = self.avatars.get_mut(avatar_id)?;
742                let anim = avatar.animations.iter_mut().find(|a| a.id == *anim_id)?;
743                let perspective = anim.perspectives.get_mut(*perspective_index)?;
744                perspective
745                    .frames
746                    .get_mut(*frame_index)
747                    .map(|f| &mut f.texture)
748            }
749        }
750    }
751
752    /// Returns an immutable avatar frame for avatar frame editing contexts.
753    pub fn get_editing_avatar_frame(
754        &self,
755        editing_ctx: &PixelEditingContext,
756    ) -> Option<&rusterix::AvatarAnimationFrame> {
757        match editing_ctx {
758            PixelEditingContext::AvatarFrame(
759                avatar_id,
760                anim_id,
761                perspective_index,
762                frame_index,
763            ) => {
764                let avatar = self.avatars.get(avatar_id)?;
765                let anim = avatar.animations.iter().find(|a| a.id == *anim_id)?;
766                let perspective = anim.perspectives.get(*perspective_index)?;
767                perspective.frames.get(*frame_index)
768            }
769            _ => None,
770        }
771    }
772
773    /// Returns a mutable avatar frame for avatar frame editing contexts.
774    pub fn get_editing_avatar_frame_mut(
775        &mut self,
776        editing_ctx: &PixelEditingContext,
777    ) -> Option<&mut rusterix::AvatarAnimationFrame> {
778        match editing_ctx {
779            PixelEditingContext::AvatarFrame(
780                avatar_id,
781                anim_id,
782                perspective_index,
783                frame_index,
784            ) => {
785                let avatar = self.avatars.get_mut(avatar_id)?;
786                let anim = avatar.animations.iter_mut().find(|a| a.id == *anim_id)?;
787                let perspective = anim.perspectives.get_mut(*perspective_index)?;
788                perspective.frames.get_mut(*frame_index)
789            }
790            _ => None,
791        }
792    }
793
794    /// Returns an immutable avatar perspective for avatar frame editing contexts.
795    pub fn get_editing_avatar_perspective(
796        &self,
797        editing_ctx: &PixelEditingContext,
798    ) -> Option<&rusterix::AvatarPerspective> {
799        match editing_ctx {
800            PixelEditingContext::AvatarFrame(avatar_id, anim_id, perspective_index, _) => {
801                let avatar = self.avatars.get(avatar_id)?;
802                let anim = avatar.animations.iter().find(|a| a.id == *anim_id)?;
803                anim.perspectives.get(*perspective_index)
804            }
805            _ => None,
806        }
807    }
808
809    /// Returns a mutable avatar perspective for avatar frame editing contexts.
810    pub fn get_editing_avatar_perspective_mut(
811        &mut self,
812        editing_ctx: &PixelEditingContext,
813    ) -> Option<&mut rusterix::AvatarPerspective> {
814        match editing_ctx {
815            PixelEditingContext::AvatarFrame(avatar_id, anim_id, perspective_index, _) => {
816                let avatar = self.avatars.get_mut(avatar_id)?;
817                let anim = avatar.animations.iter_mut().find(|a| a.id == *anim_id)?;
818                anim.perspectives.get_mut(*perspective_index)
819            }
820            _ => None,
821        }
822    }
823
824    /// Add Item
825    pub fn add_item(&mut self, item: Item) {
826        self.items.insert(item.id, item);
827    }
828
829    /// Removes the given item from the project.
830    pub fn remove_item(&mut self, id: &Uuid) {
831        self.items.shift_remove(id);
832    }
833
834    /// Add a tilemap
835    pub fn add_tilemap(&mut self, tilemap: Tilemap) {
836        self.tilemaps.push(tilemap)
837    }
838
839    /// Get the tilemap of the given uuid.
840    pub fn get_tilemap(&self, uuid: Uuid) -> Option<&Tilemap> {
841        self.tilemaps.iter().find(|t| t.id == uuid)
842    }
843
844    /// Get the tilemap of the given uuid.
845    pub fn get_tilemap_mut(&mut self, uuid: Uuid) -> Option<&mut Tilemap> {
846        self.tilemaps.iter_mut().find(|t| t.id == uuid)
847    }
848
849    /// Removes the given tilemap from the project.
850    pub fn remove_tilemap(&mut self, id: TheId) {
851        self.tilemaps.retain(|item| item.id != id.uuid);
852    }
853
854    /// Contains the region of the given uuid.
855    pub fn contains_region(&self, uuid: &Uuid) -> bool {
856        self.regions.iter().find(|t| t.id == *uuid).is_some()
857    }
858
859    /// Get the region of the given uuid.
860    pub fn get_region(&self, uuid: &Uuid) -> Option<&Region> {
861        self.regions.iter().find(|t| t.id == *uuid)
862    }
863
864    /// Get the region of the given uuid as mutable.
865    pub fn get_region_mut(&mut self, uuid: &Uuid) -> Option<&mut Region> {
866        self.regions.iter_mut().find(|t| t.id == *uuid)
867    }
868
869    /// Get the region of the given uuid.
870    pub fn get_region_ctx(&self, ctx: &ServerContext) -> Option<&Region> {
871        self.regions.iter().find(|t| t.id == ctx.curr_region)
872    }
873
874    /// Get the region of the given uuid as mutable.
875    pub fn get_region_ctx_mut(&mut self, ctx: &ServerContext) -> Option<&mut Region> {
876        self.regions.iter_mut().find(|t| t.id == ctx.curr_region)
877    }
878
879    /// Get the screen of the given uuid.
880    pub fn get_screen_ctx(&self, ctx: &ServerContext) -> Option<&Screen> {
881        self.screens.get(&ctx.curr_screen)
882    }
883
884    /// Get the mut screen of the given uuid.
885    pub fn get_screen_ctx_mut(&mut self, ctx: &ServerContext) -> Option<&mut Screen> {
886        self.screens.get_mut(&ctx.curr_screen)
887    }
888
889    /// Remove a region
890    pub fn remove_region(&mut self, id: &Uuid) {
891        self.regions.retain(|item| item.id != *id);
892    }
893
894    /// Get the map of the current context.
895    pub fn get_map(&self, ctx: &ServerContext) -> Option<&Map> {
896        if ctx.editor_view_mode != EditorViewMode::D2 {
897            if let Some(region) = self.get_region(&ctx.curr_region) {
898                if ctx.geometry_edit_mode == GeometryEditMode::Detail {
899                    if let Some(surface) = ctx.active_detail_surface.as_ref() {
900                        if let Some(surface) = region.map.surfaces.get(&surface.id) {
901                            if let Some(profile_id) = surface.profile {
902                                return region.map.profiles.get(&profile_id);
903                            }
904                        }
905                    }
906                }
907                return Some(&region.map);
908            }
909        } else if ctx.get_map_context() == MapContext::Region {
910            let id = ctx.curr_region;
911            // if let Some(id) = ctx.pc.id() {
912            if let Some(surface) = &ctx.editing_surface {
913                if let Some(region) = self.regions.iter().find(|t| t.id == id) {
914                    if let Some(surface) = region.map.surfaces.get(&surface.id) {
915                        if let Some(profile_id) = surface.profile {
916                            return region.map.profiles.get(&profile_id);
917                        }
918                    }
919                }
920                return None;
921            } else if let Some(region) = self.regions.iter().find(|t| t.id == id) {
922                return Some(&region.map);
923            }
924            // }
925        } else if ctx.get_map_context() == MapContext::Screen {
926            if let Some(id) = ctx.pc.id() {
927                if let Some(screen) = self.screens.get(&id) {
928                    return Some(&screen.map);
929                }
930            }
931        } else if ctx.get_map_context() == MapContext::Character {
932            if let ContentContext::CharacterTemplate(id) = ctx.curr_character {
933                if let Some(character) = self.characters.get(&id) {
934                    return Some(&character.map);
935                }
936            }
937        } else if ctx.get_map_context() == MapContext::Item {
938            if let ContentContext::ItemTemplate(id) = ctx.curr_item {
939                if let Some(item) = self.items.get(&id) {
940                    return Some(&item.map);
941                }
942            }
943        }
944        None
945    }
946
947    /// Get the mutable map of the current context.
948    pub fn get_map_mut(&mut self, ctx: &ServerContext) -> Option<&mut Map> {
949        if ctx.get_map_context() == MapContext::Region {
950            let id = ctx.curr_region;
951            // if let Some(id) = ctx.pc.id() {
952            if ctx.editor_view_mode != EditorViewMode::D2 {
953                if let Some(region) = self.get_region_mut(&ctx.curr_region) {
954                    if ctx.geometry_edit_mode == GeometryEditMode::Detail {
955                        if let Some(surface) = ctx.active_detail_surface.as_ref() {
956                            if let Some(surface) = region.map.surfaces.get_mut(&surface.id) {
957                                if let Some(profile_id) = surface.profile {
958                                    return region.map.profiles.get_mut(&profile_id);
959                                }
960                            }
961                        }
962                    }
963                    return Some(&mut region.map);
964                }
965            } else if let Some(surface) = &ctx.editing_surface {
966                if let Some(region) = self.regions.iter_mut().find(|t| t.id == id) {
967                    if let Some(surface) = region.map.surfaces.get_mut(&surface.id) {
968                        if let Some(profile_id) = surface.profile {
969                            return region.map.profiles.get_mut(&profile_id);
970                        }
971                    }
972                }
973                return None;
974            } else if let Some(region) = self.regions.iter_mut().find(|t| t.id == id) {
975                return Some(&mut region.map);
976            }
977            // }
978        } else if ctx.get_map_context() == MapContext::Screen {
979            if let Some(id) = ctx.pc.id() {
980                if let Some(screen) = self.screens.get_mut(&id) {
981                    return Some(&mut screen.map);
982                }
983            }
984        } else if ctx.get_map_context() == MapContext::Character {
985            if let ContentContext::CharacterTemplate(id) = ctx.curr_character {
986                if let Some(character) = self.characters.get_mut(&id) {
987                    return Some(&mut character.map);
988                }
989            }
990        } else if ctx.get_map_context() == MapContext::Item {
991            if let ContentContext::ItemTemplate(id) = ctx.curr_item {
992                if let Some(item) = self.items.get_mut(&id) {
993                    return Some(&mut item.map);
994                }
995            }
996        }
997        None
998    }
999
1000    /// Add Screen
1001    pub fn add_screen(&mut self, screen: Screen) {
1002        self.screens.insert(screen.id, screen);
1003    }
1004
1005    /// Removes the given code from the project.
1006    pub fn remove_screen(&mut self, id: &Uuid) {
1007        self.screens.shift_remove(id);
1008    }
1009
1010    /// Returns a list of all screens sorted by name.
1011    pub fn sorted_screens_list(&self) -> Vec<(Uuid, String)> {
1012        let mut entries: Vec<(Uuid, String)> = self
1013            .screens
1014            .iter()
1015            .map(|(uuid, data)| (*uuid, data.name.clone()))
1016            .collect();
1017
1018        entries.sort_by(|a, b| a.1.cmp(&b.1));
1019        entries
1020    }
1021
1022    /// Add an asset
1023    pub fn add_asset(&mut self, asset: Asset) {
1024        self.assets.insert(asset.id, asset);
1025    }
1026
1027    /// Removes the given code from the project.
1028    pub fn remove_asset(&mut self, id: &Uuid) {
1029        self.assets.shift_remove(id);
1030    }
1031
1032    /// Returns a list of all assets sorted by name.
1033    pub fn sorted_assets_list(&self) -> Vec<(Uuid, String)> {
1034        let mut entries: Vec<(Uuid, String)> = self
1035            .assets
1036            .iter()
1037            .map(|(uuid, data)| (*uuid, data.name.clone()))
1038            .collect();
1039
1040        entries.sort_by(|a, b| a.1.cmp(&b.1));
1041        entries
1042    }
1043
1044    /// Removes the given tile from the project.
1045    pub fn remove_tile(&mut self, id: &Uuid) {
1046        self.tiles.shift_remove(id);
1047    }
1048
1049    /// Gets the given tile from the project.
1050    pub fn get_tile(&self, id: &Uuid) -> Option<&rusterix::Tile> {
1051        self.tiles.get(id)
1052    }
1053
1054    /// Gets the given mutable tile from the project.
1055    pub fn get_tile_mut(&mut self, id: &Uuid) -> Option<&mut rusterix::Tile> {
1056        self.tiles.get_mut(id)
1057    }
1058
1059    pub fn find_tile_id_by_alias(&self, alias: &str) -> Option<Uuid> {
1060        let needle = alias.trim();
1061        if needle.is_empty() {
1062            return None;
1063        }
1064
1065        let matches_alias = |value: &str| {
1066            value
1067                .split([',', ';', '\n'])
1068                .map(str::trim)
1069                .any(|part| !part.is_empty() && part.eq_ignore_ascii_case(needle))
1070        };
1071
1072        for (id, tile) in &self.tiles {
1073            if matches_alias(&tile.alias) {
1074                return Some(*id);
1075            }
1076        }
1077
1078        None
1079    }
1080}