nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use super::{ComponentInspector, InspectorContext};
use crate::editor::code_editor::CodeEditor;
use crate::prelude::*;

struct ScriptPreset {
    name: &'static str,
    description: &'static str,
    source: &'static str,
}

const SCRIPT_PRESETS: &[ScriptPreset] = &[
    ScriptPreset {
        name: "Spin (Y-Axis)",
        description: "Rotate continuously around the Y axis",
        source: "rot_y = rot_y + 1.0 * dt;",
    },
    ScriptPreset {
        name: "Spin (All Axes)",
        description: "Rotate around all axes at different speeds",
        source: "rot_x = rot_x + 0.5 * dt;\nrot_y = rot_y + 1.0 * dt;\nrot_z = rot_z + 0.3 * dt;",
    },
    ScriptPreset {
        name: "Bob Up/Down",
        description: "Move up and down in a sine wave pattern",
        source: "let amplitude = 1.0;\nlet speed = 2.0;\npos_y = (time * speed).sin() * amplitude;",
    },
    ScriptPreset {
        name: "Orbit (XZ Circle)",
        description: "Move in a circle on the XZ plane",
        source: "let speed = 1.0;\nlet radius = 3.0;\npos_x = (time * speed).cos() * radius;\npos_z = (time * speed).sin() * radius;",
    },
    ScriptPreset {
        name: "Follow Mouse",
        description: "Smoothly follow the mouse cursor position",
        source: "let target_x = (mouse_x - 640.0) * 0.01;\nlet target_y = (360.0 - mouse_y) * 0.01;\npos_x = pos_x + (target_x - pos_x) * 5.0 * dt;\npos_y = pos_y + (target_y - pos_y) * 5.0 * dt;",
    },
    ScriptPreset {
        name: "WASD Movement",
        description: "Move with WASD keys",
        source: "let speed = 5.0;\nif pressed_keys.contains(\"W\") { pos_z = pos_z - speed * dt; }\nif pressed_keys.contains(\"S\") { pos_z = pos_z + speed * dt; }\nif pressed_keys.contains(\"A\") { pos_x = pos_x - speed * dt; }\nif pressed_keys.contains(\"D\") { pos_x = pos_x + speed * dt; }",
    },
    ScriptPreset {
        name: "Spawn on Space",
        description: "Spawn a cube above this entity when Space is pressed",
        source: "if just_pressed_keys.contains(\"SPACE\") {\n    do_spawn_cube = true;\n    spawn_cube_x = pos_x;\n    spawn_cube_y = pos_y + 2.0;\n    spawn_cube_z = pos_z;\n    log(\"Spawned!\");\n}",
    },
    ScriptPreset {
        name: "Pulse Scale",
        description: "Pulse the scale up and down",
        source: "let speed = 3.0;\nlet pulse = 1.0 + (time * speed).sin() * 0.3;\nscale_x = pulse;\nscale_y = pulse;\nscale_z = pulse;",
    },
    ScriptPreset {
        name: "Hover",
        description: "Gently hover up and down",
        source: "let base_y = 0.0;\nlet amplitude = 0.5;\nlet speed = 2.0;\npos_y = base_y + (time * speed).sin() * amplitude;",
    },
    ScriptPreset {
        name: "Look at Mouse",
        description: "Rotate to face the mouse cursor direction",
        source: "let dx = mouse_x - 640.0;\nrot_y = rot_y + dx * 0.001 * dt;",
    },
];

#[derive(Default)]
pub struct ScriptInspector {
    code_editor: CodeEditor,
    pending_preset: Option<usize>,
}

impl ComponentInspector for ScriptInspector {
    fn name(&self) -> &str {
        "Script"
    }

    fn has_component(&self, world: &World, entity: Entity) -> bool {
        world.entity_has_script(entity)
    }

    fn add_component(&self, world: &mut World, entity: Entity) {
        world.add_components(entity, crate::ecs::world::SCRIPT);
        world.set_script(
            entity,
            Script {
                source: ScriptSource::Embedded {
                    source: String::new(),
                },
                enabled: false,
            },
        );
    }

    fn remove_component(&self, world: &mut World, entity: Entity) {
        world.remove_script(entity);
    }

    fn ui(
        &mut self,
        world: &mut World,
        entity: Entity,
        ui: &mut egui::Ui,
        _context: &mut InspectorContext,
    ) {
        let Some(mut script) = world.get_script(entity).cloned() else {
            return;
        };

        let is_file = matches!(script.source, ScriptSource::File { .. });

        ui.horizontal(|ui| {
            ui.label("Source Type");
            if ui.selectable_label(is_file, "File").clicked() && !is_file {
                script.source = ScriptSource::File {
                    path: String::new(),
                };
            }
            if ui.selectable_label(!is_file, "Embedded").clicked() && is_file {
                script.source = ScriptSource::Embedded {
                    source: String::new(),
                };
            }
        });

        ui.separator();

        if !is_file {
            ui.horizontal(|ui| {
                ui.label("Presets");
                ui.menu_button("Select preset...", |ui| {
                    for (index, preset) in SCRIPT_PRESETS.iter().enumerate() {
                        if ui
                            .button(preset.name)
                            .on_hover_text(preset.description)
                            .clicked()
                        {
                            self.pending_preset = Some(index);
                            ui.close();
                        }
                    }
                });
            });

            if let Some(index) = self.pending_preset.take()
                && let Some(preset) = SCRIPT_PRESETS.get(index)
            {
                script.source = ScriptSource::Embedded {
                    source: preset.source.to_string(),
                };
            }

            ui.separator();
        }

        match &mut script.source {
            ScriptSource::File { path } => {
                ui.horizontal(|ui| {
                    ui.label("Path");
                    ui.text_edit_singleline(path);
                });
            }
            ScriptSource::Embedded { source } => {
                ui.label("Script");
                egui::ScrollArea::vertical()
                    .max_height(200.0)
                    .show(ui, |ui| {
                        self.code_editor.ui(ui, source);
                    });
            }
        }

        ui.separator();

        ui.collapsing("API Reference", |ui| {
            ui.label("Transform (read/write):");
            ui.monospace("  pos_x, pos_y, pos_z");
            ui.monospace("  rot_x, rot_y, rot_z (euler radians)");
            ui.monospace("  scale_x, scale_y, scale_z");
            ui.add_space(4.0);
            ui.label("Time & Input (read-only):");
            ui.monospace("  time (accumulated seconds)");
            ui.monospace("  dt, delta_time (frame delta)");
            ui.monospace("  mouse_x, mouse_y");
            ui.monospace("  pressed_keys, just_pressed_keys");
            ui.monospace("  entity_id");
            ui.add_space(4.0);
            ui.label("Spawning:");
            ui.monospace("  do_spawn_cube = true;");
            ui.monospace("  spawn_cube_x/y/z = position;");
            ui.monospace("  do_spawn_sphere = true;");
            ui.monospace("  spawn_sphere_x/y/z = position;");
            ui.add_space(4.0);
            ui.label("Shared State:");
            ui.monospace("  state[\"key\"] = value;");
            ui.monospace("  let val = state[\"key\"];");
            ui.add_space(4.0);
            ui.label("Other:");
            ui.monospace("  do_despawn = true;");
            ui.monospace("  log(\"message\");");
        });

        world.set_script(entity, script);
    }
}