jugar/
lib.rs

1//! # Jugar
2//!
3//! WASM-native game engine for mobile to ultrawide desktop experiences.
4//!
5//! Jugar provides a complete game development framework targeting `wasm32-unknown-unknown`
6//! with pure WASM binary output (zero JavaScript).
7//!
8//! ## Features
9//!
10//! - **ECS Architecture**: High-performance Entity-Component-System
11//! - **Responsive Design**: Scales from mobile to 32:9 ultrawide
12//! - **Physics**: Tiered physics with WebGPU → WASM-SIMD → Scalar fallback
13//! - **AI**: GOAP planner and Behavior Trees
14//! - **Audio**: Spatial 2D audio system
15//! - **Procedural Generation**: Noise, dungeons, and WFC
16//!
17//! ## Example
18//!
19//! ```rust,ignore
20//! use jugar::prelude::*;
21//!
22//! fn main() {
23//!     let mut engine = JugarEngine::new(JugarConfig::default());
24//!     engine.run(|_| LoopControl::Exit);
25//! }
26//! ```
27
28#![forbid(unsafe_code)]
29#![warn(missing_docs)]
30
31use core::fmt;
32
33use serde::{Deserialize, Serialize};
34use thiserror::Error;
35
36// Re-export all crates
37pub use jugar_ai as ai;
38pub use jugar_audio as audio;
39pub use jugar_core as game_core;
40pub use jugar_input as input;
41pub use jugar_physics as physics;
42pub use jugar_procgen as procgen;
43pub use jugar_render as render;
44pub use jugar_ui as ui;
45
46/// Prelude for common imports
47pub mod prelude {
48    pub use crate::{JugarConfig, JugarEngine, LoopControl};
49
50    // Core types
51    pub use jugar_core::{
52        Anchor, Camera, Entity, FrameResult, GameLoop, GameLoopConfig, GameState, Position, Rect,
53        ScaleMode, Sprite, UiElement, Velocity, World,
54    };
55
56    // Input
57    pub use jugar_input::{
58        ButtonState, GamepadButton, InputAction, InputState, KeyCode, MouseButton, TouchEvent,
59        TouchPhase,
60    };
61
62    // Render
63    pub use jugar_render::{
64        calculate_anchored_position, AspectRatio, RenderCommand, RenderQueue, Viewport,
65    };
66
67    // UI
68    pub use jugar_ui::{Button, ButtonState as UiButtonState, Label, UiContainer, WidgetId};
69
70    // Physics
71    pub use jugar_physics::{BodyHandle, PhysicsBackend, PhysicsWorld, RigidBody};
72
73    // Audio
74    pub use jugar_audio::{AudioChannel, AudioHandle, AudioListener, AudioSystem, SoundSource};
75
76    // AI
77    pub use jugar_ai::{
78        Action, BehaviorNode, Goal, NodeStatus, Planner, Selector, Sequence, WorldState,
79    };
80
81    // Procgen
82    pub use jugar_procgen::{
83        Direction, Dungeon, DungeonGenerator, DungeonTile, Rng, Room, ValueNoise, Wfc,
84    };
85
86    // External
87    pub use glam::Vec2;
88}
89
90/// Jugar engine errors
91#[derive(Error, Debug, Clone, PartialEq, Eq)]
92pub enum JugarError {
93    /// Initialization error
94    #[error("Initialization failed: {0}")]
95    InitializationFailed(String),
96    /// Runtime error
97    #[error("Runtime error: {0}")]
98    RuntimeError(String),
99}
100
101/// Result type for Jugar operations
102pub type Result<T> = core::result::Result<T, JugarError>;
103
104/// Engine configuration
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct JugarConfig {
107    /// Window/canvas width
108    pub width: u32,
109    /// Window/canvas height
110    pub height: u32,
111    /// Target frames per second
112    pub target_fps: u32,
113    /// Fixed timestep for physics (in seconds)
114    pub fixed_timestep: f32,
115    /// Maximum delta time (to prevent spiral of death)
116    pub max_delta: f32,
117    /// Enable vsync
118    pub vsync: bool,
119    /// Application title
120    pub title: String,
121}
122
123impl Default for JugarConfig {
124    fn default() -> Self {
125        Self {
126            width: 1920,
127            height: 1080,
128            target_fps: 60,
129            fixed_timestep: 1.0 / 60.0,
130            max_delta: 0.25,
131            vsync: true,
132            title: "Jugar Game".to_string(),
133        }
134    }
135}
136
137impl JugarConfig {
138    /// Creates a new configuration
139    #[must_use]
140    pub fn new(width: u32, height: u32) -> Self {
141        Self {
142            width,
143            height,
144            ..Default::default()
145        }
146    }
147
148    /// Sets the title
149    #[must_use]
150    pub fn with_title(mut self, title: impl Into<String>) -> Self {
151        self.title = title.into();
152        self
153    }
154
155    /// Sets the target FPS
156    #[must_use]
157    pub const fn with_target_fps(mut self, fps: u32) -> Self {
158        self.target_fps = fps;
159        self.fixed_timestep = 1.0 / fps as f32;
160        self
161    }
162
163    /// Mobile portrait preset
164    #[must_use]
165    pub fn mobile_portrait() -> Self {
166        Self {
167            width: 1080,
168            height: 1920,
169            ..Default::default()
170        }
171    }
172
173    /// Mobile landscape preset
174    #[must_use]
175    pub fn mobile_landscape() -> Self {
176        Self {
177            width: 1920,
178            height: 1080,
179            ..Default::default()
180        }
181    }
182
183    /// Ultrawide 21:9 preset
184    #[must_use]
185    pub fn ultrawide() -> Self {
186        Self {
187            width: 3440,
188            height: 1440,
189            ..Default::default()
190        }
191    }
192
193    /// Super ultrawide 32:9 preset
194    #[must_use]
195    pub fn super_ultrawide() -> Self {
196        Self {
197            width: 5120,
198            height: 1440,
199            ..Default::default()
200        }
201    }
202}
203
204/// Loop control returned from update callback
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum LoopControl {
207    /// Continue running
208    Continue,
209    /// Exit the game loop
210    Exit,
211}
212
213/// Engine time information
214#[derive(Debug, Clone, Copy, Default)]
215pub struct Time {
216    /// Total elapsed time since start (seconds)
217    pub elapsed: f32,
218    /// Delta time for this frame (seconds)
219    pub delta: f32,
220    /// Fixed timestep for physics
221    pub fixed_delta: f32,
222    /// Current frame number
223    pub frame: u64,
224}
225
226/// The main Jugar game engine
227pub struct JugarEngine {
228    config: JugarConfig,
229    time: Time,
230    viewport: render::Viewport,
231    input: input::InputState,
232    audio: audio::AudioSystem,
233    world: jugar_core::World,
234    physics: physics::PhysicsWorld,
235    ui: ui::UiContainer,
236    game_loop: jugar_core::GameLoop,
237    running: bool,
238}
239
240impl JugarEngine {
241    /// Creates a new Jugar engine with the given configuration
242    #[must_use]
243    pub fn new(config: JugarConfig) -> Self {
244        let viewport = render::Viewport::new(config.width, config.height);
245        let ui_width = viewport.width as f32;
246        let ui_height = viewport.height as f32;
247        let loop_config = jugar_core::GameLoopConfig {
248            fixed_dt: config.fixed_timestep,
249            max_frame_time: config.max_delta,
250            target_fps: config.target_fps,
251        };
252        let game_loop = jugar_core::GameLoop::new(loop_config);
253
254        Self {
255            config,
256            time: Time::default(),
257            viewport,
258            input: input::InputState::new(),
259            audio: audio::AudioSystem::new(),
260            world: jugar_core::World::new(),
261            physics: physics::PhysicsWorld::new(),
262            ui: ui::UiContainer::new(ui_width, ui_height),
263            game_loop,
264            running: false,
265        }
266    }
267
268    /// Gets the configuration
269    #[must_use]
270    pub const fn config(&self) -> &JugarConfig {
271        &self.config
272    }
273
274    /// Gets the current time
275    #[must_use]
276    pub const fn time(&self) -> &Time {
277        &self.time
278    }
279
280    /// Gets the viewport
281    #[must_use]
282    pub const fn viewport(&self) -> &render::Viewport {
283        &self.viewport
284    }
285
286    /// Gets the viewport mutably
287    pub fn viewport_mut(&mut self) -> &mut render::Viewport {
288        &mut self.viewport
289    }
290
291    /// Gets the input state
292    #[must_use]
293    pub const fn input(&self) -> &input::InputState {
294        &self.input
295    }
296
297    /// Gets the input state mutably
298    pub fn input_mut(&mut self) -> &mut input::InputState {
299        &mut self.input
300    }
301
302    /// Gets the audio system
303    #[must_use]
304    pub const fn audio(&self) -> &audio::AudioSystem {
305        &self.audio
306    }
307
308    /// Gets the audio system mutably
309    pub fn audio_mut(&mut self) -> &mut audio::AudioSystem {
310        &mut self.audio
311    }
312
313    /// Gets the ECS world
314    #[must_use]
315    pub const fn world(&self) -> &jugar_core::World {
316        &self.world
317    }
318
319    /// Gets the ECS world mutably
320    pub fn world_mut(&mut self) -> &mut jugar_core::World {
321        &mut self.world
322    }
323
324    /// Gets the physics world
325    #[must_use]
326    pub const fn physics(&self) -> &physics::PhysicsWorld {
327        &self.physics
328    }
329
330    /// Gets the physics world mutably
331    pub fn physics_mut(&mut self) -> &mut physics::PhysicsWorld {
332        &mut self.physics
333    }
334
335    /// Gets the UI container
336    #[must_use]
337    pub const fn ui(&self) -> &ui::UiContainer {
338        &self.ui
339    }
340
341    /// Gets the UI container mutably
342    pub fn ui_mut(&mut self) -> &mut ui::UiContainer {
343        &mut self.ui
344    }
345
346    /// Gets the game loop
347    #[must_use]
348    pub const fn game_loop(&self) -> &jugar_core::GameLoop {
349        &self.game_loop
350    }
351
352    /// Resizes the viewport
353    pub fn resize(&mut self, width: u32, height: u32) {
354        self.viewport.resize(width, height);
355        self.ui.set_viewport_size(width as f32, height as f32);
356    }
357
358    /// Checks if the engine is running
359    #[must_use]
360    pub const fn is_running(&self) -> bool {
361        self.running
362    }
363
364    /// Runs the game loop with a callback
365    ///
366    /// The callback receives a reference to the engine and returns `LoopControl`.
367    pub fn run<F>(&mut self, mut callback: F)
368    where
369        F: FnMut(&mut Self) -> LoopControl,
370    {
371        self.running = true;
372        let start_time = std::time::Instant::now();
373
374        while self.running {
375            let elapsed = start_time.elapsed().as_secs_f32();
376
377            // Update game loop and get physics ticks
378            let frame_result = self.game_loop.update(elapsed);
379
380            self.time.delta = elapsed - self.time.elapsed;
381            self.time.elapsed = elapsed;
382            self.time.fixed_delta = self.config.fixed_timestep;
383            self.time.frame += 1;
384
385            // Run physics for each tick
386            for _ in 0..frame_result.physics_ticks {
387                let _ = self.physics.step(self.config.fixed_timestep);
388            }
389
390            // Update audio
391            self.audio.update(self.time.delta);
392
393            // Call user callback
394            if callback(self) == LoopControl::Exit {
395                self.running = false;
396            }
397
398            // Advance input state
399            self.input.advance_frame();
400        }
401    }
402
403    /// Steps the engine for a single frame (useful for testing)
404    pub fn step(&mut self, delta: f32) {
405        self.time.delta = delta.min(self.config.max_delta);
406        self.time.elapsed += self.time.delta;
407        self.time.frame += 1;
408
409        // Update game loop and get physics ticks
410        let frame_result = self.game_loop.update(self.time.elapsed);
411
412        // Run physics for each tick
413        for _ in 0..frame_result.physics_ticks {
414            let _ = self.physics.step(self.config.fixed_timestep);
415        }
416
417        self.audio.update(self.time.delta);
418        self.input.advance_frame();
419    }
420
421    /// Stops the engine
422    pub fn stop(&mut self) {
423        self.running = false;
424    }
425}
426
427impl Default for JugarEngine {
428    fn default() -> Self {
429        Self::new(JugarConfig::default())
430    }
431}
432
433impl fmt::Debug for JugarEngine {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        f.debug_struct("JugarEngine")
436            .field("config", &self.config)
437            .field("time", &self.time)
438            .field("running", &self.running)
439            .field("physics_backend", &self.physics.backend())
440            .finish_non_exhaustive()
441    }
442}
443
444/// Creates a simple game with default configuration
445#[must_use]
446pub fn create_game() -> JugarEngine {
447    JugarEngine::default()
448}
449
450/// Creates a game with mobile configuration
451#[must_use]
452pub fn create_mobile_game() -> JugarEngine {
453    JugarEngine::new(JugarConfig::mobile_landscape())
454}
455
456/// Creates a game with ultrawide configuration
457#[must_use]
458pub fn create_ultrawide_game() -> JugarEngine {
459    JugarEngine::new(JugarConfig::super_ultrawide())
460}
461
462#[cfg(test)]
463#[allow(
464    clippy::unwrap_used,
465    clippy::expect_used,
466    clippy::field_reassign_with_default
467)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn test_config_default() {
473        let config = JugarConfig::default();
474        assert_eq!(config.width, 1920);
475        assert_eq!(config.height, 1080);
476        assert_eq!(config.target_fps, 60);
477    }
478
479    #[test]
480    fn test_config_mobile_portrait() {
481        let config = JugarConfig::mobile_portrait();
482        assert_eq!(config.width, 1080);
483        assert_eq!(config.height, 1920);
484    }
485
486    #[test]
487    fn test_config_super_ultrawide() {
488        let config = JugarConfig::super_ultrawide();
489        assert_eq!(config.width, 5120);
490        assert_eq!(config.height, 1440);
491    }
492
493    #[test]
494    fn test_config_builder() {
495        let config = JugarConfig::new(800, 600)
496            .with_title("Test Game")
497            .with_target_fps(30);
498
499        assert_eq!(config.width, 800);
500        assert_eq!(config.height, 600);
501        assert_eq!(config.title, "Test Game");
502        assert_eq!(config.target_fps, 30);
503    }
504
505    #[test]
506    fn test_engine_creation() {
507        let engine = JugarEngine::new(JugarConfig::default());
508        assert!(!engine.is_running());
509        assert_eq!(engine.time().frame, 0);
510    }
511
512    #[test]
513    fn test_engine_default() {
514        let engine = JugarEngine::default();
515        assert_eq!(engine.config().width, 1920);
516    }
517
518    #[test]
519    fn test_engine_resize() {
520        let mut engine = JugarEngine::default();
521        engine.resize(1280, 720);
522
523        assert_eq!(engine.viewport().width, 1280);
524        assert_eq!(engine.viewport().height, 720);
525    }
526
527    #[test]
528    fn test_engine_step() {
529        let mut engine = JugarEngine::default();
530        engine.step(1.0 / 60.0);
531
532        assert_eq!(engine.time().frame, 1);
533        assert!(engine.time().elapsed > 0.0);
534    }
535
536    #[test]
537    fn test_engine_step_multiple() {
538        let mut engine = JugarEngine::default();
539
540        for _ in 0..10 {
541            engine.step(1.0 / 60.0);
542        }
543
544        assert_eq!(engine.time().frame, 10);
545    }
546
547    #[test]
548    fn test_engine_run_exit() {
549        let mut engine = JugarEngine::default();
550        let mut count = 0;
551
552        engine.run(|_| {
553            count += 1;
554            if count >= 5 {
555                LoopControl::Exit
556            } else {
557                LoopControl::Continue
558            }
559        });
560
561        assert_eq!(count, 5);
562        assert!(!engine.is_running());
563    }
564
565    #[test]
566    fn test_engine_stop() {
567        let mut engine = JugarEngine::default();
568        engine.running = true;
569        engine.stop();
570        assert!(!engine.is_running());
571    }
572
573    #[test]
574    fn test_create_game() {
575        let engine = create_game();
576        assert_eq!(engine.config().width, 1920);
577    }
578
579    #[test]
580    fn test_create_mobile_game() {
581        let engine = create_mobile_game();
582        assert_eq!(engine.config().width, 1920);
583        assert_eq!(engine.config().height, 1080);
584    }
585
586    #[test]
587    fn test_create_ultrawide_game() {
588        let engine = create_ultrawide_game();
589        assert_eq!(engine.config().width, 5120);
590    }
591
592    #[test]
593    fn test_engine_accessors() {
594        let mut engine = JugarEngine::default();
595
596        // Test all accessors compile and work
597        let _ = engine.config();
598        let _ = engine.time();
599        let _ = engine.viewport();
600        let _ = engine.viewport_mut();
601        let _ = engine.input();
602        let _ = engine.input_mut();
603        let _ = engine.audio();
604        let _ = engine.audio_mut();
605        let _ = engine.world();
606        let _ = engine.world_mut();
607        let _ = engine.physics();
608        let _ = engine.physics_mut();
609        let _ = engine.ui();
610        let _ = engine.ui_mut();
611        let _ = engine.game_loop();
612    }
613
614    #[test]
615    fn test_loop_control() {
616        assert_eq!(LoopControl::Continue, LoopControl::Continue);
617        assert_ne!(LoopControl::Continue, LoopControl::Exit);
618    }
619
620    #[test]
621    fn test_time_default() {
622        let time = Time::default();
623        assert!(time.elapsed.abs() < f32::EPSILON);
624        assert!(time.delta.abs() < f32::EPSILON);
625        assert_eq!(time.frame, 0);
626    }
627}