1#![forbid(unsafe_code)]
29#![warn(missing_docs)]
30
31use core::fmt;
32
33use serde::{Deserialize, Serialize};
34use thiserror::Error;
35
36pub 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
46pub mod prelude {
48 pub use crate::{JugarConfig, JugarEngine, LoopControl};
49
50 pub use jugar_core::{
52 Anchor, Camera, Entity, FrameResult, GameLoop, GameLoopConfig, GameState, Position, Rect,
53 ScaleMode, Sprite, UiElement, Velocity, World,
54 };
55
56 pub use jugar_input::{
58 ButtonState, GamepadButton, InputAction, InputState, KeyCode, MouseButton, TouchEvent,
59 TouchPhase,
60 };
61
62 pub use jugar_render::{
64 calculate_anchored_position, AspectRatio, RenderCommand, RenderQueue, Viewport,
65 };
66
67 pub use jugar_ui::{Button, ButtonState as UiButtonState, Label, UiContainer, WidgetId};
69
70 pub use jugar_physics::{BodyHandle, PhysicsBackend, PhysicsWorld, RigidBody};
72
73 pub use jugar_audio::{AudioChannel, AudioHandle, AudioListener, AudioSystem, SoundSource};
75
76 pub use jugar_ai::{
78 Action, BehaviorNode, Goal, NodeStatus, Planner, Selector, Sequence, WorldState,
79 };
80
81 pub use jugar_procgen::{
83 Direction, Dungeon, DungeonGenerator, DungeonTile, Rng, Room, ValueNoise, Wfc,
84 };
85
86 pub use glam::Vec2;
88}
89
90#[derive(Error, Debug, Clone, PartialEq, Eq)]
92pub enum JugarError {
93 #[error("Initialization failed: {0}")]
95 InitializationFailed(String),
96 #[error("Runtime error: {0}")]
98 RuntimeError(String),
99}
100
101pub type Result<T> = core::result::Result<T, JugarError>;
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct JugarConfig {
107 pub width: u32,
109 pub height: u32,
111 pub target_fps: u32,
113 pub fixed_timestep: f32,
115 pub max_delta: f32,
117 pub vsync: bool,
119 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 #[must_use]
140 pub fn new(width: u32, height: u32) -> Self {
141 Self {
142 width,
143 height,
144 ..Default::default()
145 }
146 }
147
148 #[must_use]
150 pub fn with_title(mut self, title: impl Into<String>) -> Self {
151 self.title = title.into();
152 self
153 }
154
155 #[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 #[must_use]
165 pub fn mobile_portrait() -> Self {
166 Self {
167 width: 1080,
168 height: 1920,
169 ..Default::default()
170 }
171 }
172
173 #[must_use]
175 pub fn mobile_landscape() -> Self {
176 Self {
177 width: 1920,
178 height: 1080,
179 ..Default::default()
180 }
181 }
182
183 #[must_use]
185 pub fn ultrawide() -> Self {
186 Self {
187 width: 3440,
188 height: 1440,
189 ..Default::default()
190 }
191 }
192
193 #[must_use]
195 pub fn super_ultrawide() -> Self {
196 Self {
197 width: 5120,
198 height: 1440,
199 ..Default::default()
200 }
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum LoopControl {
207 Continue,
209 Exit,
211}
212
213#[derive(Debug, Clone, Copy, Default)]
215pub struct Time {
216 pub elapsed: f32,
218 pub delta: f32,
220 pub fixed_delta: f32,
222 pub frame: u64,
224}
225
226pub 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 #[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 #[must_use]
270 pub const fn config(&self) -> &JugarConfig {
271 &self.config
272 }
273
274 #[must_use]
276 pub const fn time(&self) -> &Time {
277 &self.time
278 }
279
280 #[must_use]
282 pub const fn viewport(&self) -> &render::Viewport {
283 &self.viewport
284 }
285
286 pub fn viewport_mut(&mut self) -> &mut render::Viewport {
288 &mut self.viewport
289 }
290
291 #[must_use]
293 pub const fn input(&self) -> &input::InputState {
294 &self.input
295 }
296
297 pub fn input_mut(&mut self) -> &mut input::InputState {
299 &mut self.input
300 }
301
302 #[must_use]
304 pub const fn audio(&self) -> &audio::AudioSystem {
305 &self.audio
306 }
307
308 pub fn audio_mut(&mut self) -> &mut audio::AudioSystem {
310 &mut self.audio
311 }
312
313 #[must_use]
315 pub const fn world(&self) -> &jugar_core::World {
316 &self.world
317 }
318
319 pub fn world_mut(&mut self) -> &mut jugar_core::World {
321 &mut self.world
322 }
323
324 #[must_use]
326 pub const fn physics(&self) -> &physics::PhysicsWorld {
327 &self.physics
328 }
329
330 pub fn physics_mut(&mut self) -> &mut physics::PhysicsWorld {
332 &mut self.physics
333 }
334
335 #[must_use]
337 pub const fn ui(&self) -> &ui::UiContainer {
338 &self.ui
339 }
340
341 pub fn ui_mut(&mut self) -> &mut ui::UiContainer {
343 &mut self.ui
344 }
345
346 #[must_use]
348 pub const fn game_loop(&self) -> &jugar_core::GameLoop {
349 &self.game_loop
350 }
351
352 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 #[must_use]
360 pub const fn is_running(&self) -> bool {
361 self.running
362 }
363
364 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 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 for _ in 0..frame_result.physics_ticks {
387 let _ = self.physics.step(self.config.fixed_timestep);
388 }
389
390 self.audio.update(self.time.delta);
392
393 if callback(self) == LoopControl::Exit {
395 self.running = false;
396 }
397
398 self.input.advance_frame();
400 }
401 }
402
403 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 let frame_result = self.game_loop.update(self.time.elapsed);
411
412 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 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#[must_use]
446pub fn create_game() -> JugarEngine {
447 JugarEngine::default()
448}
449
450#[must_use]
452pub fn create_mobile_game() -> JugarEngine {
453 JugarEngine::new(JugarConfig::mobile_landscape())
454}
455
456#[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 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}