konnektoren_bevy/assets/
mod.rs

1pub mod challenge_asset;
2pub mod level_asset;
3
4use bevy::prelude::*;
5pub use challenge_asset::*;
6pub use level_asset::*;
7use std::collections::HashMap;
8
9/// Plugin for loading Konnektoren assets (challenges, levels)
10/// This plugin is focused on data loading only - no game logic
11pub struct KonnektorenAssetsPlugin;
12
13impl Plugin for KonnektorenAssetsPlugin {
14    fn build(&self, app: &mut App) {
15        app
16            // Register asset types and loaders
17            .init_asset::<ChallengeAsset>()
18            .init_asset_loader::<ChallengeAssetLoader>()
19            .init_asset::<LevelAsset>()
20            .init_asset_loader::<LevelAssetLoader>()
21            // Initialize shared asset registry
22            .init_resource::<KonnektorenAssetRegistry>()
23            // Add asset tracking system
24            .add_systems(Update, update_asset_registry);
25    }
26}
27
28/// Central registry for all Konnektoren assets
29/// Maps logical IDs to asset handles for easy access
30#[derive(Resource, Default, Debug)]
31pub struct KonnektorenAssetRegistry {
32    pub challenges: HashMap<String, Handle<ChallengeAsset>>,
33    pub levels: HashMap<String, Handle<LevelAsset>>,
34    pub loaded_challenges: HashMap<String, bool>,
35    pub loaded_levels: HashMap<String, bool>,
36    // Add strong references to keep assets alive
37    pub challenge_holders: HashMap<String, Handle<ChallengeAsset>>,
38    pub level_holders: HashMap<String, Handle<LevelAsset>>,
39}
40
41impl KonnektorenAssetRegistry {
42    /// Register a challenge asset with a logical ID
43    pub fn register_challenge(&mut self, id: String, handle: Handle<ChallengeAsset>) {
44        // Store both in the registry and as a holder to prevent unloading
45        self.challenges.insert(id.clone(), handle.clone());
46        self.challenge_holders.insert(id, handle);
47    }
48
49    /// Register a level asset with a logical ID
50    pub fn register_level(&mut self, id: String, handle: Handle<LevelAsset>) {
51        // Store both in the registry and as a holder to prevent unloading
52        self.levels.insert(id.clone(), handle.clone());
53        self.level_holders.insert(id, handle);
54    }
55
56    /// Get a challenge handle by ID
57    pub fn get_challenge_handle(&self, id: &str) -> Option<&Handle<ChallengeAsset>> {
58        self.challenges.get(id)
59    }
60
61    /// Get a level handle by ID
62    pub fn get_level_handle(&self, id: &str) -> Option<&Handle<LevelAsset>> {
63        self.levels.get(id)
64    }
65
66    /// Check if a challenge is loaded
67    pub fn is_challenge_loaded(&self, id: &str) -> bool {
68        self.loaded_challenges.get(id).copied().unwrap_or(false)
69    }
70
71    /// Check if a level is loaded
72    pub fn is_level_loaded(&self, id: &str) -> bool {
73        self.loaded_levels.get(id).copied().unwrap_or(false)
74    }
75
76    /// Get all loaded challenge IDs
77    pub fn get_loaded_challenges(&self) -> Vec<String> {
78        self.loaded_challenges
79            .iter()
80            .filter_map(|(id, &loaded)| if loaded { Some(id.clone()) } else { None })
81            .collect()
82    }
83
84    /// Get all loaded level IDs
85    pub fn get_loaded_levels(&self) -> Vec<String> {
86        self.loaded_levels
87            .iter()
88            .filter_map(|(id, &loaded)| if loaded { Some(id.clone()) } else { None })
89            .collect()
90    }
91
92    /// Check if all registered assets are loaded
93    pub fn are_all_assets_loaded(&self) -> bool {
94        let all_challenges_loaded = self
95            .challenges
96            .keys()
97            .all(|id| self.is_challenge_loaded(id));
98        let all_levels_loaded = self.levels.keys().all(|id| self.is_level_loaded(id));
99
100        all_challenges_loaded && all_levels_loaded && !self.challenges.is_empty()
101    }
102
103    /// Get loading progress (0.0 to 1.0)
104    pub fn get_loading_progress(&self) -> f32 {
105        let total_assets = self.challenges.len() + self.levels.len();
106        if total_assets == 0 {
107            return 0.0;
108        }
109
110        let loaded_challenges = self
111            .loaded_challenges
112            .values()
113            .filter(|&&loaded| loaded)
114            .count();
115        let loaded_levels = self
116            .loaded_levels
117            .values()
118            .filter(|&&loaded| loaded)
119            .count();
120        let loaded_total = loaded_challenges + loaded_levels;
121
122        loaded_total as f32 / total_assets as f32
123    }
124}
125
126/// System to update the asset registry when assets finish loading
127fn update_asset_registry(
128    mut registry: ResMut<KonnektorenAssetRegistry>,
129    challenge_assets: Res<Assets<ChallengeAsset>>,
130    level_assets: Res<Assets<LevelAsset>>,
131) {
132    // Collect changes first to avoid borrowing conflicts
133    let mut challenge_updates = Vec::new();
134    let mut level_updates = Vec::new();
135
136    // Check challenge loading status
137    for (id, handle) in &registry.challenges {
138        let is_loaded = challenge_assets.get(handle).is_some();
139        let was_loaded = registry.loaded_challenges.get(id).copied().unwrap_or(false);
140
141        if is_loaded && !was_loaded {
142            challenge_updates.push(id.clone());
143        }
144    }
145
146    // Check level loading status
147    for (id, handle) in &registry.levels {
148        let is_loaded = level_assets.get(handle).is_some();
149        let was_loaded = registry.loaded_levels.get(id).copied().unwrap_or(false);
150
151        if is_loaded && !was_loaded {
152            level_updates.push(id.clone());
153        }
154    }
155
156    // Apply updates
157    for id in challenge_updates {
158        registry.loaded_challenges.insert(id.clone(), true);
159        info!("Challenge '{}' finished loading and is held in memory", id);
160    }
161
162    for id in level_updates {
163        registry.loaded_levels.insert(id.clone(), true);
164        info!("Level '{}' finished loading and is held in memory", id);
165    }
166
167    // Log overall progress periodically
168    if !registry.challenges.is_empty() || !registry.levels.is_empty() {
169        let progress = registry.get_loading_progress();
170        if progress > 0.0 && progress < 1.0 {
171            debug!("Asset loading progress: {:.1}%", progress * 100.0);
172        }
173    }
174}
175
176/// Helper trait for easy asset loading
177pub trait KonnektorenAssetLoader {
178    /// Load a challenge asset by ID and path
179    fn load_challenge(&mut self, id: &str, path: &str) -> Handle<ChallengeAsset>;
180
181    /// Load a level asset by ID and path
182    fn load_level(&mut self, id: &str, path: &str) -> Handle<LevelAsset>;
183
184    /// Load common Konnektoren assets
185    fn load_common_assets(&mut self) -> &mut Self;
186}
187
188impl KonnektorenAssetLoader for App {
189    fn load_challenge(&mut self, id: &str, path: &str) -> Handle<ChallengeAsset> {
190        let asset_server = self.world().resource::<AssetServer>();
191        let handle = asset_server.load(path);
192
193        let mut registry = self.world_mut().resource_mut::<KonnektorenAssetRegistry>();
194        registry.register_challenge(id.to_string(), handle.clone());
195
196        info!(
197            "Registered challenge '{}' from '{}' (handle will be held)",
198            id, path
199        );
200        handle
201    }
202
203    fn load_level(&mut self, id: &str, path: &str) -> Handle<LevelAsset> {
204        let asset_server = self.world().resource::<AssetServer>();
205        let handle = asset_server.load(path);
206
207        let mut registry = self.world_mut().resource_mut::<KonnektorenAssetRegistry>();
208        registry.register_level(id.to_string(), handle.clone());
209
210        info!(
211            "Registered level '{}' from '{}' (handle will be held)",
212            id, path
213        );
214        handle
215    }
216
217    fn load_common_assets(&mut self) -> &mut Self {
218        // Load common challenges
219        self.load_challenge("articles", "challenges/articles.yml");
220
221        // Load common levels
222        self.load_level("a1", "a1.level.yml");
223
224        self
225    }
226}
227
228#[cfg(test)]
229mod tests;