tirbofish/dropbear · diff
ok so i have gotten typescript support pretty nicely done :)
Signature present but could not be verified.
Unverified
@@ -40,8 +40,8 @@ open = "5" parking_lot = {version = "0.12", features = ["deadlock_detection"] } pollster = "0.4" rfd = "0.15.4" -ron = "0.10.1" -russimp-ng = { version = "3.2.4", features = ["static-link"] } +ron = "0.11" +russimp-ng = { version = "3.2" } rustyscript = { version = "0.12" } serde = { version = "1.0.219", features = ["derive"] } spin_sleep = "1.3" @@ -49,7 +49,7 @@ tokio = { version = "1", features = ["full"] } transform-gizmo-egui = { git = "https://github.com/4tkbytes/transform-gizmo"} wgpu = "26" winit = { version = "0.30", features = [] } -zip = "4.3" +zip = "4.6" walkdir = "2.3" [workspace.dependencies.image] @@ -19,10 +19,7 @@ With Unix systems (macOS not tested), you will have to download a couple depende ```bash # ubuntu, adapt to your own OS -sudo apt install libudev-dev pkg-config libssl-dev clang cmake meson - -# if on arm devices where russimp cannot compile -sudo apt install assimp-utils +sudo apt install libudev-dev pkg-config libssl-dev clang cmake meson assimp-utils ``` After downloading the requirements, you are free to build it using cargo. @@ -48,9 +45,13 @@ If you do not want to build it locally, you are able to download the latest acti ~~Depsite it looking like a dependency for `eucalyptus`, it can serve as a framework too. Looking through the `docs.rs` will you find related documentation onhow to use it and for rendering your own projects.~~ dropbear cannot be used as a framework (yet), but is best compatible with the eucalyptus editor when making games. For -scripting, eucalyptus uses `rhai`, a new language that works with rust. +scripting, eucalyptus uses `typescript`. The main philosophy of scripting is to be able to have **type safety**, which can aid developers with their code, which is the reason as to why I didn't use `javascript` for scripting instead. + +The typescript used in the scripting is ran with the deno runtime. + +TODO: Create better documentation -The rhai reference for the eucalyptus editor is under the /docs folder of this repository, so take a look there. +The typescript reference for the eucalyptus editor is under the /docs folder of this repository, so take a look there. [Here is the entrance](https://github.com/4tkbytes/dropbear/blob/main/docs/README.md) ## Compability @@ -62,10 +63,7 @@ The rhai reference for the eucalyptus editor is under the /docs folder of this r <sup>1</sup> Will never be implemented; not intended for that platform. -<sup>2</sup> Made some progress on implementing. - - -To be fair, I do not plan on supporting web, android or iOS yet (as it isnt even completed with the basic idea). Maybe I will...? +<sup>2</sup> Made some progress on implementing, but currently a WIP. ## Contributions @@ -75,5 +73,5 @@ Yeah yeah, go ahead and contribute. Make sure it works, and its not spam, and an In the case someone actually makes something with my engine and distributes it, it (meaning **dropbear-engine**, **eucalyptus** and **redback-runtime**) must abide by the license in [LICENSE.md](LICENSE.md). -The gleek package is licensed under the [MIT License](https://mit-license.org/), which allows for anyone to use my -library without _much_ restrictions. +<!-- The gleek package is licensed under the [MIT License](https://mit-license.org/), which allows for anyone to use my +library without _much_ restrictions. --> @@ -23,7 +23,7 @@ pub fn set_hook() { let full_text = format!( "The application has encountered a fatal error and must close.\n\n\ - Location: {}\nError: {}\n\nPlease report this error to the developers.", + Location: {}\nError: {}\n\nPlease report this error to the developers.", location, msg ); @@ -41,5 +41,7 @@ pub fn set_hook() { .set_level(MessageLevel::Error) .show(); } + + std::process::abort(); })); } @@ -55,7 +55,12 @@ fn main() -> anyhow::Result<()> { )); } - println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmt.lib"); + #[cfg(target_os = "windows")] + { + println!("cargo:rustc-link-arg=/FORCE:MULTIPLE"); + println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmt.lib"); + } + println!("cargo:rerun-if-changed=build.rs"); Ok(()) } @@ -1,257 +1,331 @@ // Modules for the dropbear-engine scripting component // Made by 4tkbytes -// EDIT THIS IF YOU WISH, MOST LIKELY +// EDIT THIS IF YOU WISH, RECOMMENDED TO NOT TOUCH IT -type TransformT = any; -type Vec3T = any; -type QuatT = any; type InputStateT = any; -/** - * A placeholder function that returns the value of the host function - */ -function hostFn(name: string) { - const fn = (globalThis as any)[name]; - if (typeof fn !== "function") { - throw new Error(`Host function ${name}() is not available. Make sure the runtime exposes it.`); +export class Transform { + position: Vector3; + rotation: Quaternion; + scale: Vector3; + + public constructor(position?: Vector3, rotation?: Quaternion, scale?: Vector3) { + this.position = position || Vector3.zero(); + this.rotation = rotation || Quaternion.identity(); + this.scale = scale || Vector3.one(); } - return fn; -} -const Transform = { - create: (): TransformT => hostFn("createTransform")(), - translate: (transform: TransformT, translation: [number, number, number] | number[]): TransformT => - hostFn("transformTranslate")(transform, translation), - rotateX: (transform: TransformT, angle: number): TransformT => - hostFn("transformRotateX")(transform, angle), - rotateY: (transform: TransformT, angle: number): TransformT => - hostFn("transformRotateY")(transform, angle), - rotateZ: (transform: TransformT, angle: number): TransformT => - hostFn("transformRotateZ")(transform, angle), - scale: (transform: TransformT, scale: number | [number, number, number]): TransformT => - hostFn("transformScale")(transform, scale), - matrix: (transform: TransformT): TransformT => hostFn("transformMatrix")(transform), -}; + public translate(movement: Vector3) { + // Simple direct math - no host functions needed + this.position.x += movement.x; + this.position.y += movement.y; + this.position.z += movement.z; + } + + public rotateX(angle: number) { + // Create rotation quaternion and multiply + const rotQuat = Quaternion.fromAxisAngle(new Vector3(1, 0, 0), angle); + this.rotation = Quaternion.multiply(this.rotation, rotQuat); + } + + public rotateY(angle: number) { + const rotQuat = Quaternion.fromAxisAngle(new Vector3(0, 1, 0), angle); + this.rotation = Quaternion.multiply(this.rotation, rotQuat); + } + + public rotateZ(angle: number) { + const rotQuat = Quaternion.fromAxisAngle(new Vector3(0, 0, 1), angle); + this.rotation = Quaternion.multiply(this.rotation, rotQuat); + } + + public scaleUniform(scale: number) { + this.scale.x *= scale; + this.scale.y *= scale; + this.scale.z *= scale; + } + + public scaleIndividual(scale: [number, number, number]) { + this.scale.x *= scale[0]; + this.scale.y *= scale[1]; + this.scale.z *= scale[2]; + } +} /** * A Vector of 3 components: an X, Y and Z. */ -const Vec3 = { - new: (x = 0, y = 0, z = 0): Vec3T => hostFn("createVec3")(x, y, z), - zero: (): Vec3T => hostFn("createVec3")(0, 0, 0), - one: (): Vec3T => hostFn("createVec3")(1, 1, 1), -}; +export class Vector3 { + public x: number; + public y: number; + public z: number; + + public constructor(x: number, y: number, z: number) { + this.x = x; + this.y = y; + this.z = z; + } + + public as_array(): [number, number, number] { + return [this.x, this.y, this.z]; + } + + public static zero(): Vector3 { + return new Vector3(0.0, 0.0, 0.0); + } + + public static one(): Vector3 { + return new Vector3(1.0, 1.0, 1.0); + } + + public static fromArray(arr: [number, number, number]): Vector3 { + return new Vector3(arr[0], arr[1], arr[2]); + } + + // Useful vector operations + public add(other: Vector3): Vector3 { + return new Vector3(this.x + other.x, this.y + other.y, this.z + other.z); + } + + public subtract(other: Vector3): Vector3 { + return new Vector3(this.x - other.x, this.y - other.y, this.z - other.z); + } + + public multiply(scalar: number): Vector3 { + return new Vector3(this.x * scalar, this.y * scalar, this.z * scalar); + } + + public length(): number { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + } + + public normalize(): Vector3 { + const len = this.length(); + if (len === 0) return Vector3.zero(); + return new Vector3(this.x / len, this.y / len, this.z / len); + } +} + +export class Quaternion { + public x: number; + public y: number; + public z: number; + public w: number; + + public constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + public static fromEuler(x: number, y: number, z: number): Quaternion { + // Convert euler angles to quaternion + const cx = Math.cos(x * 0.5); + const sx = Math.sin(x * 0.5); + const cy = Math.cos(y * 0.5); + const sy = Math.sin(y * 0.5); + const cz = Math.cos(z * 0.5); + const sz = Math.sin(z * 0.5); + + return new Quaternion( + sx * cy * cz - cx * sy * sz, + cx * sy * cz + sx * cy * sz, + cx * cy * sz - sx * sy * cz, + cx * cy * cz + sx * sy * sz + ); + } + + public static fromAxisAngle(axis: Vector3, angle: number): Quaternion { + const halfAngle = angle * 0.5; + const sin = Math.sin(halfAngle); + const normalizedAxis = axis.normalize(); + + return new Quaternion( + normalizedAxis.x * sin, + normalizedAxis.y * sin, + normalizedAxis.z * sin, + Math.cos(halfAngle) + ); + } + + public static multiply(a: Quaternion, b: Quaternion): Quaternion { + return new Quaternion( + a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, + a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, + a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, + a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z + ); + } + + public static fromArray(arr: [number, number, number, number]): Quaternion { + return new Quaternion(arr[0], arr[1], arr[2], arr[3]); + } + + public static identity(): Quaternion { + return new Quaternion(0.0, 0.0, 0.0, 1.0); + } -const Quaternion = { - identity: (): QuatT => hostFn("createQuatIdentity")(), - fromEuler: (x: number, y: number, z: number): QuatT => hostFn("createQuatFromEuler")(x, y, z), -}; + public as_array(): [number, number, number, number] { + return [this.x, this.y, this.z, this.w]; + } +} export const Keys = { - // Letters KeyA: "KeyA", KeyB: "KeyB", KeyC: "KeyC", KeyD: "KeyD", KeyE: "KeyE", KeyF: "KeyF", KeyG: "KeyG", KeyH: "KeyH", KeyI: "KeyI", KeyJ: "KeyJ", KeyK: "KeyK", KeyL: "KeyL", KeyM: "KeyM", KeyN: "KeyN", KeyO: "KeyO", KeyP: "KeyP", KeyQ: "KeyQ", KeyR: "KeyR", KeyS: "KeyS", KeyT: "KeyT", KeyU: "KeyU", KeyV: "KeyV", KeyW: "KeyW", KeyX: "KeyX", KeyY: "KeyY", KeyZ: "KeyZ", - - // Numbers Digit0: "Digit0", Digit1: "Digit1", Digit2: "Digit2", Digit3: "Digit3", Digit4: "Digit4", Digit5: "Digit5", Digit6: "Digit6", Digit7: "Digit7", Digit8: "Digit8", Digit9: "Digit9", - - // Special keys Space: "Space", - ShiftLeft: "ShiftLeft", - ShiftRight: "ShiftRight", - ControlLeft: "ControlLeft", - ControlRight: "ControlRight", - AltLeft: "AltLeft", - AltRight: "AltRight", - Escape: "Escape", - Enter: "Enter", - Tab: "Tab", - ArrowUp: "ArrowUp", - ArrowDown: "ArrowDown", - ArrowLeft: "ArrowLeft", - ArrowRight: "ArrowRight", - - // Function keys + ShiftLeft: "ShiftLeft", ShiftRight: "ShiftRight", + ControlLeft: "ControlLeft", ControlRight: "ControlRight", + AltLeft: "AltLeft", AltRight: "AltRight", + Escape: "Escape", Enter: "Enter", Tab: "Tab", + ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", F1: "F1", F2: "F2", F3: "F3", F4: "F4", F5: "F5", F6: "F6", F7: "F7", F8: "F8", F9: "F9", F10: "F10", F11: "F11", F12: "F12", } as const; export type KeyCode = typeof Keys[keyof typeof Keys]; -export const Input = { - /** - * Check if a key is currently pressed - * @param inputState The current input state - * @param key The key to check - * @returns true if the key is pressed, false otherwise - */ - isKeyPressed: (inputState: InputStateT, key: KeyCode): boolean => - hostFn("isKeyPressed")(inputState, key), - - /** - * Get the current mouse X position - * @param inputState The current input state - * @returns The mouse X coordinate - */ - getMouseX: (inputState: InputStateT): number => - hostFn("getMouseX")(inputState), - - /** - * Get the current mouse Y position - * @param inputState The current input state - * @returns The mouse Y coordinate - */ - getMouseY: (inputState: InputStateT): number => - hostFn("getMouseY")(inputState), - - /** - * Get the mouse delta X (movement since last frame) - * @param inputState The current input state - * @returns The mouse delta X - */ - getMouseDeltaX: (inputState: InputStateT): number => - hostFn("getMouseDeltaX")(inputState), - - /** - * Get the mouse delta Y (movement since last frame) - * @param inputState The current input state - * @returns The mouse delta Y - */ - getMouseDeltaY: (inputState: InputStateT): number => - hostFn("getMouseDeltaY")(inputState), - - /** - * Lock or unlock the cursor - * @param locked Whether to lock the cursor - */ - lockCursor: (locked: boolean): void => - hostFn("lockCursor")(locked), -}; - -export const EntityProperties = { - /** - * Get a property value (generic) - * @param properties The entity properties object - * @param key The property key - * @returns The property value or null if not found - */ - getProperty: (properties: any, key: string): any => - hostFn("getProperty")(properties, key), - - /** - * Set a string property - * @param properties The entity properties object - * @param key The property key - * @param value The string value - * @returns Updated properties object - */ - setPropertyString: (properties: any, key: string, value: string): any => - hostFn("setPropertyString")(properties, key, value), - - /** - * Set an integer property - * @param properties The entity properties object - * @param key The property key - * @param value The integer value - * @returns Updated properties object - */ - setPropertyInt: (properties: any, key: string, value: number): any => - hostFn("setPropertyInt")(properties, key, value), - - /** - * Set a float property - * @param properties The entity properties object - * @param key The property key - * @param value The float value - * @returns Updated properties object - */ - setPropertyFloat: (properties: any, key: string, value: number): any => - hostFn("setPropertyFloat")(properties, key, value), - - /** - * Set a boolean property - * @param properties The entity properties object - * @param key The property key - * @param value The boolean value - * @returns Updated properties object - */ - setPropertyBool: (properties: any, key: string, value: boolean): any => - hostFn("setPropertyBool")(properties, key, value), - - /** - * Set a Vec3 property - * @param properties The entity properties object - * @param key The property key - * @param value The Vec3 value as [x, y, z] array - * @returns Updated properties object - */ - setPropertyVec3: (properties: any, key: string, value: [number, number, number]): any => - hostFn("setPropertyVec3")(properties, key, value), - - /** - * Get a string property - * @param properties The entity properties object - * @param key The property key - * @returns The string value or empty string if not found - */ - getString: (properties: any, key: string): string => - hostFn("getString")(properties, key), - - /** - * Get an integer property - * @param properties The entity properties object - * @param key The property key - * @returns The integer value or 0 if not found - */ - getInt: (properties: any, key: string): number => - hostFn("getInt")(properties, key), - - /** - * Get a float property - * @param properties The entity properties object - * @param key The property key - * @returns The float value or 0.0 if not found - */ - getFloat: (properties: any, key: string): number => - hostFn("getFloat")(properties, key), - - /** - * Get a boolean property - * @param properties The entity properties object - * @param key The property key - * @returns The boolean value or false if not found - */ - getBool: (properties: any, key: string): boolean => - hostFn("getBool")(properties, key), - - /** - * Get a Vec3 property - * @param properties The entity properties object - * @param key The property key - * @returns The Vec3 value as [x, y, z] array or [0, 0, 0] if not found - */ - getVec3: (properties: any, key: string): [number, number, number] => - hostFn("getVec3")(properties, key), - - /** - * Check if a property exists - * @param properties The entity properties object - * @param key The property key - * @returns True if the property exists, false otherwise - */ - hasProperty: (properties: any, key: string): boolean => - hostFn("hasProperty")(properties, key), -}; - -export { Transform, Vec3, Quaternion }; +export class EntityProperties { + private data: Record<string, any>; + + public constructor(data?: Record<string, any>) { + this.data = data || {}; + } + + public getRawProperties(): Record<string, any> { + return this.data; + } + + public setString(key: string, value: string): void { + this.data[key] = value; + } + + public setNumber(key: string, value: number): void { + this.data[key] = value; + } + + public setBool(key: string, value: boolean): void { + this.data[key] = value; + } + + public getString(key: string): string { + return this.data[key] || ""; + } + + public getNumber(key: string): number { + return this.data[key] || 0; + } + + public getBool(key: string): boolean { + return this.data[key] || false; + } + + public hasProperty(key: string): boolean { + return key in this.data; + } +} + +export class Entity { + public transform: Transform; + public properties: EntityProperties; + private inputData: InputData | null = null; + + constructor(entityData?: ScriptEntityData) { + if (entityData) { + this.transform = createTransformFromData(entityData.transform); + this.properties = new EntityProperties(entityData.entity.custom_properties); + this.inputData = entityData.input; + } else { + this.transform = new Transform(); + this.properties = new EntityProperties({}); + } + } + + // Input helpers + isKeyPressed(key: KeyCode): boolean { + if (!this.inputData) return false; + return this.inputData.pressed_keys.includes(key); + } + + getMousePosition(): [number, number] { + return this.inputData?.mouse_pos || [0, 0]; + } + + getMouseDelta(): [number, number] | null { + return this.inputData?.mouse_delta || null; + } + + // Movement helpers + moveForward(distance: number): void { + const movement = new Vector3(0, 0, -distance); + this.transform.translate(movement); + } + + moveRight(distance: number): void { + const movement = new Vector3(distance, 0, 0); + this.transform.translate(movement); + } + + moveUp(distance: number): void { + const movement = new Vector3(0, distance, 0); + this.transform.translate(movement); + } + + // Convert back to data format for Rust + toEntityData(): Partial<ScriptEntityData> { + return { + transform: { + position: this.transform.position.as_array(), + rotation: this.transform.rotation.as_array(), + scale: this.transform.scale.as_array() + }, + entity: { + custom_properties: this.properties.getRawProperties() + } + }; + } +} + +// Type definitions +export interface TransformData { + position: [number, number, number]; + rotation: [number, number, number, number]; // quaternion [x, y, z, w] + scale: [number, number, number]; +} + +export interface EntityData { + custom_properties: Record<string, any>; +} + +export interface InputData { + mouse_pos: [number, number]; + pressed_keys: string[]; + mouse_delta: [number, number] | null; + is_cursor_locked: boolean; +} + +export interface ScriptEntityData { + transform: TransformData; + entity: EntityData; + input: InputData; +} + +export function createTransformFromData(data: TransformData): Transform { + const position = Vector3.fromArray(data.position); + const rotation = Quaternion.fromArray(data.rotation); + const scale = Vector3.fromArray(data.scale); + return new Transform(position, rotation, scale); +} +// Global exports globalThis.Transform = Transform; -globalThis.Vec3 = Vec3; +globalThis.Vector3 = Vector3; globalThis.Quaternion = Quaternion; -globalThis.Input = Input; globalThis.Keys = Keys; -globalThis.EntityProperties = EntityProperties; +globalThis.EntityProperties = EntityProperties; +globalThis.Entity = Entity; @@ -441,6 +441,17 @@ impl Component for ScriptComponent { .set_file_name(format!("{}_script.ts", label)) .save_file() { + // check if dropbear module exists + // todo: change this to an %APPDATA% file instead of to memory. + let lib_path = &script_path.clone().parent().unwrap().join("dropbear.ts"); + if let Err(_) = std::fs::read(lib_path) { + log::warn!("dropbear.ts library does not exist in project source directory, copying..."); + if let Err(e) = std::fs::write(lib_path, include_str!("../dropbear.ts")) { + log::error!("Non-fatal error: Creating library file failed: {}", e); + } else { + log::info!("Wrote dropbear.ts library file!"); + } + }; match std::fs::write(&script_path, TEMPLATE_SCRIPT) { Ok(_) => { let script_name = script_path @@ -2,7 +2,7 @@ use super::*; use std::{collections::HashSet, sync::LazyLock}; use dropbear_engine::{entity::Transform, lighting::{Light, LightComponent}}; -use egui; +use egui::{self, CollapsingHeader}; use egui_dock_fork::TabViewer; use egui_extras; use log; @@ -649,11 +649,13 @@ impl<'a> TabViewer for EditorTabViewer<'a> { if let Ok((e, transform, _props, script)) = self .world .query_one_mut::<(&mut AdoptedEntity, Option<&mut Transform>, Option<&ModelProperties>, Option<&mut ScriptComponent>)>(*entity) { - // let label = e.label().clone(); + let label = e.label().clone(); e.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, &mut String::new()); + let mut trans = Transform::new(); if let Some(t) = transform { t.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); + trans = t.clone(); } // if let Some(props) = _props { @@ -664,114 +666,116 @@ impl<'a> TabViewer for EditorTabViewer<'a> { script.inspect(entity, &mut cfg, ui, self.undo_stack, self.signal, e.label_mut()); } + // ============================================================== // todo: convert camera into component - // let entity_id_copy = *entity; - // let entity_label = label.clone(); - // let entity_position = transform.position; - // let camera_manager = &self.camera_manager; - // let signal = &mut *self.signal; - // let get_player_camera_target = - // camera_manager.get_player_camera_target(); - // let get_player_camera_offset = - // camera_manager.get_player_camera_offset(); - // let get_active_type = camera_manager.get_active_type(); - // let get_active_eye = camera_manager.get_active().unwrap().eye; - // - // let followed_entity_label = if let Some(target_entity) = - // get_player_camera_target - // { - // if target_entity != entity_id_copy { - // if let Ok((followed_entity, _, _)) = self.world - // .query_one_mut::<(&AdoptedEntity, &Transform, &ModelProperties)>(target_entity) - // { - // Some(followed_entity.label().to_string()) - // } else { - // None - // } - // } else { - // None - // } - // } else { - // None - // }; - - // ui.group(|ui| { - // CollapsingHeader::new("Camera") - // .default_open(true) - // .show(ui, |ui| { - // ui.horizontal(|ui| { - // if ui.button("Capture Camera Position relative to Entity").clicked() { - // let current_camera_pos = get_active_eye; - // let calculated_offset = current_camera_pos - entity_position; - // log::debug!("Capturing camera offset: entity at {:?}, camera at {:?}, offset: {:?}", - // entity_position, current_camera_pos, calculated_offset); - // *signal = Signal::CameraAction(CameraAction::SetPlayerTarget { - // entity: entity_id_copy, - // offset: calculated_offset, - // }); - // crate::success_without_console!("Camera successfully attached to {}", entity_label); - // } - // }); - // ui.separator(); - // ui.horizontal(|ui| { - // ui.label("Status:"); - // let status_text = match get_active_type { - // crate::camera::CameraType::Debug => { - // egui::RichText::new("Debug Camera (Free)") - // .color(egui::Color32::LIGHT_BLUE) - // }, - // crate::camera::CameraType::Player => { - // if let Some(target_entity) = get_player_camera_target { - // if target_entity == entity_id_copy { - // egui::RichText::new("Following THIS Entity") - // .color(egui::Color32::LIGHT_GREEN) - // .strong() - // } else { - // if let Some(followed_label) = &followed_entity_label { - // egui::RichText::new(format!("Following: {}", followed_label)) - // .color(egui::Color32::YELLOW) - // } else { - // egui::RichText::new("Following: Unknown Entity") - // .color(egui::Color32::RED) - // } - // } - // } else { - // egui::RichText::new("Player Camera (Free)") - // .color(egui::Color32::LIGHT_GRAY) - // } - // } - // }; - // ui.label(status_text); - // }); - // - // ui.separator(); - // - // if let Some(target_entity) = get_player_camera_target { - // if target_entity == entity_id_copy { - // ui.horizontal(|ui| { - // ui.label("Camera Offset:"); - // if let Some(offset) = get_player_camera_offset { - // ui.label(format!("({:.2}, {:.2}, {:.2})", offset.x, offset.y, offset.z)); - // } else { - // ui.label("Unknown"); - // } - // }); - // ui.horizontal(|ui| { - // ui.label("Distance:"); - // let camera_pos = get_active_eye; - // let distance = (camera_pos - entity_position).length(); - // ui.label(format!("{:.2} units", distance)); - // }); - // } - // } - // - // ui.horizontal(|ui| { - // if ui.button("Clear Camera Target").clicked() { - // *signal = Signal::CameraAction(CameraAction::ClearPlayerTarget); - // } - // }); - // }); - // }); + let entity_id_copy = *entity; + let entity_label = label.clone(); + let entity_position = trans.position; + let camera_manager = &self.camera_manager; + let signal = &mut *self.signal; + let get_player_camera_target = + camera_manager.get_player_camera_target(); + let get_player_camera_offset = + camera_manager.get_player_camera_offset(); + let get_active_type = camera_manager.get_active_type(); + let get_active_eye = camera_manager.get_active().unwrap().eye; + + let followed_entity_label = if let Some(target_entity) = + get_player_camera_target + { + if target_entity != entity_id_copy { + if let Ok((followed_entity, _, _)) = self.world + .query_one_mut::<(&AdoptedEntity, &Transform, &ModelProperties)>(target_entity) + { + Some(followed_entity.label().to_string()) + } else { + None + } + } else { + None + } + } else { + None + }; + + ui.group(|ui| { + CollapsingHeader::new("Camera") + .default_open(true) + .show(ui, |ui| { + ui.horizontal(|ui| { + if ui.button("Capture Camera Position relative to Entity").clicked() { + let current_camera_pos = get_active_eye; + let calculated_offset = current_camera_pos - entity_position; + log::debug!("Capturing camera offset: entity at {:?}, camera at {:?}, offset: {:?}", + entity_position, current_camera_pos, calculated_offset); + *signal = Signal::CameraAction(CameraAction::SetPlayerTarget { + entity: entity_id_copy, + offset: calculated_offset, + }); + crate::success_without_console!("Camera successfully attached to {}", entity_label); + } + }); + ui.separator(); + ui.horizontal(|ui| { + ui.label("Status:"); + let status_text = match get_active_type { + crate::camera::CameraType::Debug => { + egui::RichText::new("Debug Camera (Free)") + .color(egui::Color32::LIGHT_BLUE) + }, + crate::camera::CameraType::Player => { + if let Some(target_entity) = get_player_camera_target { + if target_entity == entity_id_copy { + egui::RichText::new("Following THIS Entity") + .color(egui::Color32::LIGHT_GREEN) + .strong() + } else { + if let Some(followed_label) = &followed_entity_label { + egui::RichText::new(format!("Following: {}", followed_label)) + .color(egui::Color32::YELLOW) + } else { + egui::RichText::new("Following: Unknown Entity") + .color(egui::Color32::RED) + } + } + } else { + egui::RichText::new("Player Camera (Free)") + .color(egui::Color32::LIGHT_GRAY) + } + } + }; + ui.label(status_text); + }); + + ui.separator(); + + if let Some(target_entity) = get_player_camera_target { + if target_entity == entity_id_copy { + ui.horizontal(|ui| { + ui.label("Camera Offset:"); + if let Some(offset) = get_player_camera_offset { + ui.label(format!("({:.2}, {:.2}, {:.2})", offset.x, offset.y, offset.z)); + } else { + ui.label("Unknown"); + } + }); + ui.horizontal(|ui| { + ui.label("Distance:"); + let camera_pos = get_active_eye; + let distance = (camera_pos - entity_position).length(); + ui.label(format!("{:.2} units", distance)); + }); + } + } + + ui.horizontal(|ui| { + if ui.button("Clear Camera Target").clicked() { + *signal = Signal::CameraAction(CameraAction::ClearPlayerTarget); + } + }); + }); + }); + // ============================================================== if let Some(t) = cfg.label_last_edit { if t.elapsed() >= Duration::from_millis(500) { @@ -97,17 +97,19 @@ impl Scene for Editor { } let mut script_entities = Vec::new(); - for (entity_id, script) in self.world.query::<&ScriptComponent>().iter() { + for (entity_id, script) in self.world.query::<&mut ScriptComponent>().iter() { + log_once::debug_once!("Script Entity -> id: {:?}, component: {:?}", entity_id, script); + script.name = script.path.file_name().unwrap().to_str().unwrap().to_string(); script_entities.push((entity_id, script.name.clone())); } if script_entities.is_empty() { - log::warn!("Script entities is empty"); + log_once::warn_once!("Script entities is empty"); } for (entity_id, script_name) in script_entities { if let Err(e) = self.script_manager.update_entity_script(entity_id, &script_name, &mut self.world, &self.input_state, dt) { - log::warn!("Failed to update script '{}' for entity {:?}: {}", script_name, entity_id, e); + log_once::warn_once!("Failed to update script '{}' for entity {:?}: {}", script_name, entity_id, e); } } } @@ -380,19 +382,24 @@ impl Scene for Editor { } for (entity_id, script) in script_entities { + log::debug!("Initialising entity script [{}] from path: {}", script.name, script.path.display()); match self.script_manager.load_script(&script.path) { Ok(script_name) => { if let Err(e) = self.script_manager.init_entity_script(entity_id, &script_name, &mut self.world, &self.input_state) { log::warn!("Failed to initialise script '{}' for entity {:?}: {}", script.name, entity_id, e); + self.signal = Signal::StopPlaying; + } else { + crate::success_without_console!("You are in play mode now! Press Escape to exit"); + log::info!("You are in play mode now! Press Escape to exit"); } } Err(e) => { - log::warn!("Failed to load script '{}': {}", script.name, e); + // todo: proper error menu + crate::fatal!("Failed to load script '{}': {}", script.name, e); + self.signal = Signal::StopPlaying; } } } - crate::success_without_console!("You are in play mode now! Press Escape to exit"); - log::info!("You are in play mode now. Press Escape to exit"); } else { crate::fatal!("Unable to build: Player camera not attached to an entity"); } @@ -9,9 +9,10 @@ use clap::{Arg, Command}; use dropbear_engine::{WindowConfiguration, scene}; use eucalyptus::APP_INFO; -#[tokio::main] +// #[tokio::main] #[cfg(feature = "editor")] -async fn main() -> anyhow::Result<()> { +// async +fn main() -> anyhow::Result<()> { #[cfg(target_os = "android")] compile_error!("The `editor` feature is not supported on Android. If you are attempting\ to use the Eucalyptus editor on Android, please don't. Instead, use the `data-only` feature\ @@ -2,7 +2,6 @@ use crate::states::{ModelProperties, PropertyValue}; use rustyscript::{serde_json, Runtime}; use serde::{Deserialize, Serialize}; -// Create a serializable version of ModelProperties for script communication #[derive(Clone, Serialize, Deserialize)] pub struct SerializableModelProperties { pub properties: std::collections::HashMap<String, serde_json::Value>, @@ -12,7 +11,6 @@ impl From<&ModelProperties> for SerializableModelProperties { fn from(props: &ModelProperties) -> Self { let mut properties = std::collections::HashMap::new(); - // Convert each property to JSON value for (key, value) in props.custom_properties.iter() { let json_value = match value { PropertyValue::String(s) => serde_json::Value::String(s.clone()), @@ -17,11 +17,10 @@ pub struct InputState { pub is_cursor_locked: bool, } -// Create a serializable version for script communication #[derive(Clone, Serialize, Deserialize)] pub struct SerializableInputState { pub mouse_pos: (f64, f64), - pub pressed_keys: Vec<String>, // Convert KeyCode to strings + pub pressed_keys: Vec<String>, pub mouse_delta: Option<(f64, f64)>, pub is_cursor_locked: bool, } @@ -58,14 +57,11 @@ impl InputState { } } - /// Locks the cursor. If its true, it locks the cursor, hiding the mouse and setting the value - /// or is_cursor_locked true, which can be used for mouse movement in the scene. pub fn lock_cursor(&mut self, toggle: bool) { self.is_cursor_locked = toggle; } pub fn register_input_modules(runtime: &mut Runtime) -> anyhow::Result<()> { - // Register input functions that take SerializableInputState as parameter runtime.register_function("isKeyPressed", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { if args.len() != 2 { return Err(rustyscript::Error::Runtime("isKeyPressed requires 2 arguments (inputState, keyCode)".to_string())); @@ -132,7 +128,7 @@ impl InputState { return Err(rustyscript::Error::Runtime("lockCursor requires 1 argument (locked)".to_string())); } - let locked = args[0].as_bool() + let _locked = args[0].as_bool() .ok_or_else(|| rustyscript::Error::Runtime("Locked parameter must be a boolean".to_string()))?; // This function would need to communicate back to the engine to actually lock the cursor @@ -145,7 +141,8 @@ impl InputState { } } -// Helper function to convert string to KeyCode +/// Helper function to convert string to KeyCode +#[allow(dead_code)] fn string_to_keycode(key_str: &str) -> Option<KeyCode> { match key_str { "KeyA" => Some(KeyCode::KeyA), @@ -214,7 +211,7 @@ fn string_to_keycode(key_str: &str) -> Option<KeyCode> { } } -// Helper function to convert KeyCode to string +/// Helper function to convert KeyCode to string fn keycode_to_string(key_code: KeyCode) -> Option<String> { match key_code { KeyCode::KeyA => Some("KeyA".to_string()), @@ -4,7 +4,6 @@ use rustyscript::{serde_json, Runtime}; pub fn register_math_functions(runtime: &mut Runtime) -> anyhow::Result<()> { - // Transform functions runtime.register_function("createTransform", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { let transform = Transform::new(); serde_json::to_value(transform) @@ -19,7 +18,6 @@ pub fn register_math_functions(runtime: &mut Runtime) -> anyhow::Result<()> { let mut transform: Transform = serde_json::from_value(args[0].clone()) .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; - // Handle both array and individual values for translation let translation = if let Some(array) = args[1].as_array() { if array.len() != 3 { return Err(rustyscript::Error::Runtime("Translation array must have 3 elements".to_string())); @@ -114,6 +112,7 @@ pub fn register_math_functions(runtime: &mut Runtime) -> anyhow::Result<()> { .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e))) })?; + // this shouldn't be here as there is no need for a matrix... runtime.register_function("transformMatrix", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { if args.len() != 1 { return Err(rustyscript::Error::Runtime("transformMatrix requires 1 argument".to_string())); @@ -122,14 +121,11 @@ pub fn register_math_functions(runtime: &mut Runtime) -> anyhow::Result<()> { let transform: Transform = serde_json::from_value(args[0].clone()) .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?; - // Assuming Transform has a matrix() method that returns a 4x4 matrix - // You may need to adjust this based on your actual Transform implementation let matrix = transform.matrix(); serde_json::to_value(matrix) .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize matrix: {}", e))) })?; - // Vec3 functions runtime.register_function("createVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { let x = args.get(0).and_then(|v| v.as_f64()).unwrap_or(0.0); let y = args.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0); @@ -140,7 +136,6 @@ pub fn register_math_functions(runtime: &mut Runtime) -> anyhow::Result<()> { .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Vec3: {}", e))) })?; - // Quaternion functions runtime.register_function("createQuatIdentity", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> { let quat = DQuat::IDENTITY; serde_json::to_value(quat) @@ -165,6 +165,11 @@ impl ScriptManager { pub fn new() -> anyhow::Result<Self> { let mut runtime = Runtime::new(RuntimeOptions::default())?; + let dropbear_content = include_str!("../dropbear.ts"); + let dropbear_module = Module::new("./dropbear.ts", dropbear_content); + runtime.load_module(&dropbear_module)?; + log::debug!("Loaded dropbear module"); + // Register modules math::register_math_functions(&mut runtime)?; input::InputState::register_input_modules(&mut runtime)?; @@ -186,7 +191,7 @@ impl ScriptManager { .as_secs_f64(); Ok(serde_json::Value::Number(serde_json::Number::from_f64(time).unwrap())) })?; - + log::debug!("Initialised ScriptManager"); Ok(Self { runtime, compiled_scripts: HashMap::new(), @@ -215,18 +220,18 @@ impl ScriptManager { log_once::debug_once!("init_entity_script: '{}' for entity {:?}", script_name, entity_id); } - if let Some(module) = self.compiled_scripts.get(script_name) { - // Prepare script data + log::debug!("Init Entity Script module name: {}", script_name); + if let Some(module) = self.compiled_scripts.get(script_name).cloned() { let mut script_data = serde_json::Map::new(); - // Add transform data + // transform if let Ok(mut transform_query) = world.query_one::<&Transform>(entity_id) { if let Some(transform) = transform_query.get() { script_data.insert("transform".to_string(), serde_json::to_value(transform)?); } } - // Add entity properties + // entity props if let Ok(mut properties_query) = world.query_one::<&ModelProperties>(entity_id) { if let Some(properties) = properties_query.get() { script_data.insert("entity".to_string(), serde_json::to_value(properties)?); @@ -239,19 +244,33 @@ impl ScriptManager { script_data.insert("entity".to_string(), serde_json::to_value(&default_props)?); } - // Add input state + // input state let serializable_input = input::SerializableInputState::from(input_state); script_data.insert("input".to_string(), serde_json::to_value(&serializable_input)?); - // Call init function if it exists - specify return type - let args: Vec<serde_json::Value> = Vec::new(); - if let Ok(_result) = self.runtime.call_function::<serde_json::Value>(Some(module), "load", &args) { - log::debug!("Called init for entity {:?}", entity_id); + // call onLoad + let script_data_value = serde_json::Value::Object(script_data); + match self.runtime.call_function::<serde_json::Value>(Some(&module), "onLoad", &vec![script_data_value.clone()]) { + Ok(result) => { + log::debug!("Called onLoad for entity {:?}", entity_id); + + let updated_data = if result.is_object() { + result + } else { + // JIC script isn't returning updated entity + script_data_value + }; + + self.apply_script_data_to_world(entity_id, &updated_data, world)?; + + self.entity_script_data.insert(entity_id, updated_data); + } + Err(e) => { + log::warn!("onLoad function not found or failed for entity {:?}: {}", entity_id, e); + self.entity_script_data.insert(entity_id, script_data_value); + } } - // Store script data for this entity - self.entity_script_data.insert(entity_id, serde_json::Value::Object(script_data)); - Ok(()) } else { Err(anyhow::anyhow!("Script '{}' not found", script_name)) @@ -266,50 +285,114 @@ impl ScriptManager { input_state: &input::InputState, dt: f32, ) -> anyhow::Result<()> { - if let Some(module) = self.compiled_scripts.get(script_name) { - // Update script data + log_once::debug_once!("Update entity script name: {}", script_name); + if let Some(module) = self.compiled_scripts.get(script_name).cloned() { let mut script_data = serde_json::Map::new(); - // Update transform data + // transform if let Ok(mut transform_query) = world.query_one::<&Transform>(entity_id) { if let Some(transform) = transform_query.get() { script_data.insert("transform".to_string(), serde_json::to_value(transform)?); } } - // Update entity properties + // entity props if let Ok(mut properties_query) = world.query_one::<&ModelProperties>(entity_id) { if let Some(properties) = properties_query.get() { script_data.insert("entity".to_string(), serde_json::to_value(properties)?); + } else { + let default_props = ModelProperties::default(); + script_data.insert("entity".to_string(), serde_json::to_value(&default_props)?); } + } else { + let default_props = ModelProperties::default(); + script_data.insert("entity".to_string(), serde_json::to_value(&default_props)?); } - // Update input state + // input state let serializable_input = input::SerializableInputState::from(input_state); script_data.insert("input".to_string(), serde_json::to_value(&serializable_input)?); - // Call update function if it exists - specify return type and fix parameter passing + let script_data_value = serde_json::Value::Object(script_data); let dt_value = serde_json::Value::Number(serde_json::Number::from_f64(dt as f64).unwrap()); - match self.runtime.call_function::<serde_json::Value>(Some(module), "update", &vec![dt_value]) { - Ok(_result) => { + + match self.runtime.call_function::<serde_json::Value>(Some(&module), "onUpdate", &vec![script_data_value.clone(), dt_value]) { + Ok(result) => { log::trace!("Called update for entity {:?}", entity_id); - // Here you would need to extract any modified data from the script - // and update the world accordingly. This depends on how rustyscript - // handles data exchange between Rust and JS. + + if let Some(result_obj) = result.as_object() { + if let Some(transform_value) = result_obj.get("transform") { + if let Ok(new_transform) = serde_json::from_value::<Transform>(transform_value.clone()) { + if let Ok(mut transform_query) = world.query_one::<&mut Transform>(entity_id) { + if let Some(transform) = transform_query.get() { + *transform = new_transform; + log::trace!("Updated transform for entity {:?}", entity_id); + } + } + } + } + + if let Some(entity_value) = result_obj.get("entity") { + if let Ok(new_properties) = serde_json::from_value::<ModelProperties>(entity_value.clone()) { + if let Ok(mut properties_query) = world.query_one::<&mut ModelProperties>(entity_id) { + if let Some(properties) = properties_query.get() { + *properties = new_properties; + log::trace!("Updated properties for entity {:?}", entity_id); + } + } + } + } + } } Err(e) => { log_once::error_once!("Script execution error for entity {:?}: {}", entity_id, e); } } - - // Update stored script data - self.entity_script_data.insert(entity_id, serde_json::Value::Object(script_data)); } else { log_once::error_once!("Unable to fetch compiled scripts for entity {:?}. Script Name: {}", entity_id, script_name); } Ok(()) } + fn apply_script_data_to_world( + &self, + entity_id: hecs::Entity, + script_data: &serde_json::Value, + world: &mut World, + ) -> anyhow::Result<()> { + if let Some(data_obj) = script_data.as_object() { + // Update transform if it exists in the script data + if let Some(transform_value) = data_obj.get("transform") { + if let Ok(updated_transform) = serde_json::from_value::<Transform>(transform_value.clone()) { + if let Ok(mut transform_query) = world.query_one::<&mut Transform>(entity_id) { + if let Some(transform) = transform_query.get() { + *transform = updated_transform; + log::trace!("Updated transform for entity {:?}", entity_id); + } + } + } + } + + if let Some(entity_value) = data_obj.get("entity") { + if let Ok(updated_properties) = serde_json::from_value::<ModelProperties>(entity_value.clone()) { + if let Ok(mut properties_query) = world.query_one::<&mut ModelProperties>(entity_id) { + if let Some(properties) = properties_query.get() { + *properties = updated_properties; + log::trace!("Updated properties for entity {:?}", entity_id); + } + } else { + if let Err(e) = world.insert_one(entity_id, updated_properties) { + log::warn!("Failed to insert updated properties for entity {:?}: {}", entity_id, e); + } + } + } + } + // input state doesn't get updated, it is only read. + } + + Ok(()) + } + pub fn load_script_from_source(&mut self, script_name: &String, script_content: &String) -> anyhow::Result<String> { let module = Module::new(&script_name, script_content); @@ -327,15 +410,20 @@ impl ScriptManager { } pub fn load_script(&mut self, script_path: &PathBuf) -> anyhow::Result<String> { + log::debug!("Reading script content"); let script_content = fs::read_to_string(script_path)?; + log::debug!("Fetching script name"); let script_name = script_path - .file_stem() + .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); + log::debug!("Script name: {}", script_name); + log::debug!("Creating module for typescript runtime"); let module = Module::new(&script_name, &script_content); + log::debug!("Loading module"); match self.runtime.load_module(&module) { Ok(module) => { self.compiled_scripts.insert(script_name.clone(), module); @@ -614,12 +614,12 @@ pub struct SceneEntity { pub entity_id: Option<hecs::Entity>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct ModelProperties { pub custom_properties: HashMap<String, PropertyValue>, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum PropertyValue { String(String), Int(i64), @@ -739,7 +739,7 @@ impl SceneConfig { ); world.clear(); - let project_config = if !cfg!(feature = "data-only") { + let _project_config = if !cfg!(feature = "data-only") { if let Ok(cfg) = PROJECT.read() { cfg.project_path.clone() } else { @@ -940,26 +940,64 @@ impl SceneConfig { if adopted_entity.label() == target_label { Some(entity_id) } else { - let stem_match = if cfg!(feature = "data-only") { - adopted_entity.model().path.to_executable_path().unwrap() - } else { + // Try to match by file stem if label doesn't match + #[cfg(not(feature = "data-only"))] + { let project_path = if let Ok(cfg) = PROJECT.read() { cfg.project_path.clone() } else { panic!("Unable to get project path to use with camera manager"); }; - adopted_entity.model().path.to_project_path(project_path).unwrap() - }; - - let stem_match = stem_match.file_stem() - .and_then(|s| s.to_str()) - .map(|s| s == target_label) - .unwrap_or(false); - - if stem_match { - Some(entity_id) - } else { - None + + match &adopted_entity.model().path.ref_type { + ResourceReferenceType::File(_reference) => { + if let Some(path) = adopted_entity.model().path.to_project_path(project_path) { + let stem_match = path.file_stem() + .and_then(|s| s.to_str()) + .map(|s| s == target_label) + .unwrap_or(false); + + if stem_match { + Some(entity_id) + } else { + None + } + } else { + None + } + }, + ResourceReferenceType::Bytes(_bytes) => { + // For bytes, we can only match by label, which already failed + None + } + _ => None + } + } + #[cfg(feature = "data-only")] + { + match &adopted_entity.model().path.ref_type { + ResourceReferenceType::File(_reference) => { + if let Ok(path) = adopted_entity.model().path.to_executable_path() { + let stem_match = path.file_stem() + .and_then(|s| s.to_str()) + .map(|s| s == target_label) + .unwrap_or(false); + + if stem_match { + Some(entity_id) + } else { + None + } + } else { + None + } + }, + ResourceReferenceType::Bytes(_bytes) => { + // For bytes, we can only match by label, which already failed + None + } + _ => None + } } } }); @@ -1,10 +1,25 @@ // dropbear-engine script template for eucalyptus -import * as Dropbear from "./dropbear"; +import * as dropbear from "./dropbear.ts"; -export function onLoad() { +export function onLoad(e) { + const entity = new dropbear.Entity(e); + // ------- Your own code here ------- + console.log("I have been awoken"); + + // ---------------------------------- + // Do not remove anything outside unless + // you know what you are doing. + return entity.toEntityData(); } -export function onUpdate(dt: Number) { - +export function onUpdate(e, dt: number) { + const entity = new dropbear.Entity(e); + // ------- Your own code here ------- + console.log("I'm being updated!"); + + + // ---------------------------------- + // Same thing over here! + return entity.toEntityData(); }