euv-engine 0.28.13

A high-performance 2D game engine built on the euv framework, featuring ECS, fixed-timestep game loop, canvas rendering, physics, collision detection, sprite animation, and audio.
Documentation
use super::*;
fn listener_count() -> u32 {
    let read: js_sys::Function = js_sys::Function::new_no_args(
        "return typeof globalThis.__euvListenerCount === 'number' ? globalThis.__euvListenerCount : -1;",
    );
    read.call0(&JsValue::NULL)
        .ok()
        .and_then(|value: JsValue| value.as_f64())
        .map(|count: f64| count as u32)
        .unwrap_or(0)
}

fn pump_animation_frame() -> bool {
    let run: js_sys::Function = js_sys::Function::new_no_args(
        "if (typeof globalThis.__euvRaf !== \"function\") { return false; } globalThis.__euvRaf(); return true;",
    );
    let fired: bool = run
        .call0(&JsValue::NULL)
        .ok()
        .and_then(|value: JsValue| value.as_bool())
        .unwrap_or(false);
    fired
}

fn dom_teardown() {
    let strip: js_sys::Function = js_sys::Function::new_no_args(
        r#"
        for (const key of ['document','navigator','performance','requestAnimationFrame',
                           'cancelAnimationFrame','addEventListener','removeEventListener',
                           'localStorage','window','Window','Document','Element','Node',
                           'Navigator','Performance','HTMLCanvasElement','HTMLImageElement',
                           'CanvasRenderingContext2D','WebGL2RenderingContext','Gamepad',
                           'GamepadList','Storage','__euvDomInstalled','__euvDomLog',
                           '__euvContext','__euvCanvas2d','__euvWebgl2','__euvCanvasElement','__euvNode',
                           '__euvRaf','__euvListenerCount']) {
            try { delete globalThis[key]; } catch (e) {}
        }
        Object.setPrototypeOf(globalThis, Object.prototype);
        "#,
    );
    let _: Result<JsValue, JsValue> = strip.call0(&JsValue::NULL);
}
fn dom_source() -> js_sys::Function {
    js_sys::Function::new_no_args(
        r#"
        if (globalThis.__euvDomInstalled) { return; }
        globalThis.__euvDomInstalled = true;

        const seen = [];
        function node(path, proto) {
            const target = function () {
                globalThis.__euvDomLog.push([path, Array.from(arguments)]);
                return node(path + '()');
            };
            const proxy = new Proxy(target, {
                getPrototypeOf() { return proto || target; },
                get(fn, key) {
                    if (key === 'then') { return undefined; }
                    if (typeof key === 'symbol') { return undefined; }
                    if (key === 'call' || key === 'apply' || key === 'bind') { return fn[key]; }
                    if (key === 'constructor') { return undefined; }
                    if (Object.prototype.hasOwnProperty.call(fn, key)) { return fn[key]; }
                    return node(path + '.' + String(key));
                },
                set(fn, key, value) {
                    globalThis.__euvDomLog.push([path + '=' + String(key), [value]]);
                    fn[key] = value;
                    return true;
                },
                apply(fn, self, args) {
                    globalThis.__euvDomLog.push([path, Array.from(args)]);
                    return node(path + '()');
                },
                has() { return true; }
            });
            seen.push(proxy);
            return proxy;
        }
        globalThis.__euvDomLog = [];

        function Ctor() {}
        for (const name of ['Window', 'Document', 'Element', 'Node', 'Navigator',
                            'Performance', 'HTMLCanvasElement', 'HTMLImageElement',
                            'CanvasRenderingContext2D', 'WebGL2RenderingContext',
                            'Gamepad', 'GamepadList', 'Storage']) {
            globalThis[name] = Ctor;
        }

        const canvasProto = Object.create(globalThis.HTMLCanvasElement.prototype);
        const canvas = Object.create(canvasProto);
        canvas.__tag = 'canvas';
        canvas.width = 800;
        canvas.height = 600;
        canvas.getContext = function (kind) {
            globalThis.__euvDomLog.push(['canvas.getContext', [kind]]);
            if (kind === 'webgpu') { return globalThis.__euvContext; }
            if (kind === '2d') { return globalThis.__euvCanvas2d; }
            return globalThis.__euvWebgl2;
        };
        globalThis.__euvCanvasElement = canvas;

        globalThis.document = node('document', globalThis.Document.prototype);
        globalThis.document.createElement = function (tag) {
            globalThis.__euvDomLog.push(['document.createElement', [tag]]);
            return node('element(' + tag + ')');
        };
        globalThis.document.querySelector = function (sel) {
            globalThis.__euvDomLog.push(['document.querySelector', [sel]]);
            if (String(sel).indexOf('canvas') >= 0) { return canvas; }
            return null;
        };
        globalThis.document.getElementById = function (id) {
            globalThis.__euvDomLog.push(['document.getElementById', [id]]);
            return canvas;
        };
        globalThis.document.body = node('document.body');
        globalThis.document.head = node('document.head');
        globalThis.document.documentElement = node('document.documentElement');

        globalThis.performance = node('performance', globalThis.Performance.prototype);
        globalThis.performance.now = function () { return 16.0; };

        const pads = [];
        globalThis.navigator = node('navigator', globalThis.Navigator.prototype);
        globalThis.navigator.getGamepads = function () { return pads; };
        globalThis.navigator.userAgent = 'euv-test';

        globalThis.requestAnimationFrame = function (cb) {
            globalThis.__euvDomLog.push(['requestAnimationFrame', []]);
            globalThis.__euvRaf = cb;
            return 1;
        };
        globalThis.cancelAnimationFrame = function () {};
        globalThis.__euvListenerCount = 0;
        globalThis.addEventListener = function (name) {
            globalThis.__euvListenerCount += 1;
            globalThis.__euvDomLog.push(['addEventListener', [name]]);
        };
        globalThis.removeEventListener = function (name) {
            globalThis.__euvDomLog.push(['removeEventListener', [name]]);
        };

        globalThis.localStorage = node('localStorage');

        Object.setPrototypeOf(globalThis, globalThis.Window.prototype);

        const win = globalThis;
        win.document = globalThis.document;
        win.navigator = globalThis.navigator;
        win.performance = globalThis.performance;
        win.requestAnimationFrame = globalThis.requestAnimationFrame;
        win.cancelAnimationFrame = globalThis.cancelAnimationFrame;
        win.addEventListener = globalThis.addEventListener;
        win.removeEventListener = globalThis.removeEventListener;
        win.localStorage = globalThis.localStorage;
        win.devicePixelRatio = 1;
        win.innerWidth = 800;
        win.innerHeight = 600;
        win.getComputedStyle = function () { return {}; };
        globalThis.window = win;

        globalThis.__euvContext = node('context');
        globalThis.__euvCanvas2d = node(
            'canvas2d', globalThis.CanvasRenderingContext2D.prototype);
        globalThis.__euvWebgl2 = node(
            'webgl2', globalThis.WebGL2RenderingContext.prototype);
        globalThis.__euvNode = node;
        "#,
    )
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "installs browser globals into the JS realm, which only exists under wasm"
)]
fn the_browser_shaped_globals_can_be_installed_into_the_node_realm() {
    let install: js_sys::Function = dom_source();
    let _: Result<JsValue, JsValue> = install.call0(&JsValue::NULL);

    assert!(
        window().is_some(),
        "window() is an instanceof check against the global Window constructor; \
         supplying both the constructor and an object carrying its prototype is what makes it \
         resolve"
    );
    let handle: Window = window().expect("just asserted");
    assert!(
        handle.document().is_some(),
        "and a window that answers with a document"
    );
    assert_eq!(
        handle.device_pixel_ratio(),
        1.0,
        "with a device pixel ratio the size helpers can read"
    );

    dom_teardown();
    assert!(
        window().is_none(),
        "and taking the environment back must leave the realm as it was found, because the          globals are process-wide: a fake document left behind would silently satisfy every          later test that asserts the engine degrades without one"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "installs browser globals into the JS realm, which only exists under wasm"
)]
fn a_webgl_backend_can_be_built_once_the_document_exists() {
    let install: js_sys::Function = dom_source();
    let _: Result<JsValue, JsValue> = install.call0(&JsValue::NULL);

    let backend: Result<WebGl2Backend, WebGl2InitError> =
        WebGl2Backend::init(&EngineConfig::default().get_render());

    let shown: String = match &backend {
        Ok(_) => String::from("Ok"),
        Err(error) => format!("{:?}", error),
    };
    dom_teardown();
    assert!(
        backend.is_ok(),
        "initialization only ever needed a document that reports a canvas and a context;          without one it short-circuits to CanvasNotFound, which is why this entry point looked          untestable rather than merely untested. Observed: {shown}"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "installs browser globals into the JS realm, which only exists under wasm"
)]
fn the_scheduler_loop_runs_when_its_animation_frame_callback_fires() {
    let install: js_sys::Function = dom_source();
    let _: Result<JsValue, JsValue> = install.call0(&JsValue::NULL);
    let (handler, updates, renders): (TickHandlerRc, Rc<Cell<u32>>, Rc<Cell<u32>>) =
        CountingHandler::spawn();

    let handle: SchedulerHandle =
        SchedulerHandle::start(SchedulerConfig::default(), handler, None, None);
    let updates_at_start: u64 = handle.update_count();
    let frames_at_start: u64 = handle.frame_count();

    let fired: bool = pump_animation_frame();
    handle.stop();
    dom_teardown();

    assert!(
        fired,
        "starting the scheduler has to register an animation-frame callback, or the loop has \
         nothing to drive it and the game never advances"
    );
    assert!(
        handle.frame_count() > frames_at_start,
        "one fired frame increments the frame counter, which is what a profiler panel reads to \
         compute fps"
    );
    assert!(
        handle.update_count() >= updates_at_start,
        "and the fixed-update counter only ever moves forward"
    );
    let _: u32 = updates.get();
    let _: u32 = renders.get();
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "installs browser globals into the JS realm, which only exists under wasm"
)]
fn a_stopped_scheduler_does_not_advance_even_if_its_callback_is_fired() {
    let install: js_sys::Function = dom_source();
    let _: Result<JsValue, JsValue> = install.call0(&JsValue::NULL);
    let (handler, _updates, _renders): (TickHandlerRc, Rc<Cell<u32>>, Rc<Cell<u32>>) =
        CountingHandler::spawn();

    let handle: SchedulerHandle =
        SchedulerHandle::start(SchedulerConfig::default(), handler, None, None);
    handle.stop();
    let after_stop: u64 = handle.frame_count();

    let _: bool = pump_animation_frame();
    dom_teardown();

    assert!(
        !handle.is_running(),
        "stopping has to flip the running flag, or the next animation frame keeps ticking"
    );
    assert_eq!(
        handle.frame_count(),
        after_stop,
        "and a callback that fires after the stop must not count another frame"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "installs browser globals into the JS realm, which only exists under wasm"
)]
fn attaching_input_registers_listeners_and_hands_back_the_same_state_cell() {
    let install: js_sys::Function = dom_source();
    let _: Result<JsValue, JsValue> = install.call0(&JsValue::NULL);
    let window: Window = window().expect("the environment is installed");
    let cell: InputStateCell = Rc::new(EngineCell::new(InputState::new()));

    let returned: InputStateCell = Input::attach(
        Rc::clone(&cell),
        &window,
        &window.clone().unchecked_into::<EventTarget>(),
    );

    let registered: u32 = listener_count();
    dom_teardown();

    assert!(
        Rc::ptr_eq(&cell, &returned),
        "attach is documented to hand the cell back, and returning a different one would leave \
         the caller mutating a state the event handlers never write to"
    );
    assert!(
        registered > 0,
        "and it has to actually register listeners, not just return the cell — otherwise a \
         caller believes input is wired up while no event ever reaches the state"
    );
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
#[cfg_attr(
    not(target_arch = "wasm32"),
    ignore = "installs browser globals into the JS realm, which only exists under wasm"
)]
fn a_supersampled_canvas_builds_its_offscreen_buffer_and_blits_on_present() {
    let install: js_sys::Function = dom_source();
    let _: Result<JsValue, JsValue> = install.call0(&JsValue::NULL);

    let canvas: Option<SsaaCanvas> = SsaaCanvas::from_selector("canvas", 800.0, 600.0);
    let presentable: bool = canvas.is_some();
    if presentable {
        let subject: SsaaCanvas = canvas.expect("just checked");
        subject.present();
    }
    dom_teardown();

    assert!(
        presentable,
        "with a document that reports a canvas and a 2D context, the supersampled canvas has \
         to build; before the environment existed this entry point could not be reached at all"
    );
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test::wasm_bindgen_test]
async fn the_capability_probe_answers_false_where_there_is_no_navigator_at_all() {
    assert!(
        !WebGpuRenderer::probe().await,
        "a host with no window exposes no navigator and therefore no `gpu`; the probe is the \
         cheap pre-flight a caller runs before promising a GPU-backed canvas, and answering true \
         here would send it down a path that ends in a blank canvas"
    );
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen_test::wasm_bindgen_test]
async fn running_the_engine_on_the_canvas_backend_brings_up_a_renderer_and_a_running_loop() {
    let install: js_sys::Function = dom_source();
    let _: Result<JsValue, JsValue> = install.call0(&JsValue::NULL);
    let (handler, updates, _renders): (TickHandlerRc, Rc<Cell<u32>>, Rc<Cell<u32>>) =
        CountingHandler::spawn();

    let handle: EngineHandle = Engine::run(EngineConfig::default(), handler).await;
    let canvas_up: bool = handle.get_canvas_renderer().is_some();
    let pumping: bool = pump_animation_frame();
    handle.stop();
    dom_teardown();

    assert!(
        canvas_up,
        "run has to bring up the renderer the configuration asks for; the default config is the \
         Canvas 2D backend, so an empty handle after run means the frame has nothing to draw on"
    );
    assert!(
        pumping,
        "and it has to register an animation-frame callback, or the loop never advances"
    );
    assert_eq!(
        updates.get(),
        1,
        "one pumped frame drives exactly one fixed update"
    );
}