kitgit

tirbofish/dropbear · commit

d59a37a11b31ec0eb55240beb1a19d0a636c7771

Merge pull request #42 from 4tkbytes/typescript changing from rhai scripting to typescript scripting. fixes #40. jarvis, run github actions (if you work in the desc).

Unverified

tk <4tkbytes@pm.me> · 2025-09-01 16:41

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index c00c557..3d89336 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,7 @@ package.repository = "https://github.com/4tkbytes/dropbear-engine"
 package.readme = "README.md"
 
 resolver = "3"
-members = ["dropbear-engine", "eucalyptus", "gleek", "redback-runtime"]
+members = ["dropbear-engine", "eucalyptus", "redback-runtime"]
 
 [workspace.dependencies]
 anyhow = { version = "1.0", features = ["backtrace"] }
@@ -40,16 +40,16 @@ open = "5"
 parking_lot = {version = "0.12", features = ["deadlock_detection"] }
 pollster = "0.4"
 rfd = "0.15.4"
-rhai = "1.22"
-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"
 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]
@@ -57,13 +57,6 @@ version = "0.25"
 default-features = false
 features = ["png"]
 
-[patch.crates-io]
-dropbear-engine = { path = "dropbear-engine" }
-eucalyptus = { path = "eucalyptus" }
-# fixing android compilation
-russimp-sys-ng = { git = "https://github.com/Kek5chen/russimp-sys-ng", branch = "fix-android-compilation" }
-
-
 [profile.dev]
 opt-level = 0
 debug = true
diff --git a/README.md b/README.md
index 8cddf1f..d9fb440 100644
--- a/README.md
+++ b/README.md
@@ -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 build-essential 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,6 @@ 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.  -->
+
diff --git a/dropbear-engine/Cargo.toml b/dropbear-engine/Cargo.toml
index 77b3464..a3cbcd5 100644
--- a/dropbear-engine/Cargo.toml
+++ b/dropbear-engine/Cargo.toml
@@ -23,7 +23,6 @@ gilrs.workspace = true
 glam.workspace = true
 log.workspace = true
 log-once.workspace = true
-#rfd.workspace = true
 russimp-ng.workspace = true
 pollster.workspace = true
 serde.workspace = true
diff --git a/dropbear-engine/src/panic.rs b/dropbear-engine/src/panic.rs
index 95f8b91..b962155 100644
--- a/dropbear-engine/src/panic.rs
+++ b/dropbear-engine/src/panic.rs
@@ -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();
     }));
 }
diff --git a/eucalyptus/Cargo.toml b/eucalyptus/Cargo.toml
index 0f77716..6a17197 100644
--- a/eucalyptus/Cargo.toml
+++ b/eucalyptus/Cargo.toml
@@ -31,7 +31,6 @@ model_to_image = { workspace = true, optional = true }
 once_cell = { workspace = true, optional = true }
 open = { workspace = true, optional = true }
 parking_lot = { workspace = true, optional = true }
-rhai = { workspace = true, optional = true }
 ron = { workspace = true, optional = true }
 serde = { workspace = true, optional = true }
 tokio = { workspace = true, optional = true }
@@ -43,6 +42,8 @@ walkdir = { workspace = true, optional = true }
 zip = { workspace = true, optional = true }
 bytemuck = "1.23.2"
 
+rustyscript.workspace = true
+
 [target.'cfg(not(target_os = "android"))'.dependencies]
 rfd = { workspace = true, optional = true }
 
@@ -69,7 +70,6 @@ editor = [
     "open",
     "parking_lot",
     "rfd",
-    "rhai",
     "ron",
     "serde",
     "tokio",
@@ -90,7 +90,6 @@ data-only = [
     "glam",
     "log",
     "dropbear-engine",
-    "rhai",
     "winit",
     "hecs",
     "ron",
@@ -107,4 +106,4 @@ data-only = [
 anyhow = "1.0"
 app_dirs2 = "2.5"
 reqwest = { version = "0.12", features = ["blocking"] }
-zip = "4.3"
\ No newline at end of file
+zip = "4.3"
diff --git a/eucalyptus/build.rs b/eucalyptus/build.rs
index 3078c4f..f27a6d2 100644
--- a/eucalyptus/build.rs
+++ b/eucalyptus/build.rs
@@ -2,6 +2,7 @@ use std::fs::{self, File};
 use std::io::Cursor;
 
 fn main() -> anyhow::Result<()> {
+    // todo: move this into the "setup" process
     let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip";
     let response = reqwest::blocking::get(repo_zip_url)
         .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))?
@@ -54,6 +55,12 @@ fn main() -> anyhow::Result<()> {
         ));
     }
 
+    #[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(())
 }
diff --git a/eucalyptus/src/dropbear.ts b/eucalyptus/src/dropbear.ts
new file mode 100644
index 0000000..b382d9a
--- /dev/null
+++ b/eucalyptus/src/dropbear.ts
@@ -0,0 +1,331 @@
+// Modules for the dropbear-engine scripting component
+// Made by 4tkbytes
+// EDIT THIS IF YOU WISH, RECOMMENDED TO NOT TOUCH IT
+
+type InputStateT = any;
+
+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();
+    }
+
+    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. 
+ */
+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);
+    }
+
+    public as_array(): [number, number, number, number] {
+        return [this.x, this.y, this.z, this.w];
+    }
+}
+
+export const Keys = {
+    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",
+    Digit0: "Digit0", Digit1: "Digit1", Digit2: "Digit2", Digit3: "Digit3", Digit4: "Digit4",
+    Digit5: "Digit5", Digit6: "Digit6", Digit7: "Digit7", Digit8: "Digit8", Digit9: "Digit9",
+    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",
+    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 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.Vector3 = Vector3;
+globalThis.Quaternion = Quaternion;
+globalThis.Keys = Keys;
+globalThis.EntityProperties = EntityProperties;
+globalThis.Entity = Entity;
\ No newline at end of file
diff --git a/eucalyptus/src/editor/component.rs b/eucalyptus/src/editor/component.rs
index 250a3a7..760311d 100644
--- a/eucalyptus/src/editor/component.rs
+++ b/eucalyptus/src/editor/component.rs
@@ -420,7 +420,7 @@ impl Component for ScriptComponent {
                     ui.horizontal(|ui| {
                         if ui.button("Browse").clicked() {
                             if let Some(script_file) = rfd::FileDialog::new()
-                                .add_filter("Rhai Script", &["rhai"])
+                                .add_filter("Typescript", &["ts"])
                                 .pick_file()
                             {
                                 let script_name = script_file
@@ -437,10 +437,21 @@ impl Component for ScriptComponent {
 
                         if ui.button("New").clicked() {
                             if let Some(script_path) = rfd::FileDialog::new()
-                                .add_filter("Rhai Script", &["rhai"])
-                                .set_file_name(format!("{}_script.rhai", label))
+                                .add_filter("TypeScript", &["ts"])
+                                .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
diff --git a/eucalyptus/src/editor/dock.rs b/eucalyptus/src/editor/dock.rs
index 1c0c4df..aad4542 100644
--- a/eucalyptus/src/editor/dock.rs
+++ b/eucalyptus/src/editor/dock.rs
@@ -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) {
diff --git a/eucalyptus/src/editor/mod.rs b/eucalyptus/src/editor/mod.rs
index 50f29ab..7c1a351 100644
--- a/eucalyptus/src/editor/mod.rs
+++ b/eucalyptus/src/editor/mod.rs
@@ -217,7 +217,7 @@ impl Editor {
             viewport_mode: ViewportMode::None,
             signal: Signal::None,
             undo_stack: Vec::new(),
-            script_manager: ScriptManager::new(),
+            script_manager: ScriptManager::new().unwrap(),
             editor_state: EditorState::Editing,
             gizmo_mode: EnumSet::empty(),
             play_mode_backup: None,
@@ -540,7 +540,6 @@ impl Editor {
                         viewport_mode: &mut self.viewport_mode,
                         undo_stack: &mut self.undo_stack,
                         signal: &mut self.signal,
-                        // engine: &mut self.rhai_engine,
                         camera_manager: &mut self.camera_manager,
                         gizmo_mode: &mut self.gizmo_mode,
                         editor_mode: &mut self.editor_state,
diff --git a/eucalyptus/src/editor/scene.rs b/eucalyptus/src/editor/scene.rs
index 847a228..c174d1d 100644
--- a/eucalyptus/src/editor/scene.rs
+++ b/eucalyptus/src/editor/scene.rs
@@ -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");
                 }
diff --git a/eucalyptus/src/main.rs b/eucalyptus/src/main.rs
index 5464df8..1881547 100644
--- a/eucalyptus/src/main.rs
+++ b/eucalyptus/src/main.rs
@@ -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\
diff --git a/eucalyptus/src/scripting/entity.rs b/eucalyptus/src/scripting/entity.rs
index 86a9bf2..73ab529 100644
--- a/eucalyptus/src/scripting/entity.rs
+++ b/eucalyptus/src/scripting/entity.rs
@@ -1,89 +1,331 @@
-use rhai::{Dynamic, Engine};
-
 use crate::states::{ModelProperties, PropertyValue};
+use rustyscript::{serde_json, Runtime};
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Serialize, Deserialize)]
+pub struct SerializableModelProperties {
+    pub properties: std::collections::HashMap<String, serde_json::Value>,
+}
+
+impl From<&ModelProperties> for SerializableModelProperties {
+    fn from(props: &ModelProperties) -> Self {
+        let mut properties = std::collections::HashMap::new();
+        
+        for (key, value) in props.custom_properties.iter() {
+            let json_value = match value {
+                PropertyValue::String(s) => serde_json::Value::String(s.clone()),
+                PropertyValue::Int(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
+                PropertyValue::Float(f) => serde_json::Value::Number(
+                    serde_json::Number::from_f64(*f as f64).unwrap_or(serde_json::Number::from(0))
+                ),
+                PropertyValue::Bool(b) => serde_json::Value::Bool(*b),
+                PropertyValue::Vec3(v) => serde_json::Value::Array(vec![
+                    serde_json::Value::Number(serde_json::Number::from_f64(v[0] as f64).unwrap()),
+                    serde_json::Value::Number(serde_json::Number::from_f64(v[1] as f64).unwrap()),
+                    serde_json::Value::Number(serde_json::Number::from_f64(v[2] as f64).unwrap()),
+                ]),
+            };
+            properties.insert(key.clone(), json_value);
+        }
+        
+        Self { properties }
+    }
+}
+
+impl SerializableModelProperties {
+    pub fn to_model_properties(&self) -> ModelProperties {
+        let mut props = ModelProperties::default();
+        
+        for (key, value) in &self.properties {
+            let property_value = match value {
+                serde_json::Value::String(s) => PropertyValue::String(s.clone()),
+                serde_json::Value::Number(n) => {
+                    if let Some(i) = n.as_i64() {
+                        PropertyValue::Int(i)
+                    } else if let Some(f) = n.as_f64() {
+                        PropertyValue::Float(f)
+                    } else {
+                        continue;
+                    }
+                },
+                serde_json::Value::Bool(b) => PropertyValue::Bool(*b),
+                serde_json::Value::Array(arr) => {
+                    if arr.len() == 3 {
+                        if let (Some(x), Some(y), Some(z)) = (
+                            arr[0].as_f64(),
+                            arr[1].as_f64(),
+                            arr[2].as_f64(),
+                        ) {
+                            PropertyValue::Vec3([x as f32, y as f32, z as f32])
+                        } else {
+                            continue;
+                        }
+                    } else {
+                        continue;
+                    }
+                },
+                _ => continue,
+            };
+            props.set_property(key.clone(), property_value);
+        }
+        
+        props
+    }
+}
+
+pub fn register_model_props_module(runtime: &mut Runtime) -> anyhow::Result<()> {
+    // Get property (generic)
+    runtime.register_function("getProperty", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("getProperty requires 2 arguments (properties, key)".to_string()));
+        }
+        
+        let props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = props.properties.get(key)
+            .cloned()
+            .unwrap_or(serde_json::Value::Null);
+        
+        Ok(value)
+    })?;
+
+    // Set property (string)
+    runtime.register_function("setPropertyString", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 3 {
+            return Err(rustyscript::Error::Runtime("setPropertyString requires 3 arguments (properties, key, value)".to_string()));
+        }
+        
+        let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = args[2].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Value must be a string".to_string()))?;
+        
+        props.properties.insert(key.to_string(), serde_json::Value::String(value.to_string()));
+        
+        serde_json::to_value(props)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e)))
+    })?;
 
-pub fn register_model_props_module(engine: &mut Engine) {
-    engine.register_type_with_name::<ModelProperties>("Properties");
-
-    engine.register_fn("get_property", |props: &mut ModelProperties, key: &str| -> Dynamic {
-        match props.get_property(key) {
-            Some(PropertyValue::String(s)) => Dynamic::from(s.clone()),
-            Some(PropertyValue::Int(i)) => Dynamic::from(*i),
-            Some(PropertyValue::Float(f)) => Dynamic::from(*f),
-            Some(PropertyValue::Bool(b)) => Dynamic::from(*b),
-            Some(PropertyValue::Vec3(v)) => Dynamic::from([v[0] as f64, v[1] as f64, v[2] as f64]),
-            None => Dynamic::UNIT,
+    // Set property (int)
+    runtime.register_function("setPropertyInt", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 3 {
+            return Err(rustyscript::Error::Runtime("setPropertyInt requires 3 arguments (properties, key, value)".to_string()));
         }
-    });
-
-    engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: String| {
-        props.set_property(key.to_string(), PropertyValue::String(value));
-    });
-
-    engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: i64| {
-        props.set_property(key.to_string(), PropertyValue::Int(value));
-    });
-
-    engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: f64| {
-        props.set_property(key.to_string(), PropertyValue::Float(value));
-    });
-
-    engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: bool| {
-        props.set_property(key.to_string(), PropertyValue::Bool(value));
-    });
-
-    engine.register_fn("set_property", |props: &mut ModelProperties, key: &str, value: rhai::Array| {
-        if value.len() == 3 {
-            if let (Ok(x), Ok(y), Ok(z)) = (
-                value[0].as_float(),
-                value[1].as_float(), 
-                value[2].as_float()
-            ) {
-                props.set_property(key.to_string(), PropertyValue::Vec3([x as f32, y as f32, z as f32]));
-            }
+        
+        let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = args[2].as_i64()
+            .ok_or_else(|| rustyscript::Error::Runtime("Value must be an integer".to_string()))?;
+        
+        props.properties.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value)));
+        
+        serde_json::to_value(props)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e)))
+    })?;
+
+    // Set property (float)
+    runtime.register_function("setPropertyFloat", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 3 {
+            return Err(rustyscript::Error::Runtime("setPropertyFloat requires 3 arguments (properties, key, value)".to_string()));
         }
-    });
+        
+        let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = args[2].as_f64()
+            .ok_or_else(|| rustyscript::Error::Runtime("Value must be a number".to_string()))?;
+        
+        props.properties.insert(key.to_string(), serde_json::Value::Number(
+            serde_json::Number::from_f64(value).unwrap_or(serde_json::Number::from(0))
+        ));
+        
+        serde_json::to_value(props)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e)))
+    })?;
 
-    engine.register_fn("get_string", |props: &mut ModelProperties, key: &str| -> String {
-        match props.get_property(key) {
-            Some(PropertyValue::String(s)) => s.clone(),
-            _ => String::new(),
+    // Set property (bool)
+    runtime.register_function("setPropertyBool", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 3 {
+            return Err(rustyscript::Error::Runtime("setPropertyBool requires 3 arguments (properties, key, value)".to_string()));
         }
-    });
+        
+        let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = args[2].as_bool()
+            .ok_or_else(|| rustyscript::Error::Runtime("Value must be a boolean".to_string()))?;
+        
+        props.properties.insert(key.to_string(), serde_json::Value::Bool(value));
+        
+        serde_json::to_value(props)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e)))
+    })?;
 
-    engine.register_fn("get_int", |props: &mut ModelProperties, key: &str| -> i64 {
-        match props.get_property(key) {
-            Some(PropertyValue::Int(i)) => *i,
-            _ => 0,
+    // Set property (Vec3)
+    runtime.register_function("setPropertyVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 3 {
+            return Err(rustyscript::Error::Runtime("setPropertyVec3 requires 3 arguments (properties, key, value)".to_string()));
+        }
+        
+        let mut props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = args[2].as_array()
+            .ok_or_else(|| rustyscript::Error::Runtime("Value must be an array".to_string()))?;
+        
+        if value.len() != 3 {
+            return Err(rustyscript::Error::Runtime("Vec3 array must have exactly 3 elements".to_string()));
         }
-    });
+        
+        let x = value[0].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?;
+        let y = value[1].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?;
+        let z = value[2].as_f64().ok_or_else(|| rustyscript::Error::Runtime("Vec3 elements must be numbers".to_string()))?;
+        
+        let vec3_array = serde_json::Value::Array(vec![
+            serde_json::Value::Number(serde_json::Number::from_f64(x).unwrap()),
+            serde_json::Value::Number(serde_json::Number::from_f64(y).unwrap()),
+            serde_json::Value::Number(serde_json::Number::from_f64(z).unwrap()),
+        ]);
+        
+        props.properties.insert(key.to_string(), vec3_array);
+        
+        serde_json::to_value(props)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize properties: {}", e)))
+    })?;
 
-    engine.register_fn("get_float", |props: &mut ModelProperties, key: &str| -> f64 {
-        match props.get_property(key) {
-            Some(PropertyValue::Float(f)) => *f,
-            _ => 0.0,
+    // Typed getters
+    runtime.register_function("getString", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("getString requires 2 arguments (properties, key)".to_string()));
         }
-    });
+        
+        let props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = props.properties.get(key)
+            .and_then(|v| v.as_str())
+            .unwrap_or("")
+            .to_string();
+        
+        Ok(serde_json::Value::String(value))
+    })?;
 
-    engine.register_fn("get_bool", |props: &mut ModelProperties, key: &str| -> bool {
-        match props.get_property(key) {
-            Some(PropertyValue::Bool(b)) => *b,
-            _ => false,
+    runtime.register_function("getInt", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("getInt requires 2 arguments (properties, key)".to_string()));
         }
-    });
-
-    engine.register_fn("get_vec3", |props: &mut ModelProperties, key: &str| -> rhai::Array {
-        match props.get_property(key) {
-            Some(PropertyValue::Vec3(v)) => {
-                vec![Dynamic::from(v[0] as f64), Dynamic::from(v[1] as f64), Dynamic::from(v[2] as f64)]
-            },
-            _ => vec![Dynamic::from(0.0), Dynamic::from(0.0), Dynamic::from(0.0)],
+        
+        let props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = props.properties.get(key)
+            .and_then(|v| v.as_i64())
+            .unwrap_or(0);
+        
+        Ok(serde_json::Value::Number(serde_json::Number::from(value)))
+    })?;
+
+    runtime.register_function("getFloat", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("getFloat requires 2 arguments (properties, key)".to_string()));
         }
-    });
+        
+        let props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = props.properties.get(key)
+            .and_then(|v| v.as_f64())
+            .unwrap_or(0.0);
+        
+        Ok(serde_json::Value::Number(serde_json::Number::from_f64(value).unwrap()))
+    })?;
 
-    engine.register_fn("has_property", |props: &mut ModelProperties, key: &str| -> bool {
-        props.get_property(key).is_some()
-    });
+    runtime.register_function("getBool", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("getBool requires 2 arguments (properties, key)".to_string()));
+        }
+        
+        let props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = props.properties.get(key)
+            .and_then(|v| v.as_bool())
+            .unwrap_or(false);
+        
+        Ok(serde_json::Value::Bool(value))
+    })?;
+
+    runtime.register_function("getVec3", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("getVec3 requires 2 arguments (properties, key)".to_string()));
+        }
+        
+        let props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let value = props.properties.get(key)
+            .and_then(|v| v.as_array())
+            .cloned()
+            .unwrap_or_else(|| vec![
+                serde_json::Value::Number(serde_json::Number::from(0)),
+                serde_json::Value::Number(serde_json::Number::from(0)),
+                serde_json::Value::Number(serde_json::Number::from(0)),
+            ]);
+        
+        Ok(serde_json::Value::Array(value))
+    })?;
+
+    runtime.register_function("hasProperty", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("hasProperty requires 2 arguments (properties, key)".to_string()));
+        }
+        
+        let props: SerializableModelProperties = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid properties: {}", e)))?;
+        
+        let key = args[1].as_str()
+            .ok_or_else(|| rustyscript::Error::Runtime("Key must be a string".to_string()))?;
+        
+        let has_property = props.properties.contains_key(key);
+        Ok(serde_json::Value::Bool(has_property))
+    })?;
 
     log::info!("[Script] Initialised entity custom properties module");
+    Ok(())
 }
\ No newline at end of file
diff --git a/eucalyptus/src/scripting/input.rs b/eucalyptus/src/scripting/input.rs
index 3938c2d..0f4de63 100644
--- a/eucalyptus/src/scripting/input.rs
+++ b/eucalyptus/src/scripting/input.rs
@@ -1,11 +1,14 @@
 use std::{collections::{HashMap, HashSet}, time::{Duration, Instant}};
 
-use rhai::*;
+use rustyscript::{serde_json, Runtime};
+use serde::{Deserialize, Serialize};
 use winit::{event::MouseButton, keyboard::KeyCode};
 
-#[derive(rhai::CustomType, Clone)]
+#[derive(Clone)]
 pub struct InputState {
+    #[allow(dead_code)]
     pub last_key_press_times: HashMap<KeyCode, Instant>,
+    #[allow(dead_code)]
     pub double_press_threshold: Duration,
     pub mouse_pos: (f64, f64),
     pub mouse_button: HashSet<MouseButton>,    
@@ -14,6 +17,27 @@ pub struct InputState {
     pub is_cursor_locked: bool,
 }
 
+#[derive(Clone, Serialize, Deserialize)]
+pub struct SerializableInputState {
+    pub mouse_pos: (f64, f64),
+    pub pressed_keys: Vec<String>,
+    pub mouse_delta: Option<(f64, f64)>,
+    pub is_cursor_locked: bool,
+}
+
+impl From<&InputState> for SerializableInputState {
+    fn from(input_state: &InputState) -> Self {
+        Self {
+            mouse_pos: input_state.mouse_pos,
+            pressed_keys: input_state.pressed_keys.iter()
+                .filter_map(|key| keycode_to_string(*key))
+                .collect(),
+            mouse_delta: input_state.mouse_delta,
+            is_cursor_locked: input_state.is_cursor_locked,
+        }
+    }
+}
+
 impl Default for InputState {
     fn default() -> Self {
         Self::new()
@@ -33,78 +57,228 @@ 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(engine: &mut Engine) {
-        engine.register_type_with_name::<InputState>("InputState");
-        engine.register_type_with_name::<Key>("Key");
+    pub fn register_input_modules(runtime: &mut Runtime) -> anyhow::Result<()> {
+        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()));
+            }
+            
+            let input_state: SerializableInputState = serde_json::from_value(args[0].clone())
+                .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?;
+            
+            let key_code_str = args[1].as_str()
+                .ok_or_else(|| rustyscript::Error::Runtime("Key code must be a string".to_string()))?;
+            
+            let is_pressed = input_state.pressed_keys.contains(&key_code_str.to_string());
+            Ok(serde_json::Value::Bool(is_pressed))
+        })?;
 
-        engine
-            .register_fn("is_pressed", |s: &mut InputState, k: Key| s.pressed_keys.contains(&k.0))
-            .register_get("mouse_x", |s: &mut InputState| s.mouse_pos.0)
-            .register_get("mouse_y", |s: &mut InputState| s.mouse_pos.1)
-            .register_get("mouse_dx", |s: &mut InputState| s.mouse_delta.map(|d| d.0).unwrap_or(0.0))
-            .register_get("mouse_dy", |s: &mut InputState| s.mouse_delta.map(|d| d.1).unwrap_or(0.0))
-            .register_fn("lock_cursor", |s: &mut InputState, value: bool| s.is_cursor_locked = value);
+        runtime.register_function("getMouseX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+            if args.len() != 1 {
+                return Err(rustyscript::Error::Runtime("getMouseX requires 1 argument (inputState)".to_string()));
+            }
+            
+            let input_state: SerializableInputState = serde_json::from_value(args[0].clone())
+                .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?;
+            
+            Ok(serde_json::Value::Number(serde_json::Number::from_f64(input_state.mouse_pos.0).unwrap()))
+        })?;
 
-        let mut kmod = Module::new();
+        runtime.register_function("getMouseY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+            if args.len() != 1 {
+                return Err(rustyscript::Error::Runtime("getMouseY requires 1 argument (inputState)".to_string()));
+            }
+            
+            let input_state: SerializableInputState = serde_json::from_value(args[0].clone())
+                .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?;
+            
+            Ok(serde_json::Value::Number(serde_json::Number::from_f64(input_state.mouse_pos.1).unwrap()))
+        })?;
 
-        macro_rules! add_key {
-            ($name:ident, $kc:expr) => {
-                kmod.set_var(stringify!($name), Key($kc));
-            };
-        }
+        runtime.register_function("getMouseDeltaX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+            if args.len() != 1 {
+                return Err(rustyscript::Error::Runtime("getMouseDeltaX requires 1 argument (inputState)".to_string()));
+            }
+            
+            let input_state: SerializableInputState = serde_json::from_value(args[0].clone())
+                .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?;
+            
+            let delta_x = input_state.mouse_delta.map(|d| d.0).unwrap_or(0.0);
+            Ok(serde_json::Value::Number(serde_json::Number::from_f64(delta_x).unwrap()))
+        })?;
+
+        runtime.register_function("getMouseDeltaY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+            if args.len() != 1 {
+                return Err(rustyscript::Error::Runtime("getMouseDeltaY requires 1 argument (inputState)".to_string()));
+            }
+            
+            let input_state: SerializableInputState = serde_json::from_value(args[0].clone())
+                .map_err(|e| rustyscript::Error::Runtime(format!("Invalid input state: {}", e)))?;
+            
+            let delta_y = input_state.mouse_delta.map(|d| d.1).unwrap_or(0.0);
+            Ok(serde_json::Value::Number(serde_json::Number::from_f64(delta_y).unwrap()))
+        })?;
+
+        runtime.register_function("lockCursor", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+            if args.len() != 1 {
+                return Err(rustyscript::Error::Runtime("lockCursor requires 1 argument (locked)".to_string()));
+            }
+            
+            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
+            // For now, just return success
+            Ok(serde_json::Value::Bool(true))
+        })?;
 
-        add_key!(A, KeyCode::KeyA);
-        add_key!(B, KeyCode::KeyB);
-        add_key!(C, KeyCode::KeyC);
-        add_key!(D, KeyCode::KeyD);
-        add_key!(E, KeyCode::KeyE);
-        add_key!(F, KeyCode::KeyF);
-        add_key!(G, KeyCode::KeyG);
-        add_key!(H, KeyCode::KeyH);
-        add_key!(I, KeyCode::KeyI);
-        add_key!(J, KeyCode::KeyJ);
-        add_key!(K, KeyCode::KeyK);
-        add_key!(L, KeyCode::KeyL);
-        add_key!(M, KeyCode::KeyM);
-        add_key!(N, KeyCode::KeyN);
-        add_key!(O, KeyCode::KeyO);
-        add_key!(P, KeyCode::KeyP);
-        add_key!(Q, KeyCode::KeyQ);
-        add_key!(R, KeyCode::KeyR);
-        add_key!(S, KeyCode::KeyS);
-        add_key!(T, KeyCode::KeyT);
-        add_key!(U, KeyCode::KeyU);
-        add_key!(V, KeyCode::KeyV);
-        add_key!(W, KeyCode::KeyW);
-        add_key!(X, KeyCode::KeyX);
-        add_key!(Y, KeyCode::KeyY);
-        add_key!(Z, KeyCode::KeyZ);
-
-        add_key!(Space, KeyCode::Space);
-        add_key!(ShiftLeft, KeyCode::ShiftLeft);
-        add_key!(ShiftRight, KeyCode::ShiftRight);
-        add_key!(ControlLeft, KeyCode::ControlLeft);
-        add_key!(ControlRight, KeyCode::ControlRight);
-        add_key!(AltLeft, KeyCode::AltLeft);
-        add_key!(AltRight, KeyCode::AltRight);
-        add_key!(Escape, KeyCode::Escape);
-        add_key!(Enter, KeyCode::Enter);
-        add_key!(Tab, KeyCode::Tab);
-        add_key!(Up, KeyCode::ArrowUp);
-        add_key!(Down, KeyCode::ArrowDown);
-        add_key!(Left, KeyCode::ArrowLeft);
-        add_key!(Right, KeyCode::ArrowRight);
-
-        engine.register_static_module("keys", kmod.into());
         log::info!("[Script] Initialised input module");
+        Ok(())
+    }
+}
+
+/// 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),
+        "KeyB" => Some(KeyCode::KeyB),
+        "KeyC" => Some(KeyCode::KeyC),
+        "KeyD" => Some(KeyCode::KeyD),
+        "KeyE" => Some(KeyCode::KeyE),
+        "KeyF" => Some(KeyCode::KeyF),
+        "KeyG" => Some(KeyCode::KeyG),
+        "KeyH" => Some(KeyCode::KeyH),
+        "KeyI" => Some(KeyCode::KeyI),
+        "KeyJ" => Some(KeyCode::KeyJ),
+        "KeyK" => Some(KeyCode::KeyK),
+        "KeyL" => Some(KeyCode::KeyL),
+        "KeyM" => Some(KeyCode::KeyM),
+        "KeyN" => Some(KeyCode::KeyN),
+        "KeyO" => Some(KeyCode::KeyO),
+        "KeyP" => Some(KeyCode::KeyP),
+        "KeyQ" => Some(KeyCode::KeyQ),
+        "KeyR" => Some(KeyCode::KeyR),
+        "KeyS" => Some(KeyCode::KeyS),
+        "KeyT" => Some(KeyCode::KeyT),
+        "KeyU" => Some(KeyCode::KeyU),
+        "KeyV" => Some(KeyCode::KeyV),
+        "KeyW" => Some(KeyCode::KeyW),
+        "KeyX" => Some(KeyCode::KeyX),
+        "KeyY" => Some(KeyCode::KeyY),
+        "KeyZ" => Some(KeyCode::KeyZ),
+        "Space" => Some(KeyCode::Space),
+        "ShiftLeft" => Some(KeyCode::ShiftLeft),
+        "ShiftRight" => Some(KeyCode::ShiftRight),
+        "ControlLeft" => Some(KeyCode::ControlLeft),
+        "ControlRight" => Some(KeyCode::ControlRight),
+        "AltLeft" => Some(KeyCode::AltLeft),
+        "AltRight" => Some(KeyCode::AltRight),
+        "Escape" => Some(KeyCode::Escape),
+        "Enter" => Some(KeyCode::Enter),
+        "Tab" => Some(KeyCode::Tab),
+        "ArrowUp" => Some(KeyCode::ArrowUp),
+        "ArrowDown" => Some(KeyCode::ArrowDown),
+        "ArrowLeft" => Some(KeyCode::ArrowLeft),
+        "ArrowRight" => Some(KeyCode::ArrowRight),
+        "Digit0" => Some(KeyCode::Digit0),
+        "Digit1" => Some(KeyCode::Digit1),
+        "Digit2" => Some(KeyCode::Digit2),
+        "Digit3" => Some(KeyCode::Digit3),
+        "Digit4" => Some(KeyCode::Digit4),
+        "Digit5" => Some(KeyCode::Digit5),
+        "Digit6" => Some(KeyCode::Digit6),
+        "Digit7" => Some(KeyCode::Digit7),
+        "Digit8" => Some(KeyCode::Digit8),
+        "Digit9" => Some(KeyCode::Digit9),
+        "F1" => Some(KeyCode::F1),
+        "F2" => Some(KeyCode::F2),
+        "F3" => Some(KeyCode::F3),
+        "F4" => Some(KeyCode::F4),
+        "F5" => Some(KeyCode::F5),
+        "F6" => Some(KeyCode::F6),
+        "F7" => Some(KeyCode::F7),
+        "F8" => Some(KeyCode::F8),
+        "F9" => Some(KeyCode::F9),
+        "F10" => Some(KeyCode::F10),
+        "F11" => Some(KeyCode::F11),
+        "F12" => Some(KeyCode::F12),
+        _ => None,
+    }
+}
+
+/// 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()),
+        KeyCode::KeyB => Some("KeyB".to_string()),
+        KeyCode::KeyC => Some("KeyC".to_string()),
+        KeyCode::KeyD => Some("KeyD".to_string()),
+        KeyCode::KeyE => Some("KeyE".to_string()),
+        KeyCode::KeyF => Some("KeyF".to_string()),
+        KeyCode::KeyG => Some("KeyG".to_string()),
+        KeyCode::KeyH => Some("KeyH".to_string()),
+        KeyCode::KeyI => Some("KeyI".to_string()),
+        KeyCode::KeyJ => Some("KeyJ".to_string()),
+        KeyCode::KeyK => Some("KeyK".to_string()),
+        KeyCode::KeyL => Some("KeyL".to_string()),
+        KeyCode::KeyM => Some("KeyM".to_string()),
+        KeyCode::KeyN => Some("KeyN".to_string()),
+        KeyCode::KeyO => Some("KeyO".to_string()),
+        KeyCode::KeyP => Some("KeyP".to_string()),
+        KeyCode::KeyQ => Some("KeyQ".to_string()),
+        KeyCode::KeyR => Some("KeyR".to_string()),
+        KeyCode::KeyS => Some("KeyS".to_string()),
+        KeyCode::KeyT => Some("KeyT".to_string()),
+        KeyCode::KeyU => Some("KeyU".to_string()),
+        KeyCode::KeyV => Some("KeyV".to_string()),
+        KeyCode::KeyW => Some("KeyW".to_string()),
+        KeyCode::KeyX => Some("KeyX".to_string()),
+        KeyCode::KeyY => Some("KeyY".to_string()),
+        KeyCode::KeyZ => Some("KeyZ".to_string()),
+        KeyCode::Space => Some("Space".to_string()),
+        KeyCode::ShiftLeft => Some("ShiftLeft".to_string()),
+        KeyCode::ShiftRight => Some("ShiftRight".to_string()),
+        KeyCode::ControlLeft => Some("ControlLeft".to_string()),
+        KeyCode::ControlRight => Some("ControlRight".to_string()),
+        KeyCode::AltLeft => Some("AltLeft".to_string()),
+        KeyCode::AltRight => Some("AltRight".to_string()),
+        KeyCode::Escape => Some("Escape".to_string()),
+        KeyCode::Enter => Some("Enter".to_string()),
+        KeyCode::Tab => Some("Tab".to_string()),
+        KeyCode::ArrowUp => Some("ArrowUp".to_string()),
+        KeyCode::ArrowDown => Some("ArrowDown".to_string()),
+        KeyCode::ArrowLeft => Some("ArrowLeft".to_string()),
+        KeyCode::ArrowRight => Some("ArrowRight".to_string()),
+        KeyCode::Digit0 => Some("Digit0".to_string()),
+        KeyCode::Digit1 => Some("Digit1".to_string()),
+        KeyCode::Digit2 => Some("Digit2".to_string()),
+        KeyCode::Digit3 => Some("Digit3".to_string()),
+        KeyCode::Digit4 => Some("Digit4".to_string()),
+        KeyCode::Digit5 => Some("Digit5".to_string()),
+        KeyCode::Digit6 => Some("Digit6".to_string()),
+        KeyCode::Digit7 => Some("Digit7".to_string()),
+        KeyCode::Digit8 => Some("Digit8".to_string()),
+        KeyCode::Digit9 => Some("Digit9".to_string()),
+        KeyCode::F1 => Some("F1".to_string()),
+        KeyCode::F2 => Some("F2".to_string()),
+        KeyCode::F3 => Some("F3".to_string()),
+        KeyCode::F4 => Some("F4".to_string()),
+        KeyCode::F5 => Some("F5".to_string()),
+        KeyCode::F6 => Some("F6".to_string()),
+        KeyCode::F7 => Some("F7".to_string()),
+        KeyCode::F8 => Some("F8".to_string()),
+        KeyCode::F9 => Some("F9".to_string()),
+        KeyCode::F10 => Some("F10".to_string()),
+        KeyCode::F11 => Some("F11".to_string()),
+        KeyCode::F12 => Some("F12".to_string()),
+        _ => None,
     }
 }
 
-#[derive(rhai::CustomType, Clone, Copy)]
+#[derive(Clone, Copy)]
 pub struct Key(pub KeyCode);
\ No newline at end of file
diff --git a/eucalyptus/src/scripting/math.rs b/eucalyptus/src/scripting/math.rs
index d185836..83f5731 100644
--- a/eucalyptus/src/scripting/math.rs
+++ b/eucalyptus/src/scripting/math.rs
@@ -1,61 +1,161 @@
-use rhai::*;
-use rhai::plugin::*;
-
-#[rhai::export_module]
-pub mod math_functions {
-    // Basic math
-    pub fn abs(x: f64) -> f64 { x.abs() }
-    pub fn sqrt(x: f64) -> f64 { x.sqrt() }
-    pub fn pow(x: f64, y: f64) -> f64 { x.powf(y) }
-    pub fn min(x: f64, y: f64) -> f64 { x.min(y) }
-    pub fn max(x: f64, y: f64) -> f64 { x.max(y) }
-    pub fn clamp(x: f64, min: f64, max: f64) -> f64 { x.clamp(min, max) }
-
-    // Trig functions
-    pub fn sin(x: f64) -> f64 { x.sin() }
-    pub fn cos(x: f64) -> f64 { x.cos() }
-    pub fn tan(x: f64) -> f64 { x.tan() }
-    pub fn asin(x: f64) -> f64 { x.asin() }
-    pub fn acos(x: f64) -> f64 { x.acos() }
-    pub fn atan(x: f64) -> f64 { x.atan() }
-    pub fn atan2(y: f64, x: f64) -> f64 { y.atan2(x) }
-
-    // Hyperbolic functions
-    pub fn sinh(x: f64) -> f64 { x.sinh() }
-    pub fn cosh(x: f64) -> f64 { x.cosh() }
-    pub fn tanh(x: f64) -> f64 { x.tanh() }
-
-    // Logarithmic and exponential
-    pub fn exp(x: f64) -> f64 { x.exp() }
-    pub fn ln(x: f64) -> f64 { x.ln() }
-    pub fn log10(x: f64) -> f64 { x.log10() }
-    pub fn log2(x: f64) -> f64 { x.log2() }
-
-    // Rounding functions
-    pub fn floor(x: f64) -> f64 { x.floor() }
-    pub fn ceil(x: f64) -> f64 { x.ceil() }
-    pub fn round(x: f64) -> f64 { x.round() }
-    pub fn trunc(x: f64) -> f64 { x.trunc() }
-    pub fn fract(x: f64) -> f64 { x.fract() }
-
-    // Conversion
-    pub fn to_radians(degrees: f64) -> f64 { degrees.to_radians() }
-    pub fn to_degrees(radians: f64) -> f64 { radians.to_degrees() }
-
-    // Utility functions
-    pub fn lerp(a: f64, b: f64, t: f64) -> f64 { a + (b - a) * t }
-    pub fn smoothstep(edge0: f64, edge1: f64, x: f64) -> f64 {
-        let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
-        t * t * (3.0 - 2.0 * t)
-    }
-
-    // Consts
-    pub const PI: f64 = std::f64::consts::PI;
-    pub const E: f64 = std::f64::consts::E;
-    pub const TAU: f64 = std::f64::consts::TAU;
-}
-
-pub fn register_math_functions(engine: &mut Engine) {
-    engine.register_static_module("math", exported_module!(math_functions).into());
+use dropbear_engine::entity::Transform;
+use glam::{DQuat, DVec3};
+use rustyscript::{serde_json, Runtime};
+
+pub fn register_math_functions(runtime: &mut Runtime) -> anyhow::Result<()> {
+    
+    runtime.register_function("createTransform", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        let transform = Transform::new();
+        serde_json::to_value(transform)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e)))
+    })?;
+
+    runtime.register_function("transformTranslate", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("transformTranslate requires 2 arguments".to_string()));
+        }
+        
+        let mut transform: Transform = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?;
+        
+        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()));
+            }
+            DVec3::new(
+                array[0].as_f64().unwrap_or(0.0),
+                array[1].as_f64().unwrap_or(0.0),
+                array[2].as_f64().unwrap_or(0.0),
+            )
+        } else {
+            return Err(rustyscript::Error::Runtime("Translation must be an array".to_string()));
+        };
+        
+        transform.position += translation;
+        serde_json::to_value(transform)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e)))
+    })?;
+
+    runtime.register_function("transformRotateX", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("transformRotateX requires 2 arguments".to_string()));
+        }
+        
+        let mut transform: Transform = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?;
+        
+        let angle = args[1].as_f64().unwrap_or(0.0);
+        let rotation = DQuat::from_rotation_x(angle);
+        transform.rotation = rotation * transform.rotation;
+        
+        serde_json::to_value(transform)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e)))
+    })?;
+
+    runtime.register_function("transformRotateY", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("transformRotateY requires 2 arguments".to_string()));
+        }
+        
+        let mut transform: Transform = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?;
+        
+        let angle = args[1].as_f64().unwrap_or(0.0);
+        let rotation = DQuat::from_rotation_y(angle);
+        transform.rotation = rotation * transform.rotation;
+        
+        serde_json::to_value(transform)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e)))
+    })?;
+
+    runtime.register_function("transformRotateZ", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("transformRotateZ requires 2 arguments".to_string()));
+        }
+        
+        let mut transform: Transform = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?;
+        
+        let angle = args[1].as_f64().unwrap_or(0.0);
+        let rotation = DQuat::from_rotation_z(angle);
+        transform.rotation = rotation * transform.rotation;
+        
+        serde_json::to_value(transform)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Transform: {}", e)))
+    })?;
+
+    runtime.register_function("transformScale", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 2 {
+            return Err(rustyscript::Error::Runtime("transformScale requires 2 arguments".to_string()));
+        }
+        
+        let mut transform: Transform = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?;
+        
+        let scale = if let Some(num) = args[1].as_f64() {
+            DVec3::splat(num)
+        } else if let Some(array) = args[1].as_array() {
+            if array.len() != 3 {
+                return Err(rustyscript::Error::Runtime("Scale array must have 3 elements".to_string()));
+            }
+            DVec3::new(
+                array[0].as_f64().unwrap_or(1.0),
+                array[1].as_f64().unwrap_or(1.0),
+                array[2].as_f64().unwrap_or(1.0),
+            )
+        } else {
+            return Err(rustyscript::Error::Runtime("Scale must be a number or array".to_string()));
+        };
+        
+        transform.scale *= scale;
+        serde_json::to_value(transform)
+            .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()));
+        }
+        
+        let transform: Transform = serde_json::from_value(args[0].clone())
+            .map_err(|e| rustyscript::Error::Runtime(format!("Invalid transform: {}", e)))?;
+        
+        let matrix = transform.matrix();
+        serde_json::to_value(matrix)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize matrix: {}", e)))
+    })?;
+
+    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);
+        let z = args.get(2).and_then(|v| v.as_f64()).unwrap_or(0.0);
+        
+        let vec3 = DVec3::new(x, y, z);
+        serde_json::to_value(vec3)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Vec3: {}", e)))
+    })?;
+
+    runtime.register_function("createQuatIdentity", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        let quat = DQuat::IDENTITY;
+        serde_json::to_value(quat)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Quaternion: {}", e)))
+    })?;
+
+    runtime.register_function("createQuatFromEuler", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+        if args.len() != 3 {
+            return Err(rustyscript::Error::Runtime("createQuatFromEuler requires 3 arguments".to_string()));
+        }
+        
+        let x = args[0].as_f64().unwrap_or(0.0);
+        let y = args[1].as_f64().unwrap_or(0.0);
+        let z = args[2].as_f64().unwrap_or(0.0);
+        
+        let quat = DQuat::from_euler(glam::EulerRot::XYZ, x, y, z);
+        serde_json::to_value(quat)
+            .map_err(|e| rustyscript::Error::Runtime(format!("Failed to serialize Quaternion: {}", e)))
+    })?;
+
     log::info!("[Script] Initialised math module");
+    Ok(())
 }
\ No newline at end of file
diff --git a/eucalyptus/src/scripting/mod.rs b/eucalyptus/src/scripting/mod.rs
index 15cae7e..0ee10c9 100644
--- a/eucalyptus/src/scripting/mod.rs
+++ b/eucalyptus/src/scripting/mod.rs
@@ -3,15 +3,14 @@ pub mod math;
 pub mod input;
 
 use dropbear_engine::entity::{AdoptedEntity, Transform};
-use glam::{DQuat, DVec3};
 use hecs::World;
-use rhai::*;
+use rustyscript::{serde_json, Module, ModuleHandle, Runtime, RuntimeOptions};
 use std::path::PathBuf;
 use std::{collections::HashMap, fs};
 
 use crate::states::{EntityNode, ModelProperties, ScriptComponent, PROJECT, SOURCE};
 
-pub const TEMPLATE_SCRIPT: &'static str = include_str!("../template.rhai");
+pub const TEMPLATE_SCRIPT: &'static str = include_str!("../template.ts");
 
 pub enum ScriptAction {
     AttachScript {
@@ -157,81 +156,47 @@ pub fn attach_script_to_entity(
 }
 
 pub struct ScriptManager {
-    pub engine: rhai::Engine,
-    compiled_scripts: HashMap<String, AST>,
-    script_scopes: HashMap<hecs::Entity, Scope<'static>>,
+    pub runtime: Runtime,
+    compiled_scripts: HashMap<String, ModuleHandle>,
+    entity_script_data: HashMap<hecs::Entity, serde_json::Value>,
 }
 
 impl ScriptManager {
-    pub fn new() -> Self {
-        let mut engine = rhai::Engine::new();
-
-        // REGISTER FUNCTIONS HERE
-        math::register_math_functions(&mut engine);
-        input::InputState::register_input_modules(&mut engine);
-        entity::register_model_props_module(&mut engine);
-        
-        engine.register_type_with_name::<Transform>("Transform");
-        engine.register_type_with_name::<DQuat>("Quaternion");
-
-        // transform
-        engine.register_fn("new_transform", Transform::new);
-        engine.register_get_set(
-            "position",
-            |t: &mut Transform| t.position,
-            |t: &mut Transform, pos: DVec3| t.position = pos,
-        );
-        engine.register_get_set(
-            "rotation",
-            |t: &mut Transform| t.rotation,
-            |t: &mut Transform, rot: DQuat| t.rotation = rot,
-        );
-        engine.register_get_set(
-            "scale",
-            |t: &mut Transform| t.scale,
-            |t: &mut Transform, scale: DVec3| t.scale = scale,
-        );
-
-        // vector methods
-        engine.register_type_with_name::<DVec3>("Vector3");
-        engine.register_fn("vec3", |x: f64, y: f64, z: f64| DVec3::new(x, y, z));
-        engine.register_get_set("x", |v: &mut DVec3| v.x, |v: &mut DVec3, x: f64| v.x = x);
-        engine.register_get_set("y", |v: &mut DVec3| v.y, |v: &mut DVec3, y: f64| v.y = y);
-        engine.register_get_set("z", |v: &mut DVec3| v.z, |v: &mut DVec3, z: f64| v.z = z);
-
-        // utils
-        engine.register_fn("log", |msg: &str| {
+    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)?;
+        entity::register_model_props_module(&mut runtime)?;
+
+        // Register utility functions
+        runtime.register_function("log", |args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+            let msg = args.get(0)
+                .and_then(|v| v.as_str())
+                .unwrap_or("undefined");
             println!("[Script] {}", msg);
-        });
-        engine.register_fn("time", || {
-            std::time::SystemTime::now()
+            Ok(serde_json::Value::Null)
+        })?;
+
+        runtime.register_function("time", |_args: &[serde_json::Value]| -> Result<serde_json::Value, rustyscript::Error> {
+            let time = std::time::SystemTime::now()
                 .duration_since(std::time::UNIX_EPOCH)
                 .unwrap()
-                .as_secs_f64()
-        });
-
-        // operators
-        engine.register_fn("<", |a: f64, b: f32| a < b as f64);
-        engine.register_fn("<=", |a: f64, b: f32| a <= b as f64);
-        engine.register_fn(">", |a: f64, b: f32| a > b as f64);
-        engine.register_fn(">=", |a: f64, b: f32| a >= b as f64);
-        engine.register_fn("==", |a: f64, b: f32| a == b as f64);
-        engine.register_fn("!=", |a: f64, b: f32| a != b as f64);
-
-        engine.register_fn("<", |a: f32, b: f64| (a as f64) < b);
-        engine.register_fn("<=", |a: f32, b: f64| (a as f64) <= b);
-        engine.register_fn(">", |a: f32, b: f64| (a as f64) > b);
-        engine.register_fn(">=", |a: f32, b: f64| (a as f64) >= b);
-        engine.register_fn("==", |a: f32, b: f64| (a as f64) == b);
-        engine.register_fn("!=", |a: f32, b: f64| (a as f64) != b);
-
-        // input
-
-        Self {
-            engine,
+                .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(),
-            script_scopes: HashMap::new(),
-        }
+            entity_script_data: HashMap::new(),
+        })
     }
 
     pub fn init_entity_script(
@@ -242,8 +207,8 @@ impl ScriptManager {
         input_state: &input::InputState,
     ) -> anyhow::Result<()> {
         if let Ok(mut q) = world.query_one::<&AdoptedEntity>(entity_id) {
-                if let Some(adopted) = q.get() {
-                    log_once::debug_once!(
+            if let Some(adopted) = q.get() {
+                log_once::debug_once!(
                     "init_entity_script: '{}' for entity {:?} -> label='{}' path='{}'",
                     script_name,
                     entity_id,
@@ -255,42 +220,57 @@ impl ScriptManager {
             log_once::debug_once!("init_entity_script: '{}' for entity {:?}", script_name, entity_id);
         }
 
-        if let Some(ast) = self.compiled_scripts.get(script_name) {
-            let mut scope = Scope::new();
+        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();
 
-            if let Ok(transform) = world.query_one_mut::<&mut Transform>(entity_id) {
-                scope.push("transform", *transform);
+            // 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)?);
+                }
             }
 
-            if let Ok(properties) = world.query_one_mut::<&mut ModelProperties>(entity_id) {
-                scope.push("entity", properties.clone());
+            // 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();
-                // default_props.set_property(String::from("speed"), PropertyValue::Float(1.0));
-                scope.push("entity", default_props);
+                script_data.insert("entity".to_string(), serde_json::to_value(&default_props)?);
             }
 
-            scope.push("input", input_state.clone());
-
-            if let Ok(_) = self.engine.call_fn::<()>(&mut scope, ast, "init", ()) {
-                log::debug!("Called init for entity {:?}", entity_id);
-
-                if let Some(properties_from_scope) = scope.get_value::<ModelProperties>("entity") {
-                    if let Ok(properties) = world.query_one_mut::<&mut ModelProperties>(entity_id) {
-                        *properties = properties_from_scope;
+            // input state
+            let serializable_input = input::SerializableInputState::from(input_state);
+            script_data.insert("input".to_string(), serde_json::to_value(&serializable_input)?);
+
+            // 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 {
-                        let _ = world.insert_one(entity_id, properties_from_scope);
-                    }
+                        // 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);
                 }
-
-                if let Some(transform_from_scope) = scope.get_value::<Transform>("transform") {
-                    if let Ok(transform) = world.query_one_mut::<&mut Transform>(entity_id) {
-                        *transform = transform_from_scope;
-                    }
+                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);
                 }
             }
 
-            self.script_scopes.insert(entity_id, scope);
             Ok(())
         } else {
             Err(anyhow::anyhow!("Script '{}' not found", script_name))
@@ -305,40 +285,68 @@ impl ScriptManager {
         input_state: &input::InputState,
         dt: f32,
     ) -> anyhow::Result<()> {
-        if let Some(ast) = self.compiled_scripts.get(script_name) {
-            if let Some(scope) = self.script_scopes.get_mut(&entity_id) {
-                if let Ok(transform) = world.query_one_mut::<&mut Transform>(entity_id) {
-                    scope.set_value("transform", *transform);
+        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();
+
+            // 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)?);
                 }
+            }
 
-                if let Ok(mut properties_query) = world.query_one::<&ModelProperties>(entity_id) {
-                    if let Some(properties) = properties_query.get() {
-                        scope.set_value("entity", properties.clone());
-                    }
+            // 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)?);
+            }
 
-                scope.set_value("input", input_state.clone());
-
-                match self.engine.call_fn::<()>(scope, ast, "update", (dt,)) {
-                    Ok(_) => {
-                        if let Some(transform_from_scope) = scope.get_value::<Transform>("transform") {
-                            if let Ok(transform) = world.query_one_mut::<&mut Transform>(entity_id) {
-                                *transform = transform_from_scope;
+            // input state
+            let serializable_input = input::SerializableInputState::from(input_state);
+            script_data.insert("input".to_string(), serde_json::to_value(&serializable_input)?);
+
+            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), "onUpdate", &vec![script_data_value.clone(), dt_value]) {
+                Ok(result) => {
+                    log::trace!("Called update for entity {:?}", entity_id);
+                    
+                    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(properties_from_scope) = scope.get_value::<ModelProperties>("entity") {
-                            if let Ok(properties) = world.query_one_mut::<&mut ModelProperties>(entity_id) {
-                                *properties = properties_from_scope;
+                        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);
-                    }
                 }
-            } else {
-                log_once::error_once!("Unable to get scope of entity {:?}", entity_id);
+                Err(e) => {
+                    log_once::error_once!("Script execution error for entity {:?}: {}", entity_id, e);
+                }
             }
         } else {
             log_once::error_once!("Unable to fetch compiled scripts for entity {:?}. Script Name: {}", entity_id, script_name);
@@ -346,50 +354,90 @@ impl ScriptManager {
         Ok(())
     }
 
-    pub fn load_script_from_source(&mut self, script_name: &String, script_content: &String) -> anyhow::Result<String> {
-        let ast = self.engine.compile(&script_content).map_err(|e| {
-            log::error!("Compiling AST for script [{}] returned an error: {}", script_name, e);
-            e
-        })?;
-        self.compiled_scripts.insert(script_name.clone(), ast);
+    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. 
+        }
 
-        log::info!("Loaded script: {}", script_name);
-        Ok(script_name.to_string())
+        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);
+        
+        match self.runtime.load_module(&module) {
+            Ok(module) => {
+                self.compiled_scripts.insert(script_name.clone(), module);
+                log::info!("Loaded script: {}", script_name);
+                Ok(script_name.to_string())
+            }
+            Err(e) => {
+                log::error!("Compiling module for script [{}] returned an error: {}", script_name, e);
+                Err(e.into())
+            }
+        }
     }
 
     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);
 
-        let ast = self.engine.compile(&script_content).map_err(|e| {
-            log::error!("Compiling AST for script path [{}] returned an error: {}", script_path.display(), e);
-            e
-        })?;
-        self.compiled_scripts.insert(script_name.clone(), ast);
-
-        log::info!("Loaded script: {}", script_name);
-        Ok(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);
+                log::info!("Loaded script: {}", script_name);
+                Ok(script_name)
+            }
+            Err(e) => {
+                log::error!("Compiling module for script path [{}] returned an error: {}", script_path.display(), e);
+                Err(e.into())
+            }
+        }
     }
 
     pub fn remove_entity_script(&mut self, entity_id: hecs::Entity) {
-        self.script_scopes.remove(&entity_id);
+        self.entity_script_data.remove(&entity_id);
     }
-
-    // maybe useful later???
-    // pub fn reload_script(
-    //     &mut self,
-    //     script_name: &str,
-    //     script_path: &PathBuf,
-    // ) -> anyhow::Result<()> {
-    //     let script_content = fs::read_to_string(script_path)?;
-    //     let ast = self.engine.compile(&script_content)?;
-    //     self.compiled_scripts.insert(script_name.to_string(), ast);
-
-    //     log::info!("Reloaded script: {}", script_name);
-    //     Ok(())
-    // }
 }
\ No newline at end of file
diff --git a/eucalyptus/src/states.rs b/eucalyptus/src/states.rs
index dc52497..431085f 100644
--- a/eucalyptus/src/states.rs
+++ b/eucalyptus/src/states.rs
@@ -31,7 +31,6 @@ use serde::{Deserialize, Serialize};
 use dropbear_engine::model::Model;
 use dropbear_engine::starter::plane::PlaneBuilder;
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
-use crate::build::build;
 use crate::camera::CameraType;
 #[cfg(feature = "editor")]
 use crate::editor::EditorTab;
@@ -615,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),
@@ -740,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 {
@@ -941,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
+                                }
                             }
                         }
                     });
diff --git a/eucalyptus/src/template.rhai b/eucalyptus/src/template.rhai
deleted file mode 100644
index 3b9acee..0000000
--- a/eucalyptus/src/template.rhai
+++ /dev/null
@@ -1,20 +0,0 @@
-// Docs:
-// https://github.com/4tkbytes/dropbear/blob/main/docs/README.md
-
-fn init() {
-    print("Script initialised!");
-    // Access the entity's transform
-    // transform.position.x = 0.0;
-}
-
-fn update(dt) {
-    // Example: rotate the entity
-    // let rotation_speed = 1.0;
-    // transform.position.x = sin(time()) * 2.0;
-    // transform.position.y = cos(time()) * 2.0;
-    
-    // Example: log every second
-    // if (time() % 1.0 < dt) {
-    //     print("Entity position: " + transform.position.x + ", " + transform.position.y + ", " + transform.position.z);
-    // }
-}
\ No newline at end of file
diff --git a/eucalyptus/src/template.ts b/eucalyptus/src/template.ts
new file mode 100644
index 0000000..65f8308
--- /dev/null
+++ b/eucalyptus/src/template.ts
@@ -0,0 +1,25 @@
+// dropbear-engine script template for eucalyptus
+import * as dropbear from "./dropbear.ts";
+
+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(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();
+}
diff --git a/redback-runtime b/redback-runtime
index 922f1bd..36de5a6 160000
--- a/redback-runtime
+++ b/redback-runtime
@@ -1 +1 @@
-Subproject commit 922f1bd096aefbce8bac8c6a7c699c144c3a4307
+Subproject commit 36de5a65d6cb8c77d30d3d3170ac5794d94d7e01