#![forbid(unsafe_code)]
#![warn(missing_docs)]
use core::fmt;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub use jugar_ai as ai;
pub use jugar_audio as audio;
pub use jugar_core as game_core;
pub use jugar_input as input;
pub use jugar_physics as physics;
pub use jugar_procgen as procgen;
pub use jugar_render as render;
pub use jugar_ui as ui;
pub mod prelude {
pub use crate::{JugarConfig, JugarEngine, LoopControl};
pub use jugar_core::{
Anchor, Camera, Entity, FrameResult, GameLoop, GameLoopConfig, GameState, Position, Rect,
ScaleMode, Sprite, UiElement, Velocity, World,
};
pub use jugar_input::{
ButtonState, GamepadButton, InputAction, InputState, KeyCode, MouseButton, TouchEvent,
TouchPhase,
};
pub use jugar_render::{
calculate_anchored_position, AspectRatio, RenderCommand, RenderQueue, Viewport,
};
pub use jugar_ui::{Button, ButtonState as UiButtonState, Label, UiContainer, WidgetId};
pub use jugar_physics::{BodyHandle, PhysicsBackend, PhysicsWorld, RigidBody};
pub use jugar_audio::{AudioChannel, AudioHandle, AudioListener, AudioSystem, SoundSource};
pub use jugar_ai::{
Action, BehaviorNode, Goal, NodeStatus, Planner, Selector, Sequence, WorldState,
};
pub use jugar_procgen::{
Direction, Dungeon, DungeonGenerator, DungeonTile, Rng, Room, ValueNoise, Wfc,
};
pub use glam::Vec2;
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum JugarError {
#[error("Initialization failed: {0}")]
InitializationFailed(String),
#[error("Runtime error: {0}")]
RuntimeError(String),
}
pub type Result<T> = core::result::Result<T, JugarError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JugarConfig {
pub width: u32,
pub height: u32,
pub target_fps: u32,
pub fixed_timestep: f32,
pub max_delta: f32,
pub vsync: bool,
pub title: String,
}
impl Default for JugarConfig {
fn default() -> Self {
Self {
width: 1920,
height: 1080,
target_fps: 60,
fixed_timestep: 1.0 / 60.0,
max_delta: 0.25,
vsync: true,
title: "Jugar Game".to_string(),
}
}
}
impl JugarConfig {
#[must_use]
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
..Default::default()
}
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
#[must_use]
pub const fn with_target_fps(mut self, fps: u32) -> Self {
self.target_fps = fps;
self.fixed_timestep = 1.0 / fps as f32;
self
}
#[must_use]
pub fn mobile_portrait() -> Self {
Self {
width: 1080,
height: 1920,
..Default::default()
}
}
#[must_use]
pub fn mobile_landscape() -> Self {
Self {
width: 1920,
height: 1080,
..Default::default()
}
}
#[must_use]
pub fn ultrawide() -> Self {
Self {
width: 3440,
height: 1440,
..Default::default()
}
}
#[must_use]
pub fn super_ultrawide() -> Self {
Self {
width: 5120,
height: 1440,
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopControl {
Continue,
Exit,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Time {
pub elapsed: f32,
pub delta: f32,
pub fixed_delta: f32,
pub frame: u64,
}
pub struct JugarEngine {
config: JugarConfig,
time: Time,
viewport: render::Viewport,
input: input::InputState,
audio: audio::AudioSystem,
world: jugar_core::World,
physics: physics::PhysicsWorld,
ui: ui::UiContainer,
game_loop: jugar_core::GameLoop,
running: bool,
}
impl JugarEngine {
#[must_use]
pub fn new(config: JugarConfig) -> Self {
let viewport = render::Viewport::new(config.width, config.height);
let ui_width = viewport.width as f32;
let ui_height = viewport.height as f32;
let loop_config = jugar_core::GameLoopConfig {
fixed_dt: config.fixed_timestep,
max_frame_time: config.max_delta,
target_fps: config.target_fps,
};
let game_loop = jugar_core::GameLoop::new(loop_config);
Self {
config,
time: Time::default(),
viewport,
input: input::InputState::new(),
audio: audio::AudioSystem::new(),
world: jugar_core::World::new(),
physics: physics::PhysicsWorld::new(),
ui: ui::UiContainer::new(ui_width, ui_height),
game_loop,
running: false,
}
}
#[must_use]
pub const fn config(&self) -> &JugarConfig {
&self.config
}
#[must_use]
pub const fn time(&self) -> &Time {
&self.time
}
#[must_use]
pub const fn viewport(&self) -> &render::Viewport {
&self.viewport
}
#[allow(clippy::missing_const_for_fn)]
pub fn viewport_mut(&mut self) -> &mut render::Viewport {
&mut self.viewport
}
#[must_use]
pub const fn input(&self) -> &input::InputState {
&self.input
}
#[allow(clippy::missing_const_for_fn)]
pub fn input_mut(&mut self) -> &mut input::InputState {
&mut self.input
}
#[must_use]
pub const fn audio(&self) -> &audio::AudioSystem {
&self.audio
}
#[allow(clippy::missing_const_for_fn)]
pub fn audio_mut(&mut self) -> &mut audio::AudioSystem {
&mut self.audio
}
#[must_use]
pub const fn world(&self) -> &jugar_core::World {
&self.world
}
#[allow(clippy::missing_const_for_fn)]
pub fn world_mut(&mut self) -> &mut jugar_core::World {
&mut self.world
}
#[must_use]
pub const fn physics(&self) -> &physics::PhysicsWorld {
&self.physics
}
#[allow(clippy::missing_const_for_fn)]
pub fn physics_mut(&mut self) -> &mut physics::PhysicsWorld {
&mut self.physics
}
#[must_use]
pub const fn ui(&self) -> &ui::UiContainer {
&self.ui
}
#[allow(clippy::missing_const_for_fn)]
pub fn ui_mut(&mut self) -> &mut ui::UiContainer {
&mut self.ui
}
#[must_use]
pub const fn game_loop(&self) -> &jugar_core::GameLoop {
&self.game_loop
}
pub fn resize(&mut self, width: u32, height: u32) {
self.viewport.resize(width, height);
self.ui.set_viewport_size(width as f32, height as f32);
}
#[must_use]
pub const fn is_running(&self) -> bool {
self.running
}
pub fn run<F>(&mut self, mut callback: F)
where
F: FnMut(&mut Self) -> LoopControl,
{
self.running = true;
let start_time = std::time::Instant::now();
while self.running {
let elapsed = start_time.elapsed().as_secs_f32();
let frame_result = self.game_loop.update(elapsed);
self.time.delta = elapsed - self.time.elapsed;
self.time.elapsed = elapsed;
self.time.fixed_delta = self.config.fixed_timestep;
self.time.frame += 1;
for _ in 0..frame_result.physics_ticks {
let _ = self.physics.step(self.config.fixed_timestep);
}
self.audio.update(self.time.delta);
if callback(self) == LoopControl::Exit {
self.running = false;
}
self.input.advance_frame();
}
}
pub fn step(&mut self, delta: f32) {
self.time.delta = delta.min(self.config.max_delta);
self.time.elapsed += self.time.delta;
self.time.frame += 1;
let frame_result = self.game_loop.update(self.time.elapsed);
for _ in 0..frame_result.physics_ticks {
let _ = self.physics.step(self.config.fixed_timestep);
}
self.audio.update(self.time.delta);
self.input.advance_frame();
}
pub const fn stop(&mut self) {
self.running = false;
}
}
impl Default for JugarEngine {
fn default() -> Self {
Self::new(JugarConfig::default())
}
}
impl fmt::Debug for JugarEngine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JugarEngine")
.field("config", &self.config)
.field("time", &self.time)
.field("running", &self.running)
.field("physics_backend", &self.physics.backend())
.finish_non_exhaustive()
}
}
#[must_use]
pub fn create_game() -> JugarEngine {
JugarEngine::default()
}
#[must_use]
pub fn create_mobile_game() -> JugarEngine {
JugarEngine::new(JugarConfig::mobile_landscape())
}
#[must_use]
pub fn create_ultrawide_game() -> JugarEngine {
JugarEngine::new(JugarConfig::super_ultrawide())
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::field_reassign_with_default
)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = JugarConfig::default();
assert_eq!(config.width, 1920);
assert_eq!(config.height, 1080);
assert_eq!(config.target_fps, 60);
}
#[test]
fn test_config_mobile_portrait() {
let config = JugarConfig::mobile_portrait();
assert_eq!(config.width, 1080);
assert_eq!(config.height, 1920);
}
#[test]
fn test_config_super_ultrawide() {
let config = JugarConfig::super_ultrawide();
assert_eq!(config.width, 5120);
assert_eq!(config.height, 1440);
}
#[test]
fn test_config_builder() {
let config = JugarConfig::new(800, 600)
.with_title("Test Game")
.with_target_fps(30);
assert_eq!(config.width, 800);
assert_eq!(config.height, 600);
assert_eq!(config.title, "Test Game");
assert_eq!(config.target_fps, 30);
}
#[test]
fn test_engine_creation() {
let engine = JugarEngine::new(JugarConfig::default());
assert!(!engine.is_running());
assert_eq!(engine.time().frame, 0);
}
#[test]
fn test_engine_default() {
let engine = JugarEngine::default();
assert_eq!(engine.config().width, 1920);
}
#[test]
fn test_engine_resize() {
let mut engine = JugarEngine::default();
engine.resize(1280, 720);
assert_eq!(engine.viewport().width, 1280);
assert_eq!(engine.viewport().height, 720);
}
#[test]
fn test_engine_step() {
let mut engine = JugarEngine::default();
engine.step(1.0 / 60.0);
assert_eq!(engine.time().frame, 1);
assert!(engine.time().elapsed > 0.0);
}
#[test]
fn test_engine_step_multiple() {
let mut engine = JugarEngine::default();
for _ in 0..10 {
engine.step(1.0 / 60.0);
}
assert_eq!(engine.time().frame, 10);
}
#[test]
fn test_engine_run_exit() {
let mut engine = JugarEngine::default();
let mut count = 0;
engine.run(|_| {
count += 1;
if count >= 5 {
LoopControl::Exit
} else {
LoopControl::Continue
}
});
assert_eq!(count, 5);
assert!(!engine.is_running());
}
#[test]
fn test_engine_stop() {
let mut engine = JugarEngine::default();
engine.running = true;
engine.stop();
assert!(!engine.is_running());
}
#[test]
fn test_create_game() {
let engine = create_game();
assert_eq!(engine.config().width, 1920);
}
#[test]
fn test_create_mobile_game() {
let engine = create_mobile_game();
assert_eq!(engine.config().width, 1920);
assert_eq!(engine.config().height, 1080);
}
#[test]
fn test_create_ultrawide_game() {
let engine = create_ultrawide_game();
assert_eq!(engine.config().width, 5120);
}
#[test]
fn test_engine_accessors() {
let mut engine = JugarEngine::default();
let _ = engine.config();
let _ = engine.time();
let _ = engine.viewport();
let _ = engine.viewport_mut();
let _ = engine.input();
let _ = engine.input_mut();
let _ = engine.audio();
let _ = engine.audio_mut();
let _ = engine.world();
let _ = engine.world_mut();
let _ = engine.physics();
let _ = engine.physics_mut();
let _ = engine.ui();
let _ = engine.ui_mut();
let _ = engine.game_loop();
}
#[test]
fn test_loop_control() {
assert_eq!(LoopControl::Continue, LoopControl::Continue);
assert_ne!(LoopControl::Continue, LoopControl::Exit);
}
#[test]
fn test_time_default() {
let time = Time::default();
assert!(time.elapsed.abs() < f32::EPSILON);
assert!(time.delta.abs() < f32::EPSILON);
assert_eq!(time.frame, 0);
}
}